{ "nbformat": 4, "nbformat_minor": 0, "metadata": { "colab": { "provenance": [] }, "kernelspec": { "name": "python3", "display_name": "Python 3" }, "language_info": { "name": "python" } }, "cells": [ { "cell_type": "code", "execution_count": 1, "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 72 }, "id": "fWDlrdFPI2FD", "outputId": "99718d98-50a4-47d8-ed4a-8b0c73bbd9d1" }, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "" ], "text/html": [ "\n", " \n", " \n", " Upload widget is only available when the cell has been executed in the\n", " current browser session. Please rerun this cell to enable.\n", " \n", " " ] }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "Saving volve_grounded_events.csv to volve_grounded_events.csv\n" ] } ], "source": [ "from google.colab import files\n", "uploaded = files.upload()" ] }, { "cell_type": "code", "source": [ "uploaded = files.upload()" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 72 }, "id": "w_vWhOpDJIKD", "outputId": "ccd6a515-0b70-4439-fbbb-1ac166da0e28" }, "execution_count": 2, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "" ], "text/html": [ "\n", " \n", " \n", " Upload widget is only available when the cell has been executed in the\n", " current browser session. Please rerun this cell to enable.\n", " \n", " " ] }, "metadata": {} }, { "output_type": "stream", "name": "stdout", "text": [ "Saving volve_well_daily.csv to volve_well_daily.csv\n" ] } ] }, { "cell_type": "code", "source": [ "import pandas as pd\n", "import numpy as np" ], "metadata": { "id": "a2aevc8JI5YT" }, "execution_count": 3, "outputs": [] }, { "cell_type": "code", "source": [ "import tensorflow as tf\n", "from tensorflow.keras import layers, models" ], "metadata": { "id": "KY0mLUrNSc2J" }, "execution_count": 8, "outputs": [] }, { "cell_type": "code", "source": [ "from tensorflow.keras import layers, models, regularizers\n", "from sklearn.metrics import roc_auc_score, average_precision_score" ], "metadata": { "id": "2s0TV8u8TElC" }, "execution_count": 11, "outputs": [] }, { "cell_type": "code", "source": [ "from tensorflow.keras import layers, models, regularizers" ], "metadata": { "id": "QOHOmUpsXJkT" }, "execution_count": 17, "outputs": [] }, { "cell_type": "code", "source": [ "from sklearn.metrics import roc_auc_score, mean_absolute_error, accuracy_score\n" ], "metadata": { "id": "GDDw3y7HXMU9" }, "execution_count": 20, "outputs": [] }, { "cell_type": "code", "source": [ "# SUPERVISED dual-head model notebook.\n", "'''\n", "Same single source and same preparation decisions as the baseline notebook.\n", "Condensed here so this notebook stands alone.\n", "'''\n", "\n", "data = pd.read_csv('volve_well_daily.csv', parse_dates=['date'])\n", "ev = pd.read_csv('volve_grounded_events.csv', parse_dates=['onset','offset'])\n", "\n", "# oil producers only; zero = missing; 3-day per-well fill; reduced reliable channels + deltas\n", "op = data[data.well_type == 'OP'].copy()\n", "core_features = ['dp_tubing','whp','wht','choke_pct','oil_vol','gas_vol','water_vol']\n", "op[core_features] = op[core_features].replace(0, np.nan)\n", "op = op.sort_values(['well','date']).reset_index(drop=True)\n", "op[core_features] = op.groupby('well')[core_features].ffill(limit=3)\n", "for c in core_features:\n", " op[c + '_delta'] = op.groupby('well')[c].diff()\n", "\n", "model_features = core_features + [c + '_delta' for c in core_features]\n", "trainable_wells = ['15/9-F-1 C','15/9-F-11','15/9-F-12','15/9-F-14','15/9-F-15 D']\n", "\n", "print(\"Prepared:\", op.shape, \"|\", len(model_features), \"features |\", len(trainable_wells), \"wells\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "4jxcT9n5JNoH", "outputId": "3de2cc2e-727d-4d14-d953-3e95cb7a5e40" }, "execution_count": 4, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Prepared: (9143, 24) | 14 features | 5 wells\n" ] } ] }, { "cell_type": "markdown", "source": [ "### From daily rows to windows\n", "\n", "The baseline scored one day at a time. This model marks *where an event starts and ends*, so it needs to see a stretch of time, not a single day. We therefore slide a **60-day window** forward **7 days at a time** across each well.\n", "\n", "Each window becomes one training example: a 60×14 block of features, plus any grounded events that fall inside it, recorded as their **start and end position within the window** (0–59) and their type. This within-window interval is the 1D, time-based analogue of the bounding box the metal-AM defect model drew around a defect in an image in a previous published paper.\n", "\n", "A window with no event is a *background* window. The equivalent of an image with no defect. Window length (60) and step (7) are declared assumptions." ], "metadata": { "id": "br66b6WXJwbz" } }, { "cell_type": "code", "source": [ "'''\n", "Each window is one training example: a 60x14 feature block + any events that fall inside it, recorded as (start, end) positions WITHIN the window plus type.\n", "This is the 1D interval label, the time analogue of the metal-AM bounding box.\n", "'''\n", "\n", "WINDOW = 60\n", "STEP = 7\n", "type_to_id = {'shut_in':1,'restart_transient':2,'water_breakthrough':3,\n", " 'productivity_loss':4,'gaslift_instability':5} # 0 = background\n", "\n", "windows = [] # each: dict(well, start_date, X, events=[(s,e,type_id), ...])\n", "\n", "for well in trainable_wells:\n", " g = op[op.well == well].sort_values('date').reset_index(drop=True)\n", " g_feats = g[model_features].values\n", " g_dates = pd.DatetimeIndex(g['date']) # proper datetime index\n", " n = len(g)\n", " evw = ev[ev.well == well]\n", "\n", " for start in range(0, n - WINDOW + 1, STEP):\n", " end = start + WINDOW\n", " X = g_feats[start:end] # 60 x 14\n", " if np.isnan(X).mean() > 0.2: # skip too-empty windows\n", " continue\n", " w_start_date = g_dates[start]\n", " w_end_date = g_dates[end-1]\n", "\n", " evlist = []\n", " for _, e in evw.iterrows():\n", " if e.offset < w_start_date or e.onset > w_end_date:\n", " continue # no overlap\n", " s_idx = max(0, g_dates.searchsorted(e.onset) - start)\n", " e_idx = min(WINDOW-1, g_dates.searchsorted(e.offset) - start)\n", " if e_idx >= s_idx:\n", " evlist.append((int(s_idx), int(e_idx), type_to_id[e.event_type]))\n", "\n", " windows.append(dict(well=well, start_date=w_start_date, X=X, events=evlist))\n", "\n", "print(f\"Total windows: {len(windows)}\")\n", "n_with = sum(1 for w in windows if w['events'])\n", "print(f\"Windows containing >=1 event: {n_with} ({100*n_with/len(windows):.0f}%)\")\n", "print(f\"Background windows (no event): {len(windows)-n_with}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "rac83f6iJyoo", "outputId": "f3834ef5-900f-4eca-d6d5-aa0a5ea6d65e" }, "execution_count": 6, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Total windows: 1141\n", "Windows containing >=1 event: 584 (51%)\n", "Background windows (no event): 557\n" ] } ] }, { "cell_type": "code", "source": [ "from sklearn.preprocessing import StandardScaler\n", "\n", "'''\n", "We are using a per-well normalization (consistent with the baseline):\n", "fit each well's scaler on that well's full producer history, so every window from that well is judged against that well's own normal.\n", "Then fill any remaining within-window NaNs. Reason for per-well: channels have different units, and a pooled scaler would flag a well as anomalous merely for running at different levels.\n", "'''\n", "# 1. fit one scaler per well on its full history\n", "well_scalers = {}\n", "for well in trainable_wells:\n", " g = op[op.well == well]\n", " well_scalers[well] = StandardScaler().fit(g[model_features].values)\n", "\n", "# 2. apply to each window, then fill remaining NaNs with 0 (= the well's mean, since after scaling mean is 0).\n", "#Filling with the mean is the neutral choice because it says \"no information\" rather than inventing a value.\n", "X_all, y_has_event, well_of = [], [], []\n", "for w in windows:\n", " Xs = well_scalers[w['well']].transform(w['X']) # 60 x 14, scaled\n", " Xs = np.nan_to_num(Xs, nan=0.0) # fill gaps with scaled-mean (0)\n", " X_all.append(Xs)\n", " y_has_event.append(1 if w['events'] else 0)\n", " well_of.append(w['well'])\n", "\n", "X_all = np.array(X_all) # (1141, 60, 14)\n", "y_has_event = np.array(y_has_event)\n", "\n", "print(\"Feature tensor:\", X_all.shape, \"(windows, days, channels)\")\n", "print(\"Any NaN left?\", np.isnan(X_all).any())\n", "print(\"Event-present labels:\", y_has_event.sum(), \"/\", len(y_has_event))" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "eLZWVFSwQy8m", "outputId": "b6b3d5ef-29b4-4265-c5c1-963d2dc4f3f0" }, "execution_count": 7, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Feature tensor: (1141, 60, 14) (windows, days, channels)\n", "Any NaN left? False\n", "Event-present labels: 584 / 1141\n" ] } ] }, { "cell_type": "code", "source": [ "\n", "\n", "print(\"TensorFlow:\", tf.__version__)\n", "\n", "'''Leave-one-well-out: each well takes a turn as the test set while the other four train.\n", "No window from a test well ever appears in training, so the heavy window overlap (~88% between neighbors) cannot leak the answer.\n", "'''\n", "\n", "well_of = np.array(well_of)\n", "\n", "def split_leave_one_out(test_well):\n", " tr = well_of != test_well\n", " te = well_of == test_well\n", " return X_all[tr], y_has_event[tr], X_all[te], y_has_event[te]\n", "\n", "# quick check of the per-fold sizes and event balance\n", "print(\"\\nLeave-one-well-out folds:\")\n", "for w in trainable_wells:\n", " Xtr, ytr, Xte, yte = split_leave_one_out(w)\n", " print(f\" hold out {w:14s}: train {len(ytr):4d} (ev {ytr.sum():3d}) | \"\n", " f\"test {len(yte):4d} (ev {yte.sum():3d})\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "AGN6e8akSZIO", "outputId": "c378a7fe-dfbe-4af6-99b7-9efca7c17f95" }, "execution_count": 10, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "TensorFlow: 2.20.0\n", "\n", "Leave-one-well-out folds:\n", " hold out 15/9-F-1 C : train 1097 (ev 544) | test 44 (ev 40)\n", " hold out 15/9-F-11 : train 985 (ev 526) | test 156 (ev 58)\n", " hold out 15/9-F-12 : train 720 (ev 375) | test 421 (ev 209)\n", " hold out 15/9-F-14 : train 738 (ev 396) | test 403 (ev 188)\n", " hold out 15/9-F-15 D : train 1024 (ev 495) | test 117 (ev 89)\n" ] } ] }, { "cell_type": "code", "source": [ "# STEP ONE: does a window contain an event at all? (binary)Compact + regularized on purpose (small, overlapping data — Section 8.3).\n", "# Goal: confirm the pipeline can learn and generalize across wells before we add the harder localization head.\n", "\n", "def build_presence_model(n_days=60, n_ch=14):\n", " m = models.Sequential([\n", " layers.Input(shape=(n_days, n_ch)),\n", " layers.Conv1D(16, 5, activation='relu',\n", " kernel_regularizer=regularizers.l2(1e-3)),\n", " layers.MaxPooling1D(2),\n", " layers.Conv1D(32, 3, activation='relu',\n", " kernel_regularizer=regularizers.l2(1e-3)),\n", " layers.GlobalAveragePooling1D(),\n", " layers.Dropout(0.5),\n", " layers.Dense(16, activation='relu'),\n", " layers.Dense(1, activation='sigmoid'),\n", " ])\n", " m.compile(optimizer='adam', loss='binary_crossentropy',\n", " metrics=['accuracy'])\n", " return m\n", "\n", "# Leave-one-well-out, collect held-out predictions\n", "results = {}\n", "for test_well in trainable_wells:\n", " Xtr, ytr, Xte, yte = split_leave_one_out(test_well)\n", " tf.random.set_seed(0) # reproducible\n", " model = build_presence_model()\n", " model.fit(Xtr, ytr, epochs=30, batch_size=32, verbose=0)\n", " p = model.predict(Xte, verbose=0).ravel()\n", " # AUC only meaningful if both classes present in test fold\n", " if len(np.unique(yte)) == 2:\n", " auc = roc_auc_score(yte, p)\n", " ap = average_precision_score(yte, p)\n", " else:\n", " auc = ap = float('nan')\n", " results[test_well] = (auc, ap, yte.mean())\n", " print(f\"hold out {test_well:14s}: ROC-AUC {auc:.3f} PR-AUC {ap:.3f} \"\n", " f\"(test event rate {yte.mean():.2f})\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "1eTcN5OJSidJ", "outputId": "317c5917-cd99-4d04-87ca-210ca4a17523" }, "execution_count": 12, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "hold out 15/9-F-1 C : ROC-AUC 0.850 PR-AUC 0.986 (test event rate 0.91)\n", "hold out 15/9-F-11 : ROC-AUC 0.876 PR-AUC 0.742 (test event rate 0.37)\n" ] }, { "output_type": "stream", "name": "stderr", "text": [ "WARNING:tensorflow:5 out of the last 8 calls to .one_step_on_data_distributed at 0x7cc6bee06520> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "hold out 15/9-F-12 : ROC-AUC 0.899 PR-AUC 0.929 (test event rate 0.50)\n", "hold out 15/9-F-14 : ROC-AUC 0.833 PR-AUC 0.871 (test event rate 0.47)\n", "hold out 15/9-F-15 D : ROC-AUC 0.952 PR-AUC 0.987 (test event rate 0.76)\n" ] } ] }, { "cell_type": "markdown", "source": [ "### Step-one result: the pipeline learns and generalizes across wells\n", "\n", "The event-presence model trained on four wells and tested on the held-out fifth, rotated through all five, successfully tells event-windows from background on wells it never saw during training.\n", "\n", "The two folds we trust most, because their test sets are large and balanced, are strong: **ROC-AUC 0.899** holding out F-12 (50% event rate) and **0.833** holding out F-14 (47%). The other three folds agree (F-15D 0.952, F-11 0.876, F-1C 0.850) but rest on small or skewed test sets, F-1C in particular is 91% positive, so its high scores are inflated and should not be read as strong evidence.\n", "\n", "The point that matters: on small, heavily-overlapping data where cross-well transfer was not guaranteed, a model trained on four wells still generalizes to a fifth. The pipeline learns a real, transferable signal. This earns the next step; adding the localization head to build the full dual-head model.\n", "\n", "*Caveat, as everywhere:* recovering the grounded events is not the same as detecting confirmed faults (see label-construction note)." ], "metadata": { "id": "TS9Pih24TtvN" } }, { "cell_type": "markdown", "source": [ "# Next step:\n", "STEP TWO labels. For each window we keep ONE event,the primary one, and record: onset (0-59), offset (0-59), type (1-5).\n", "Background windows get onset=offset=0 and type=0, and are masked out of the localization loss later.\n", "\"Primary event\" = the longest event in the window (most salient); ties broken by earliest onset. Multi-event windows are simplified to this primary event; full multi-event prediction is future work. (Decision stated in method section.)" ], "metadata": { "id": "0rRw099JURC4" } }, { "cell_type": "code", "source": [ "y_present = [] # 1 if window has an event, else 0\n", "y_onset = [] # start position within window (0-59)\n", "y_offset = [] # end position within window (0-59)\n", "y_type = [] # 0 background, 1-5 event type\n", "\n", "for w in windows:\n", " if not w['events']:\n", " y_present.append(0); y_onset.append(0); y_offset.append(0); y_type.append(0)\n", " continue\n", " # pick primary = longest span; tie -> earliest onset\n", " primary = sorted(w['events'], key=lambda e: (-(e[1]-e[0]), e[0]))[0]\n", " s, e, t = primary\n", " y_present.append(1); y_onset.append(s); y_offset.append(e); y_type.append(t)\n", "\n", "y_present = np.array(y_present, dtype='float32')\n", "y_onset = np.array(y_onset, dtype='float32') / (WINDOW-1) # scale to 0-1\n", "y_offset = np.array(y_offset, dtype='float32') / (WINDOW-1) # scale to 0-1\n", "y_type = np.array(y_type, dtype='int32')\n", "\n", "print(\"Label arrays built.\")\n", "print(\" present:\", y_present.shape, \"-> events:\", int(y_present.sum()))\n", "print(\" onset/offset scaled to 0-1 (divide by 59)\")\n", "print(\" type distribution:\", {int(k): int((y_type==k).sum()) for k in range(6)})" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "KP5MLSHuUUjK", "outputId": "df640af9-f819-4eae-a110-c36d7d3dcc06" }, "execution_count": 14, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Label arrays built.\n", " present: (1141,) -> events: 584\n", " onset/offset scaled to 0-1 (divide by 59)\n", " type distribution: {0: 557, 1: 411, 2: 12, 3: 39, 4: 46, 5: 76}\n" ] } ] }, { "cell_type": "markdown", "source": [ "`\n", "### Merging shut-in and restart\n", "\n", "The one-label-per-window rule created a problem: shut-in and restart almost always occur together in a window, and because restart is short, the \"pick the longest event\" rule always dropped it — collapsing restart from 79 events to just 12.\n", "\n", "Rather than engineer around this, we **merge shut-in and restart into a single \"shut-in/restart cycle\" class**. This is physically honest, they are one operational episode (a well stops, then restarts), and it recovers the restart data instead of discarding it. The result is a cleaner four-class problem where every class has enough examples to learn from.\n", "\n", "We document this openly: the merge is a consequence of the one-event-per-window simplification, and full separation of paired events is future work.\n" ], "metadata": { "id": "d9PYGDwgWPjP" } }, { "cell_type": "code", "source": [ "\n", "# remap types: 1&2 -> 1 (cycle); 3->2 (water_bt); 4->3 (prod_loss); 5->4 (gaslift)\n", "remap = {0:0, 1:1, 2:1, 3:2, 4:3, 5:4}\n", "class_names = {0:'background', 1:'shutin_restart_cycle',\n", " 2:'water_breakthrough', 3:'productivity_loss', 4:'gaslift_instability'}\n", "\n", "y_present, y_onset, y_offset, y_type = [], [], [], []\n", "for w in windows:\n", " if not w['events']:\n", " y_present.append(0); y_onset.append(0); y_offset.append(0); y_type.append(0)\n", " continue\n", " # primary = longest span (ties -> earliest onset), then remap its type\n", " primary = sorted(w['events'], key=lambda e: (-(e[1]-e[0]), e[0]))[0]\n", " s, e, t = primary\n", " y_present.append(1); y_onset.append(s); y_offset.append(e)\n", " y_type.append(remap[t])\n", "\n", "y_present = np.array(y_present, dtype='float32')\n", "y_onset = np.array(y_onset, dtype='float32') / (WINDOW-1)\n", "y_offset = np.array(y_offset, dtype='float32') / (WINDOW-1)\n", "y_type = np.array(y_type, dtype='int32')\n", "\n", "print(\"Merged label arrays built.\")\n", "print(\" events:\", int(y_present.sum()), \"/\", len(y_present))\n", "print(\" class distribution:\")\n", "for k in range(5):\n", " print(f\" {k} {class_names[k]:24s}: {int((y_type==k).sum())}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "bzDZFO0cWXmk", "outputId": "86759eb8-8526-46c3-982c-dff81fb192c3" }, "execution_count": 15, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Merged label arrays built.\n", " events: 584 / 1141\n", " class distribution:\n", " 0 background : 557\n", " 1 shutin_restart_cycle : 423\n", " 2 water_breakthrough : 39\n", " 3 productivity_loss : 46\n", " 4 gaslift_instability : 76\n" ] } ] }, { "cell_type": "code", "source": [ "'''\n", "DUAL-HEAD MODEL (step two). Shared 1D-conv backbone (proven in step one),splitting into three heads: presence (is there an event),\n", "interval (onset/offset = the 1D localization \"box\"), and type (4-class).\n", "This extends the metal-AM localize-plus-classify architecture from 2D image defects to 1D temporal events.\n", "This is Compact + regularized for the small, overlapping data\n", "'''\n", "\n", "def build_dualhead(n_days=60, n_ch=14, n_types=5):\n", " inp = layers.Input(shape=(n_days, n_ch))\n", " x = layers.Conv1D(16, 5, activation='relu',\n", " kernel_regularizer=regularizers.l2(1e-3))(inp)\n", " x = layers.MaxPooling1D(2)(x)\n", " x = layers.Conv1D(32, 3, activation='relu',\n", " kernel_regularizer=regularizers.l2(1e-3))(x)\n", " x = layers.GlobalAveragePooling1D()(x)\n", " x = layers.Dropout(0.5)(x)\n", " shared = layers.Dense(32, activation='relu')(x)\n", "\n", " # three heads off the shared representation\n", " presence = layers.Dense(1, activation='sigmoid', name='presence')(shared)\n", " interval = layers.Dense(2, activation='sigmoid', name='interval')(shared) # onset,offset in 0-1\n", " etype = layers.Dense(n_types, activation='softmax', name='etype')(shared)\n", "\n", " model = models.Model(inp, [presence, interval, etype])\n", " model.compile(\n", " optimizer='adam',\n", " loss={'presence':'binary_crossentropy',\n", " 'interval':'mse', # regression on onset/offset\n", " 'etype':'sparse_categorical_crossentropy'},\n", " loss_weights={'presence':1.0, 'interval':1.0, 'etype':1.0},\n", " metrics={'presence':'accuracy', 'etype':'accuracy'})\n", " return model\n", "\n", "m = build_dualhead()\n", "m.summary()" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 510 }, "id": "XCEgvD9vWo5U", "outputId": "05965416-1f0a-4a59-bd31-00e333c79cf8" }, "execution_count": 16, "outputs": [ { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1mModel: \"functional_5\"\u001b[0m\n" ], "text/html": [ "
Model: \"functional_5\"\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n", "┃\u001b[1m \u001b[0m\u001b[1mLayer (type) \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mOutput Shape \u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1m Param #\u001b[0m\u001b[1m \u001b[0m┃\u001b[1m \u001b[0m\u001b[1mConnected to \u001b[0m\u001b[1m \u001b[0m┃\n", "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n", "│ input_layer_5 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m60\u001b[0m, \u001b[38;5;34m14\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ - │\n", "│ (\u001b[38;5;33mInputLayer\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ conv1d_10 (\u001b[38;5;33mConv1D\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m56\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m1,136\u001b[0m │ input_layer_5[\u001b[38;5;34m0\u001b[0m]… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ max_pooling1d_5 │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m28\u001b[0m, \u001b[38;5;34m16\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ conv1d_10[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mMaxPooling1D\u001b[0m) │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ conv1d_11 (\u001b[38;5;33mConv1D\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m26\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m1,568\u001b[0m │ max_pooling1d_5[\u001b[38;5;34m…\u001b[0m │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ global_average_poo… │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ conv1d_11[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "│ (\u001b[38;5;33mGlobalAveragePool…\u001b[0m │ │ │ │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dropout_5 (\u001b[38;5;33mDropout\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m0\u001b[0m │ global_average_p… │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ dense_10 (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m32\u001b[0m) │ \u001b[38;5;34m1,056\u001b[0m │ dropout_5[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ presence (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m1\u001b[0m) │ \u001b[38;5;34m33\u001b[0m │ dense_10[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ interval (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m2\u001b[0m) │ \u001b[38;5;34m66\u001b[0m │ dense_10[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n", "│ etype (\u001b[38;5;33mDense\u001b[0m) │ (\u001b[38;5;45mNone\u001b[0m, \u001b[38;5;34m5\u001b[0m) │ \u001b[38;5;34m165\u001b[0m │ dense_10[\u001b[38;5;34m0\u001b[0m][\u001b[38;5;34m0\u001b[0m] │\n", "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n" ], "text/html": [ "
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓\n",
              "┃ Layer (type)         Output Shape          Param #  Connected to      ┃\n",
              "┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩\n",
              "│ input_layer_5       │ (None, 60, 14)    │          0 │ -                 │\n",
              "│ (InputLayer)        │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ conv1d_10 (Conv1D)  │ (None, 56, 16)    │      1,136 │ input_layer_5[0]… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ max_pooling1d_5     │ (None, 28, 16)    │          0 │ conv1d_10[0][0]   │\n",
              "│ (MaxPooling1D)      │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ conv1d_11 (Conv1D)  │ (None, 26, 32)    │      1,568 │ max_pooling1d_5[ │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ global_average_poo… │ (None, 32)        │          0 │ conv1d_11[0][0]   │\n",
              "│ (GlobalAveragePool… │                   │            │                   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dropout_5 (Dropout) │ (None, 32)        │          0 │ global_average_p… │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ dense_10 (Dense)    │ (None, 32)        │      1,056 │ dropout_5[0][0]   │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ presence (Dense)    │ (None, 1)         │         33 │ dense_10[0][0]    │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ interval (Dense)    │ (None, 2)         │         66 │ dense_10[0][0]    │\n",
              "├─────────────────────┼───────────────────┼────────────┼───────────────────┤\n",
              "│ etype (Dense)       │ (None, 5)         │        165 │ dense_10[0][0]    │\n",
              "└─────────────────────┴───────────────────┴────────────┴───────────────────┘\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Total params: \u001b[0m\u001b[38;5;34m4,024\u001b[0m (15.72 KB)\n" ], "text/html": [ "
 Total params: 4,024 (15.72 KB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Trainable params: \u001b[0m\u001b[38;5;34m4,024\u001b[0m (15.72 KB)\n" ], "text/html": [ "
 Trainable params: 4,024 (15.72 KB)\n",
              "
\n" ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "\u001b[1m Non-trainable params: \u001b[0m\u001b[38;5;34m0\u001b[0m (0.00 B)\n" ], "text/html": [ "
 Non-trainable params: 0 (0.00 B)\n",
              "
\n" ] }, "metadata": {} } ] }, { "cell_type": "markdown", "source": [ "Masking: interval and type losses apply ONLY to event-containing windows (background windows have no interval/type to learn). We pass per-sample weights = y_present, so those two heads ignore background windows. Presence head uses all windows." ], "metadata": { "id": "PNsPQbaQXb8e" } }, { "cell_type": "code", "source": [ "\n", "def split_idx(test_well):\n", " tr = np.where(well_of != test_well)[0]\n", " te = np.where(well_of == test_well)[0]\n", " return tr, te\n", "\n", "dh_results = {}\n", "for test_well in trainable_wells:\n", " tr, te = split_idx(test_well)\n", " tf.random.set_seed(0)\n", " model = build_dualhead()\n", "\n", " # targets in OUTPUT ORDER: [presence, interval, etype]\n", " y_tr = [\n", " y_present[tr],\n", " np.stack([y_onset[tr], y_offset[tr]], axis=1),\n", " y_type[tr],\n", " ]\n", " # sample weights in the SAME order; interval/type only on event windows\n", " sw = [\n", " np.ones(len(tr), dtype='float32'), # presence: all windows\n", " y_present[tr], # interval: events only\n", " y_present[tr], # etype: events only\n", " ]\n", "\n", " model.fit(X_all[tr], y_tr, sample_weight=sw,\n", " epochs=40, batch_size=32, verbose=0)\n", "\n", " pres_p, intv_p, type_p = model.predict(X_all[te], verbose=0)\n", " pres_p = pres_p.ravel()\n", "\n", " yte_pres = y_present[te]\n", " auc = roc_auc_score(yte_pres, pres_p) if len(np.unique(yte_pres))==2 else np.nan\n", "\n", " ev_mask = yte_pres == 1\n", " if ev_mask.sum() > 0:\n", " onset_mae = mean_absolute_error(y_onset[te][ev_mask], intv_p[ev_mask,0])\n", " offset_mae = mean_absolute_error(y_offset[te][ev_mask], intv_p[ev_mask,1])\n", " type_acc = accuracy_score(y_type[te][ev_mask], type_p[ev_mask].argmax(1))\n", " else:\n", " onset_mae = offset_mae = type_acc = np.nan\n", "\n", " dh_results[test_well] = dict(auc=auc, onset_mae=onset_mae,\n", " offset_mae=offset_mae, type_acc=type_acc)\n", " print(f\"hold out {test_well:14s}: presence AUC {auc:.3f} | \"\n", " f\"onset MAE {onset_mae:.3f} offset MAE {offset_mae:.3f} | \"\n", " f\"type acc {type_acc:.3f}\")" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/" }, "id": "vcKNAShzXUPP", "outputId": "1782287f-d078-4afc-db67-dc596c2c7652" }, "execution_count": 21, "outputs": [ { "output_type": "stream", "name": "stderr", "text": [ "WARNING:tensorflow:5 out of the last 18 calls to .one_step_on_data_distributed at 0x7cc6be1732e0> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has reduce_retracing=True option that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for more details.\n" ] }, { "output_type": "stream", "name": "stdout", "text": [ "hold out 15/9-F-1 C : presence AUC 0.881 | onset MAE 0.240 offset MAE 0.204 | type acc 0.650\n", "hold out 15/9-F-11 : presence AUC 0.873 | onset MAE 0.280 offset MAE 0.279 | type acc 0.621\n", "hold out 15/9-F-12 : presence AUC 0.907 | onset MAE 0.292 offset MAE 0.293 | type acc 0.794\n", "hold out 15/9-F-14 : presence AUC 0.819 | onset MAE 0.287 offset MAE 0.289 | type acc 0.511\n", "hold out 15/9-F-15 D : presence AUC 0.939 | onset MAE 0.296 offset MAE 0.313 | type acc 0.775\n" ] } ] }, { "cell_type": "markdown", "source": [ "# Training one final model on all data\n", "FINAL MODEL for release: trained on ALL five wells (no held-out test). The reported metrics come from the leave-one-well-out evaluation;this all-data model is the usable artifact for sharing, not for reporting numbers on." ], "metadata": { "id": "eg-lr5JQYqed" } }, { "cell_type": "code", "source": [ "\n", "tf.random.set_seed(0)\n", "final_model = build_dualhead()\n", "\n", "y_all = [\n", " y_present,\n", " np.stack([y_onset, y_offset], axis=1),\n", " y_type,\n", "]\n", "sw_all = [\n", " np.ones(len(y_present), dtype='float32'), # presence: all windows\n", " y_present, # interval: events only\n", " y_present, # etype: events only\n", "]\n", "\n", "final_model.fit(X_all, y_all, sample_weight=sw_all,\n", " epochs=40, batch_size=32, verbose=0)\n", "\n", "final_model.save('volve_dualhead_model.keras')\n", "print(\"Saved: volve_dualhead_model.keras\")\n", "\n", "reloaded = tf.keras.models.load_model('volve_dualhead_model.keras')\n", "check = reloaded.predict(X_all[:3], verbose=0)\n", "print(\"Reload OK presence preds on 3 windows:\", check[0].ravel().round(3))\n", "\n", "\n", "files.download('volve_dualhead_model.keras')" ], "metadata": { "colab": { "base_uri": "https://localhost:8080/", "height": 52 }, "id": "DV-KqErZYuZK", "outputId": "84241355-e417-47a7-d6d1-6ea0fc958a8a" }, "execution_count": 22, "outputs": [ { "output_type": "stream", "name": "stdout", "text": [ "Saved: volve_dualhead_model.keras\n", "Reload OK presence preds on 3 windows: [0.556 0.102 0.136]\n" ] }, { "output_type": "display_data", "data": { "text/plain": [ "" ], "application/javascript": [ "\n", " async function download(id, filename, size) {\n", " if (!google.colab.kernel.accessAllowed) {\n", " return;\n", " }\n", " const div = document.createElement('div');\n", " const label = document.createElement('label');\n", " label.textContent = `Downloading \"${filename}\": `;\n", " div.appendChild(label);\n", " const progress = document.createElement('progress');\n", " progress.max = size;\n", " div.appendChild(progress);\n", " document.body.appendChild(div);\n", "\n", " const buffers = [];\n", " let downloaded = 0;\n", "\n", " const channel = await google.colab.kernel.comms.open(id);\n", " // Send a message to notify the kernel that we're ready.\n", " channel.send({})\n", "\n", " for await (const message of channel.messages) {\n", " // Send a message to notify the kernel that we're ready.\n", " channel.send({})\n", " if (message.buffers) {\n", " for (const buffer of message.buffers) {\n", " buffers.push(buffer);\n", " downloaded += buffer.byteLength;\n", " progress.value = downloaded;\n", " }\n", " }\n", " }\n", " const blob = new Blob(buffers, {type: 'application/binary'});\n", " const a = document.createElement('a');\n", " a.href = window.URL.createObjectURL(blob);\n", " a.download = filename;\n", " div.appendChild(a);\n", " a.click();\n", " div.remove();\n", " }\n", " " ] }, "metadata": {} }, { "output_type": "display_data", "data": { "text/plain": [ "" ], "application/javascript": [ "download(\"download_c9c66de3-90b3-4005-a542-f66f498484c3\", \"volve_dualhead_model.keras\", 100124)" ] }, "metadata": {} } ] }, { "cell_type": "markdown", "source": [ "### Final model for release\n", "\n", "This is a single dual-head model trained on **all five wells** (no held-out test). It is the usable artifact for sharing — the reported metrics come from the leave-one-well-out evaluation above, while this all-data model is what someone would download and run on new data.\n", "\n", "Saved as `volve_dualhead_model.keras`. The reload check predicts on just the first 3 windows to confirm the file works, the model itself handles any number of windows.\n", "\n", "**To use it on new data:** the input must be shaped `(N, 60, 14)` and prepared the same way as here (oil producers, reduced reliable-channel features, per-well normalization). Raw telemetry fed in directly will not give meaningful results." ], "metadata": { "id": "d1qH1NQEZx22" } } ] }