gea-program 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gea/BENCH_TEST_PROTOCOL.md +97 -0
- gea/IMPORT_RECORD.md +61 -0
- gea/__init__.py +160 -0
- gea/__main__.py +661 -0
- gea/acceptance_tests.py +1315 -0
- gea/accuracy_statement.py +181 -0
- gea/alarm_engine.py +307 -0
- gea/bench.py +144 -0
- gea/blind_harness.py +108 -0
- gea/case_study.py +229 -0
- gea/catalog/acex_lomonosov_age_depth_model.provenance.json +23 -0
- gea/catalog/acex_lomonosov_age_depth_model.txt +28 -0
- gea/catalog/agassiz77_canada_temperature.csv +68 -0
- gea/catalog/agassiz77_canada_temperature.provenance.json +17 -0
- gea/catalog/barbados_110_consolidation.provenance.json +24 -0
- gea/catalog/barbados_110_consolidation.txt +91 -0
- gea/catalog/bengal_u1452_grain_size.provenance.json +20 -0
- gea/catalog/bengal_u1452_grain_size.txt +252 -0
- gea/catalog/blake_164_methane_isotopes.provenance.json +22 -0
- gea/catalog/blake_164_methane_isotopes.txt +68 -0
- gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +23 -0
- gea/catalog/chicxulub_m0077a_pwave_velocity.txt +735 -0
- gea/catalog/collingwood_1_28_ks_complete.las +128 -0
- gea/catalog/collingwood_1_28_ks_complete.provenance.json +17 -0
- gea/catalog/costa_rica_odp_friction_envelope.provenance.json +24 -0
- gea/catalog/costa_rica_odp_friction_envelope.txt +57 -0
- gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +21 -0
- gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +94 -0
- gea/catalog/dsdp_504b_physical_properties.provenance.json +24 -0
- gea/catalog/dsdp_504b_physical_properties.txt +82 -0
- gea/catalog/dsdp_504b_sound_velocity.provenance.json +21 -0
- gea/catalog/dsdp_504b_sound_velocity.txt +81 -0
- gea/catalog/elgygytgyn_5011_turbidites.provenance.json +20 -0
- gea/catalog/elgygytgyn_5011_turbidites.txt +193 -0
- gea/catalog/epica_domec_co2_800kyr.provenance.json +21 -0
- gea/catalog/epica_domec_co2_800kyr.txt +265 -0
- gea/catalog/fram_909_organic_petrography.provenance.json +22 -0
- gea/catalog/fram_909_organic_petrography.txt +40 -0
- gea/catalog/gbr_325_coral_uth_ages.provenance.json +23 -0
- gea/catalog/gbr_325_coral_uth_ages.txt +71 -0
- gea/catalog/gisp2_greenland_temperature.csv +599 -0
- gea/catalog/gisp2_greenland_temperature.provenance.json +17 -0
- gea/catalog/gom_308_t2p_insitu.provenance.json +21 -0
- gea/catalog/gom_308_t2p_insitu.txt +40 -0
- gea/catalog/guaymas_385_dom_d13c.provenance.json +21 -0
- gea/catalog/guaymas_385_dom_d13c.txt +103 -0
- gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +26 -0
- gea/catalog/hikurangi_u1520_friction_insitu.txt +74 -0
- gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +22 -0
- gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +60 -0
- gea/catalog/iodp_u1324_pore_pressure.provenance.json +22 -0
- gea/catalog/iodp_u1324_pore_pressure.txt +42 -0
- gea/catalog/jfast_c0019_slow_slip_events.provenance.json +28 -0
- gea/catalog/jfast_c0019_slow_slip_events.txt +35 -0
- gea/catalog/kennetcook_2_p129_excerpt.las +139 -0
- gea/catalog/kennetcook_2_p129_excerpt.provenance.json +17 -0
- gea/catalog/ktb_hb_bhgm_density.dat +227 -0
- gea/catalog/ktb_hb_bhgm_density.provenance.json +22 -0
- gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +20 -0
- gea/catalog/ktb_hb_complog_6020_excerpt.txt +72 -0
- gea/catalog/ktb_hb_hlog246_temperature.dat +1636 -0
- gea/catalog/ktb_hb_hlog246_temperature.provenance.json +22 -0
- gea/catalog/ktb_hb_rockmech_compress.dat +33 -0
- gea/catalog/ktb_hb_rockmech_compress.provenance.json +22 -0
- gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +2817 -0
- gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +20 -0
- gea/catalog/ktb_vb_rockmech_compress.dat +125 -0
- gea/catalog/ktb_vb_rockmech_compress.provenance.json +22 -0
- gea/catalog/ktb_vb_vlog251_temperature.dat +1127 -0
- gea/catalog/ktb_vb_vlog251_temperature.provenance.json +24 -0
- gea/catalog/l06_06_nl_survey.csv +201 -0
- gea/catalog/l06_06_nl_survey.provenance.json +17 -0
- gea/catalog/l07_01_nl_excerpt.las +90 -0
- gea/catalog/l07_01_nl_excerpt.provenance.json +17 -0
- gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +21 -0
- gea/catalog/mariana_1200_serpentinite_geochem.txt +63 -0
- gea/catalog/med_160_sapropels.provenance.json +22 -0
- gea/catalog/med_160_sapropels.txt +43 -0
- gea/catalog/nankai_megasplay_shear_strength.provenance.json +26 -0
- gea/catalog/nankai_megasplay_shear_strength.txt +42 -0
- gea/catalog/odp_1027b_thermal_conductivity.provenance.json +21 -0
- gea/catalog/odp_1027b_thermal_conductivity.txt +52 -0
- gea/catalog/odp_1027c_cork_temperature.provenance.json +20 -0
- gea/catalog/odp_1027c_cork_temperature.txt +26 -0
- gea/catalog/odp_1165b_thermal_conductivity.provenance.json +22 -0
- gea/catalog/odp_1165b_thermal_conductivity.txt +102 -0
- gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +27 -0
- gea/catalog/odp_1274a_mantle_peridotite_mad.txt +44 -0
- gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +27 -0
- gea/catalog/odp_504b_dike_elastic_moduli.txt +85 -0
- gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +19 -0
- gea/catalog/odp_504b_leg137_borehole_fluids.txt +68 -0
- gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +28 -0
- gea/catalog/odp_735b_gabbro_elastic_moduli.txt +127 -0
- gea/catalog/peru_201_sulfate_reduction.provenance.json +22 -0
- gea/catalog/peru_201_sulfate_reduction.txt +322 -0
- gea/catalog/scorpio_e1_sa_excerpt.las +113 -0
- gea/catalog/scorpio_e1_sa_excerpt.provenance.json +17 -0
- gea/catalog/sumatra_362_cohesion.provenance.json +21 -0
- gea/catalog/sumatra_362_cohesion.txt +38 -0
- gea/catalog/university_6_17_no1_tx_excerpt.las +119 -0
- gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +17 -0
- gea/catalog/ursa_308_xrd_mineralogy.provenance.json +21 -0
- gea/catalog/ursa_308_xrd_mineralogy.txt +46 -0
- gea/catalog/volve_15_9_19_sr_excerpt.las +183 -0
- gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +16 -0
- gea/catalog/volve_15_9_19a_core_excerpt.csv +88 -0
- gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +18 -0
- gea/catalog/volve_f12_f14_production_excerpt.csv +167 -0
- gea/catalog/volve_f12_f14_production_excerpt.provenance.json +21 -0
- gea/catalog/walvis_208_petm_carbonate.provenance.json +21 -0
- gea/catalog/walvis_208_petm_carbonate.txt +268 -0
- gea/catalog/woodlark_1109_rock_eval.provenance.json +23 -0
- gea/catalog/woodlark_1109_rock_eval.txt +30 -0
- gea/cli.py +125 -0
- gea/client_reports.py +943 -0
- gea/config_versioning.py +133 -0
- gea/correlation.py +155 -0
- gea/dashboard.py +390 -0
- gea/deviation.py +70 -0
- gea/downhole_engine.py +395 -0
- gea/drift_monitor.py +310 -0
- gea/earth_model.py +230 -0
- gea/example_register_map.json +14 -0
- gea/fat_sat.py +68 -0
- gea/follower.py +98 -0
- gea/forward_model.py +139 -0
- gea/gamma.py +176 -0
- gea/gauge_specs.py +112 -0
- gea/gravity_reference.py +116 -0
- gea/inverse_engine.py +215 -0
- gea/matplotlib_demo.py +85 -0
- gea/modbus.py +229 -0
- gea/model_card.py +248 -0
- gea/operator_app.py +442 -0
- gea/ports.py +340 -0
- gea/profile_catalog.py +773 -0
- gea/project.py +213 -0
- gea/qt6_downhole_app.py +144 -0
- gea/quartz_hpht_extension.py +152 -0
- gea/reconciler.py +206 -0
- gea/rock_inventory.py +404 -0
- gea/sample_record.py +430 -0
- gea/sample_well_profile.csv +15 -0
- gea/sbom.py +116 -0
- gea/segy.py +181 -0
- gea/service_life.py +173 -0
- gea/shell.py +107 -0
- gea/sla_report.py +199 -0
- gea/store_forward.py +234 -0
- gea/strata_join.py +186 -0
- gea/survey_cmd.py +264 -0
- gea/survey_view.py +138 -0
- gea/telemetry.py +306 -0
- gea/tool_library.py +260 -0
- gea/well_assembler.py +457 -0
- gea/well_test_validation.py +369 -0
- gea_program-0.1.0.dist-info/METADATA +138 -0
- gea_program-0.1.0.dist-info/RECORD +163 -0
- gea_program-0.1.0.dist-info/WHEEL +5 -0
- gea_program-0.1.0.dist-info/entry_points.txt +2 -0
- gea_program-0.1.0.dist-info/licenses/LICENSE +373 -0
- gea_program-0.1.0.dist-info/top_level.txt +1 -0
gea/downhole_engine.py
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""downhole_engine — the simulation engine of the GEA Downhole Simulator.
|
|
5
|
+
|
|
6
|
+
Faithful port of the 22Aug2026 template's converged design (imperial units:
|
|
7
|
+
ft / degF / psi; six quartz gauges; noise + transient events; rolling history;
|
|
8
|
+
CSV export), adjusted to the gea-program repo: drift comes from the
|
|
9
|
+
canonical-primitive physics layer, and the template's per-sensor "K_MEX"/
|
|
10
|
+
"Phi_res" fields are the renamed engineering trims (knob ruling, 2026-08-22).
|
|
11
|
+
|
|
12
|
+
Headless-safe by design: no matplotlib/Qt imports here — the engine runs and
|
|
13
|
+
exports under the fidelity gate with no display.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import csv
|
|
19
|
+
import random
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import List, Optional
|
|
24
|
+
|
|
25
|
+
import numpy as np
|
|
26
|
+
|
|
27
|
+
from .quartz_hpht_extension import (
|
|
28
|
+
calculate_quartz_transducer_hpht_program,
|
|
29
|
+
canonical_suppression,
|
|
30
|
+
conventional_drift,
|
|
31
|
+
PROGRAM_MODEL_AVAILABLE,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Well geometry defaults (port of the template's 6,200 m TD well to the
|
|
35
|
+
# converged imperial layout: TD ~20,300 ft, six gauges spanning the string).
|
|
36
|
+
DEFAULT_TD_FT = 20300.0
|
|
37
|
+
DEFAULT_SENSOR_DEPTHS_FT = [2600.0, 6200.0, 9800.0, 13400.0, 17000.0, 20000.0]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class WellProfile:
|
|
42
|
+
"""A real well profile (survey/log): depth vs pressure and temperature.
|
|
43
|
+
|
|
44
|
+
Load with `load_well_profile_csv`; when attached to SimulatorConfig, the
|
|
45
|
+
engine interpolates base P/T from the profile instead of linear gradients.
|
|
46
|
+
"""
|
|
47
|
+
depths_ft: List[float]
|
|
48
|
+
pressures_psi: List[float]
|
|
49
|
+
temps_F: List[float]
|
|
50
|
+
name: str = "profile"
|
|
51
|
+
|
|
52
|
+
def interp(self, depth_ft: float) -> tuple:
|
|
53
|
+
p = float(np.interp(depth_ft, self.depths_ft, self.pressures_psi))
|
|
54
|
+
tF = float(np.interp(depth_ft, self.depths_ft, self.temps_F))
|
|
55
|
+
return p, tF
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def load_well_profile_csv(path) -> WellProfile:
|
|
59
|
+
"""Load a well profile CSV with header: depth_ft,pressure_psi,temp_F
|
|
60
|
+
(rows in any depth order; sorted on load). Extra columns are ignored.
|
|
61
|
+
"""
|
|
62
|
+
d, pr, tf = [], [], []
|
|
63
|
+
with Path(path).open(newline="") as f:
|
|
64
|
+
reader = csv.DictReader(f)
|
|
65
|
+
for row in reader:
|
|
66
|
+
d.append(float(row["depth_ft"]))
|
|
67
|
+
pr.append(float(row["pressure_psi"]))
|
|
68
|
+
tf.append(float(row["temp_F"]))
|
|
69
|
+
order = np.argsort(d)
|
|
70
|
+
return WellProfile(depths_ft=[d[i] for i in order],
|
|
71
|
+
pressures_psi=[pr[i] for i in order],
|
|
72
|
+
temps_F=[tf[i] for i in order],
|
|
73
|
+
name=Path(path).stem)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def make_sensor_string(n: int, td_ft: float = DEFAULT_TD_FT,
|
|
77
|
+
start_ft: float = 2000.0) -> List[float]:
|
|
78
|
+
"""Evenly spaced N-gauge string from start_ft to just above TD."""
|
|
79
|
+
if n < 1:
|
|
80
|
+
raise ValueError("need at least one gauge")
|
|
81
|
+
if n == 1:
|
|
82
|
+
return [td_ft * 0.985]
|
|
83
|
+
return list(np.linspace(start_ft, td_ft * 0.985, n))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class Sensor:
|
|
88
|
+
depth_ft: float
|
|
89
|
+
cal_offset_P: float = 0.0
|
|
90
|
+
cal_offset_T: float = 0.0
|
|
91
|
+
k_structural_trim: float = 1.0 # engineering trim (renamed from template 'K_MEX' knob)
|
|
92
|
+
phi_coupling_trim: float = 1.0 # engineering trim (renamed from template 'Phi_res' knob)
|
|
93
|
+
name: str = ""
|
|
94
|
+
tool_name: str = "quartz_pt_program_geoq177_30k" # v1.47.0 mixed strings: which library tool sits here
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class SimulatorConfig:
|
|
99
|
+
td_ft: float = DEFAULT_TD_FT
|
|
100
|
+
sensor_depths_ft: List[float] = field(default_factory=lambda: DEFAULT_SENSOR_DEPTHS_FT.copy())
|
|
101
|
+
surface_temp_F: float = 75.0 # anchor: surface ambient (template)
|
|
102
|
+
surface_pressure_psi: float = 14.7 # anchor: 1 atm
|
|
103
|
+
temp_gradient_F_per_ft: float = 0.018 # anchor: geothermal gradient (template)
|
|
104
|
+
pressure_gradient_psi_per_ft: float = 0.465 # anchor: hydrostatic gradient (industry)
|
|
105
|
+
global_k_structural_trim: float = 1.0
|
|
106
|
+
global_phi_coupling_trim: float = 1.0
|
|
107
|
+
noise_scale: float = 1.0
|
|
108
|
+
event_probability: float = 0.27 # template transient-event rate
|
|
109
|
+
history_length: int = 400
|
|
110
|
+
profile: Optional[WellProfile] = None # real well profile (CSV); overrides gradients
|
|
111
|
+
comparison_mode: bool = True # twin-gauge GEA-vs-conventional tracking
|
|
112
|
+
gauge_spec: object = None # GaugeSpec (v1.5.0, gauge_specs); None = template anchors
|
|
113
|
+
deviation: object = None # DeviationSurvey (v1.6.0): sensors at MD, physics at TVD
|
|
114
|
+
toolstring: object = None # ToolString (v1.47.0): mixed per-station tool models
|
|
115
|
+
acknowledge_over_rating: bool = False # v1.47.0: the ONLY way past the in-engine rating block
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DownholeEngine:
|
|
119
|
+
"""Deep-well quartz-gauge string simulator (GEA-stabilized drift)."""
|
|
120
|
+
|
|
121
|
+
def __init__(self, config: SimulatorConfig | None = None):
|
|
122
|
+
self.cfg = config or SimulatorConfig()
|
|
123
|
+
self.time = 0.0
|
|
124
|
+
self.sensors: List[Sensor] = []
|
|
125
|
+
self._build_sensors()
|
|
126
|
+
self._init_state()
|
|
127
|
+
if self.cfg.toolstring is not None:
|
|
128
|
+
self._enforce_rating()
|
|
129
|
+
|
|
130
|
+
def _enforce_rating(self) -> None:
|
|
131
|
+
"""v1.47.0: the rating check runs INSIDE the engine, not only as a
|
|
132
|
+
print - a tool over its cited rating at its station (against the
|
|
133
|
+
REAL profile when one is attached) blocks construction unless the
|
|
134
|
+
operator's explicit acknowledge_over_rating rides in the config."""
|
|
135
|
+
from .tool_library import rating_check
|
|
136
|
+
report = rating_check(self.cfg.toolstring,
|
|
137
|
+
profile=self.cfg.profile,
|
|
138
|
+
deviation=self.cfg.deviation)
|
|
139
|
+
blocks = [r for r in report if not r["ok"]]
|
|
140
|
+
self.rating_report = report
|
|
141
|
+
if blocks and not self.cfg.acknowledge_over_rating:
|
|
142
|
+
names = "; ".join(
|
|
143
|
+
f"{b['tool']}@{b['md_ft']:.0f}ft ({b['station_temp_C']}C > "
|
|
144
|
+
f"{b['temp_rating_C']}C rated)" for b in blocks)
|
|
145
|
+
raise RuntimeError(
|
|
146
|
+
f"ENGINE RATING BLOCK: {names}. The check runs inside the "
|
|
147
|
+
"engine (v1.47.0) - set acknowledge_over_rating=True in the "
|
|
148
|
+
"config as an explicit operator decision, or fix the string.")
|
|
149
|
+
|
|
150
|
+
# -- construction -------------------------------------------------------
|
|
151
|
+
def _build_sensors(self) -> None:
|
|
152
|
+
if self.cfg.toolstring is not None:
|
|
153
|
+
stations = sorted(self.cfg.toolstring.stations, key=lambda x: x[0])
|
|
154
|
+
self.sensors = [
|
|
155
|
+
Sensor(depth_ft=float(md),
|
|
156
|
+
k_structural_trim=self.cfg.global_k_structural_trim,
|
|
157
|
+
phi_coupling_trim=self.cfg.global_phi_coupling_trim,
|
|
158
|
+
name=f"S{i + 1}", tool_name=tn)
|
|
159
|
+
for i, (md, tn) in enumerate(stations)
|
|
160
|
+
]
|
|
161
|
+
self.cfg.sensor_depths_ft = [s.depth_ft for s in self.sensors]
|
|
162
|
+
else:
|
|
163
|
+
self.sensors = [
|
|
164
|
+
Sensor(depth_ft=float(d),
|
|
165
|
+
k_structural_trim=self.cfg.global_k_structural_trim,
|
|
166
|
+
phi_coupling_trim=self.cfg.global_phi_coupling_trim,
|
|
167
|
+
name=f"S{i + 1}")
|
|
168
|
+
for i, d in enumerate(self.cfg.sensor_depths_ft)
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
def _physics_depth_ft(self, md_ft: float) -> float:
|
|
172
|
+
"""Sensor addresses are MD (position on the string); pressure and
|
|
173
|
+
temperature are set by TVD (v1.6.0 deviation support)."""
|
|
174
|
+
if self.cfg.deviation is not None:
|
|
175
|
+
return float(self.cfg.deviation.tvd_of(md_ft))
|
|
176
|
+
return float(md_ft)
|
|
177
|
+
|
|
178
|
+
def _init_state(self) -> None:
|
|
179
|
+
tvds = [self._physics_depth_ft(s.depth_ft) for s in self.sensors]
|
|
180
|
+
if self.cfg.profile is not None:
|
|
181
|
+
pairs = [self.cfg.profile.interp(tvd) for tvd in tvds] # profiles are TVD-indexed
|
|
182
|
+
self.base_P = np.array([p for p, _ in pairs], dtype=float)
|
|
183
|
+
self.base_T = np.array([tF for _, tF in pairs], dtype=float)
|
|
184
|
+
else:
|
|
185
|
+
self.base_P = np.array(
|
|
186
|
+
[self.cfg.surface_pressure_psi + tvd * self.cfg.pressure_gradient_psi_per_ft
|
|
187
|
+
for tvd in tvds], dtype=float)
|
|
188
|
+
self.base_T = np.array(
|
|
189
|
+
[self.cfg.surface_temp_F + tvd * self.cfg.temp_gradient_F_per_ft
|
|
190
|
+
for tvd in tvds], dtype=float)
|
|
191
|
+
self.P = self.base_P.copy()
|
|
192
|
+
self.T = self.base_T.copy()
|
|
193
|
+
self.history_t: List[float] = [0.0]
|
|
194
|
+
self.history_P: List[np.ndarray] = [self.P.copy()]
|
|
195
|
+
self.history_T: List[np.ndarray] = [self.T.copy()]
|
|
196
|
+
|
|
197
|
+
# -- physics ------------------------------------------------------------
|
|
198
|
+
def compute_drift(self, sensor: Sensor, temp_F: float, pressure_psi: float) -> float:
|
|
199
|
+
temp_C = (temp_F - 32.0) * 5.0 / 9.0
|
|
200
|
+
depth_m = sensor.depth_ft / 3.28084
|
|
201
|
+
r = calculate_quartz_transducer_hpht_program(
|
|
202
|
+
depth_m=depth_m, temp_c=temp_C, pressure_psi=pressure_psi,
|
|
203
|
+
k_structural_trim=sensor.k_structural_trim,
|
|
204
|
+
phi_coupling_trim=sensor.phi_coupling_trim,
|
|
205
|
+
spec=self.cfg.gauge_spec)
|
|
206
|
+
return float(r["value"]["drift_pct"])
|
|
207
|
+
|
|
208
|
+
def station_drift_legs(self, i: int) -> dict:
|
|
209
|
+
"""v1.47.0 mixed strings: each station's drift legs come from ITS
|
|
210
|
+
tool's runnable model - and tools without a runnable model REFUSE
|
|
211
|
+
(status says why) instead of borrowing the quartz curve."""
|
|
212
|
+
from .tool_library import TOOL_LIBRARY, piezoresistive_drift
|
|
213
|
+
s = self.sensors[i]
|
|
214
|
+
tool = TOOL_LIBRARY.get(s.tool_name)
|
|
215
|
+
t_C = (float(self.T[i]) - 32.0) * 5.0 / 9.0
|
|
216
|
+
out = {"station": s.name, "md_ft": s.depth_ft, "tool": s.tool_name,
|
|
217
|
+
"tool_class": tool.tool_class if tool else "?",
|
|
218
|
+
"drift_pct": None, "conventional_drift_pct": None}
|
|
219
|
+
if tool is None:
|
|
220
|
+
out["status"] = f"UNKNOWN_TOOL '{s.tool_name}' - refused"
|
|
221
|
+
return out
|
|
222
|
+
if tool.tool_class == "QUARTZ_PT_GAUGE":
|
|
223
|
+
out["conventional_drift_pct"] = round(
|
|
224
|
+
conventional_drift(t_C, float(self.P[i]),
|
|
225
|
+
spec=self.cfg.gauge_spec), 4)
|
|
226
|
+
if "conventional" in s.tool_name:
|
|
227
|
+
out["status"] = ("NO_PROGRAM_LEG: conventional instrument - the "
|
|
228
|
+
"reference leg only (twin comparison needs "
|
|
229
|
+
"the GEA tool at this station)")
|
|
230
|
+
else:
|
|
231
|
+
out["drift_pct"] = round(
|
|
232
|
+
self.compute_drift(s, float(self.T[i]),
|
|
233
|
+
float(self.P[i])), 4)
|
|
234
|
+
out["status"] = "TWIN_LEGS"
|
|
235
|
+
elif tool.tool_class == "PIEZORESISTIVE_PT_GAUGE":
|
|
236
|
+
out["conventional_drift_pct"] = round(piezoresistive_drift(t_C), 4)
|
|
237
|
+
out["status"] = ("PIEZO_CLASS_ENVELOPE (labeled representative "
|
|
238
|
+
"fit, vendor-scatter disclosed) - NO_PROGRAM_MODEL: "
|
|
239
|
+
"no GEA piezo derivation in the corpus, refused "
|
|
240
|
+
"rather than invented")
|
|
241
|
+
else:
|
|
242
|
+
out["status"] = ("PARAMETERS_USER_SUPPLIED: no vendor datasheet "
|
|
243
|
+
"fetched for this tool class - drift model "
|
|
244
|
+
"REFUSED; station still streams well P/T")
|
|
245
|
+
return out
|
|
246
|
+
|
|
247
|
+
def mixed_summary(self) -> dict:
|
|
248
|
+
"""Per-station tool report + aggregates computed ONLY over stations
|
|
249
|
+
possessing both legs, with the counts disclosed."""
|
|
250
|
+
rows = [self.station_drift_legs(i) for i in range(len(self.sensors))]
|
|
251
|
+
twin = [r for r in rows if r["status"] == "TWIN_LEGS"]
|
|
252
|
+
agg = None
|
|
253
|
+
if twin:
|
|
254
|
+
uq = float(np.mean([r["drift_pct"] for r in twin]))
|
|
255
|
+
cv = float(np.mean([r["conventional_drift_pct"] for r in twin]))
|
|
256
|
+
agg = {"avg_program_drift_pct": round(uq, 4),
|
|
257
|
+
"avg_conventional_drift_pct": round(cv, 4),
|
|
258
|
+
"measured_ratio_mean": round(cv / uq, 4) if uq > 0 else None}
|
|
259
|
+
return {"stations": rows,
|
|
260
|
+
"twin_leg_stations": len(twin),
|
|
261
|
+
"single_or_refused_stations": len(rows) - len(twin),
|
|
262
|
+
"aggregate_over_twin_stations_only": agg}
|
|
263
|
+
|
|
264
|
+
def _noise_drifts(self) -> np.ndarray:
|
|
265
|
+
"""Drift values feeding the template noise-suppression map: each
|
|
266
|
+
station's own runnable model; refused stations contribute 0.0
|
|
267
|
+
(neutral suppression - template noise unshaped, per mixed_summary)."""
|
|
268
|
+
if self.cfg.toolstring is None:
|
|
269
|
+
return self.current_drifts
|
|
270
|
+
out = []
|
|
271
|
+
for i in range(len(self.sensors)):
|
|
272
|
+
legs = self.station_drift_legs(i)
|
|
273
|
+
d = legs["drift_pct"]
|
|
274
|
+
if d is None:
|
|
275
|
+
d = legs["conventional_drift_pct"]
|
|
276
|
+
out.append(0.0 if d is None else float(d))
|
|
277
|
+
return np.array(out)
|
|
278
|
+
|
|
279
|
+
@property
|
|
280
|
+
def current_drifts(self) -> np.ndarray:
|
|
281
|
+
return np.array([self.compute_drift(s, self.T[i], self.P[i])
|
|
282
|
+
for i, s in enumerate(self.sensors)])
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def current_conventional_drifts(self) -> np.ndarray:
|
|
286
|
+
"""Comparison-mode reference leg: conventional gauges at the same T/P."""
|
|
287
|
+
out = []
|
|
288
|
+
for i in range(len(self.sensors)):
|
|
289
|
+
temp_C = (self.T[i] - 32.0) * 5.0 / 9.0
|
|
290
|
+
out.append(conventional_drift(temp_C, self.P[i], spec=self.cfg.gauge_spec))
|
|
291
|
+
return np.array(out)
|
|
292
|
+
|
|
293
|
+
def comparison_summary(self) -> dict:
|
|
294
|
+
"""Twin-gauge report: measured conventional/GEA drift ratio vs the
|
|
295
|
+
suppression prediction (equal when no gauge clips)."""
|
|
296
|
+
uq = self.current_drifts
|
|
297
|
+
cv = self.current_conventional_drifts
|
|
298
|
+
ratios = cv / np.where(uq > 0, uq, np.nan)
|
|
299
|
+
return {
|
|
300
|
+
'avg_program_drift_pct': round(float(np.mean(uq)), 4),
|
|
301
|
+
'avg_conventional_drift_pct': round(float(np.mean(cv)), 4),
|
|
302
|
+
'measured_ratio_mean': round(float(np.nanmean(ratios)), 4),
|
|
303
|
+
'predicted_ratio_suppression': round(canonical_suppression(
|
|
304
|
+
self.cfg.global_k_structural_trim, self.cfg.global_phi_coupling_trim), 4),
|
|
305
|
+
'per_sensor_ratio': [round(float(r), 4) for r in ratios],
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
# -- stepping (template-faithful) ---------------------------------------
|
|
309
|
+
def step(self, dt: float = 0.12) -> None:
|
|
310
|
+
self.time += dt
|
|
311
|
+
n = len(self.sensors)
|
|
312
|
+
drifts = self._noise_drifts()
|
|
313
|
+
suppression = np.clip(1.0 - (drifts / 0.28), 0.25, 1.0) # template noise-suppression map
|
|
314
|
+
noise_P = np.random.normal(0, 28 * self.cfg.noise_scale, n) * suppression
|
|
315
|
+
noise_T = np.random.normal(0, 0.9 * self.cfg.noise_scale, n) * suppression
|
|
316
|
+
event = 0.0
|
|
317
|
+
if random.random() < self.cfg.event_probability:
|
|
318
|
+
event = 110.0 * np.sin(self.time / 7.2) * float(np.mean(suppression))
|
|
319
|
+
self.P = self.base_P + noise_P + event
|
|
320
|
+
self.T = self.base_T + noise_T
|
|
321
|
+
for i, s in enumerate(self.sensors):
|
|
322
|
+
self.P[i] += s.cal_offset_P
|
|
323
|
+
self.T[i] += s.cal_offset_T
|
|
324
|
+
self.P = np.clip(self.P, 50, 28000) # template plausibility bounds
|
|
325
|
+
self.T = np.clip(self.T, 40, 550)
|
|
326
|
+
self.history_t.append(self.time)
|
|
327
|
+
self.history_P.append(self.P.copy())
|
|
328
|
+
self.history_T.append(self.T.copy())
|
|
329
|
+
if len(self.history_t) > self.cfg.history_length:
|
|
330
|
+
self.history_t = self.history_t[-self.cfg.history_length:]
|
|
331
|
+
self.history_P = self.history_P[-self.cfg.history_length:]
|
|
332
|
+
self.history_T = self.history_T[-self.cfg.history_length:]
|
|
333
|
+
|
|
334
|
+
def set_global_trims(self, k_structural_trim: float, phi_coupling_trim: float) -> None:
|
|
335
|
+
self.cfg.global_k_structural_trim = float(k_structural_trim)
|
|
336
|
+
self.cfg.global_phi_coupling_trim = float(phi_coupling_trim)
|
|
337
|
+
for s in self.sensors:
|
|
338
|
+
s.k_structural_trim = float(k_structural_trim)
|
|
339
|
+
s.phi_coupling_trim = float(phi_coupling_trim)
|
|
340
|
+
|
|
341
|
+
# -- export (template-faithful) -----------------------------------------
|
|
342
|
+
def export_csv(self, path: str | None = None) -> Path:
|
|
343
|
+
if path is None:
|
|
344
|
+
path = f"downhole_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv"
|
|
345
|
+
p = Path(path)
|
|
346
|
+
with p.open("w", newline="") as f:
|
|
347
|
+
writer = csv.writer(f)
|
|
348
|
+
header = ["time_s"]
|
|
349
|
+
for s in self.sensors:
|
|
350
|
+
header += [f"P_{s.name}_{s.depth_ft:.0f}ft", f"T_{s.name}_{s.depth_ft:.0f}ft"]
|
|
351
|
+
if self.cfg.comparison_mode:
|
|
352
|
+
cmp_now = self.comparison_summary()
|
|
353
|
+
header += ["avg_program_drift_pct", "avg_conventional_drift_pct", "measured_ratio_mean"]
|
|
354
|
+
writer.writerow(header)
|
|
355
|
+
for i, t in enumerate(self.history_t):
|
|
356
|
+
row = [round(t, 3)]
|
|
357
|
+
for j in range(len(self.sensors)):
|
|
358
|
+
row.append(round(float(self.history_P[i][j]), 2))
|
|
359
|
+
row.append(round(float(self.history_T[i][j]), 2))
|
|
360
|
+
if self.cfg.comparison_mode:
|
|
361
|
+
row += [cmp_now['avg_program_drift_pct'], cmp_now['avg_conventional_drift_pct'],
|
|
362
|
+
cmp_now['measured_ratio_mean']]
|
|
363
|
+
writer.writerow(row)
|
|
364
|
+
return p
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def depths_ft(self) -> np.ndarray:
|
|
368
|
+
return np.array([s.depth_ft for s in self.sensors])
|
|
369
|
+
|
|
370
|
+
def summary(self) -> dict:
|
|
371
|
+
return {
|
|
372
|
+
"live": PROGRAM_MODEL_AVAILABLE,
|
|
373
|
+
"canonical_suppression_at_unity_trims": round(canonical_suppression(), 4),
|
|
374
|
+
"sensors": len(self.sensors),
|
|
375
|
+
"td_ft": self.cfg.td_ft,
|
|
376
|
+
"time_s": round(self.time, 3),
|
|
377
|
+
"avg_drift_pct": round(float(np.mean(self.current_drifts)), 4),
|
|
378
|
+
"history_points": len(self.history_t),
|
|
379
|
+
"profile": self.cfg.profile.name if self.cfg.profile else "linear gradients",
|
|
380
|
+
"deviation": self.cfg.deviation.name if self.cfg.deviation is not None else "vertical (MD == TVD)",
|
|
381
|
+
"gauge_spec": getattr(self.cfg.gauge_spec, 'name', None) or "template_generic (default)",
|
|
382
|
+
"comparison": self.comparison_summary() if self.cfg.comparison_mode else None,
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def run_batch(wells: dict, steps: int = 100, dt: float = 0.12) -> dict:
|
|
387
|
+
"""Multi-well batch (v1.6.0): run each named SimulatorConfig for `steps`
|
|
388
|
+
and return {well_name: summary}. A field-wide study in one call."""
|
|
389
|
+
out = {}
|
|
390
|
+
for name, cfg in wells.items():
|
|
391
|
+
eng = DownholeEngine(cfg)
|
|
392
|
+
for _ in range(int(steps)):
|
|
393
|
+
eng.step(dt=dt)
|
|
394
|
+
out[name] = eng.summary()
|
|
395
|
+
return out
|