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/project.py
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
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
|
+
"""project - Part 7 of the subsurface surveying tool: THE CLIENT SHELL
|
|
5
|
+
(v1.84.0) - project files and the client report.
|
|
6
|
+
|
|
7
|
+
A SurveyProject is what a client engagement touches: a JSON project file
|
|
8
|
+
(name, created stamp, tool versions, render paths, notes) plus
|
|
9
|
+
`generate_report()` - the deliverable that assembles EVERYTHING the tool
|
|
10
|
+
can currently prove into one markdown document:
|
|
11
|
+
|
|
12
|
+
- Earth Model census and the structural-ladder audit
|
|
13
|
+
- K2 gravity validation (with its circularity caveat, verbatim)
|
|
14
|
+
- the inverse engine's current read and both predictions' status
|
|
15
|
+
(including the REFUTED one - the scoring record IS the product)
|
|
16
|
+
- the standing blind accuracy table, weak tails included
|
|
17
|
+
- the correlation findings (time frame + the refusal census)
|
|
18
|
+
- the honest renders, regenerated fresh if matplotlib is present
|
|
19
|
+
|
|
20
|
+
Every number in the report is computed AT GENERATION TIME from the same
|
|
21
|
+
modules the gate verifies - a report cannot say anything the gate cannot
|
|
22
|
+
prove, and each section names its machinery. The CLI exposes this as
|
|
23
|
+
`python -m gea report --out <dir>`.
|
|
24
|
+
|
|
25
|
+
The Qt operator surface gains geology through `build_geology_tab(...)`
|
|
26
|
+
(guarded, optional): the site map and cross-section rendered into the
|
|
27
|
+
operator app - the surface stops drawing only gauges.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
import os
|
|
34
|
+
import statistics
|
|
35
|
+
import time
|
|
36
|
+
from typing import Dict, Optional
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def create_project(path: str, name: str, notes: str = '') -> Dict:
|
|
40
|
+
"""Create/overwrite a survey project file (JSON, human-readable)."""
|
|
41
|
+
from . import __version__ as sim_version
|
|
42
|
+
proj = {'name': name, 'notes': notes,
|
|
43
|
+
'created_utc': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
44
|
+
'simulator_version': sim_version,
|
|
45
|
+
'renders': {}, 'report': None}
|
|
46
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
47
|
+
json.dump(proj, f, indent=2)
|
|
48
|
+
return proj
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def load_project(path: str) -> Dict:
|
|
52
|
+
with open(path, encoding='utf-8') as f:
|
|
53
|
+
return json.load(f)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def generate_report(out_dir: str, project_path: Optional[str] = None,
|
|
57
|
+
name: str = 'GEA Subsurface Survey Report') -> Dict:
|
|
58
|
+
"""The client deliverable. Returns {'report_path', 'renders', 'sections'};
|
|
59
|
+
every figure is regenerated, every number recomputed, at call time."""
|
|
60
|
+
from . import __version__ as sim_version
|
|
61
|
+
from .earth_model import EarthModel
|
|
62
|
+
from .forward_model import ktb_gravity_test
|
|
63
|
+
from .inverse_engine import invert_gravity_column
|
|
64
|
+
from .blind_harness import accuracy_report
|
|
65
|
+
from .correlation import time_frame, depth_frame_pairs
|
|
66
|
+
|
|
67
|
+
os.makedirs(out_dir, exist_ok=True)
|
|
68
|
+
em = EarthModel()
|
|
69
|
+
cen = em.census()
|
|
70
|
+
k2 = ktb_gravity_test()
|
|
71
|
+
inv = invert_gravity_column(prior_family='continental_crystalline')
|
|
72
|
+
acc = accuracy_report()
|
|
73
|
+
tf = time_frame(em)
|
|
74
|
+
df = depth_frame_pairs(em)
|
|
75
|
+
|
|
76
|
+
renders = {}
|
|
77
|
+
try:
|
|
78
|
+
from .survey_view import render_site_map, render_ktb_inversion
|
|
79
|
+
m = os.path.join(out_dir, 'site_map.png')
|
|
80
|
+
x = os.path.join(out_dir, 'inversion_cross_section.png')
|
|
81
|
+
render_site_map(m)
|
|
82
|
+
render_ktb_inversion(x)
|
|
83
|
+
renders = {'site_map': m, 'cross_section': x}
|
|
84
|
+
except ImportError:
|
|
85
|
+
renders = {'note': 'matplotlib absent - figures skipped, disclosed'}
|
|
86
|
+
|
|
87
|
+
ins = [e for e in inv['estimates']
|
|
88
|
+
if e.posteriors['vp']['status'] == 'OK'
|
|
89
|
+
and not e.posteriors['vp']['extrapolation']]
|
|
90
|
+
v2 = [e.posteriors['vp']['estimate'] for e in ins]
|
|
91
|
+
nf = k2['null_filtered']
|
|
92
|
+
|
|
93
|
+
lines = []
|
|
94
|
+
w = lines.append
|
|
95
|
+
w('# %s' % name)
|
|
96
|
+
w('')
|
|
97
|
+
w('*Generated %s UTC by gea v%s (ENRGYONE / '
|
|
98
|
+
'GEA-Program). Every number below is recomputed at generation '
|
|
99
|
+
'time by the same gate-verified modules; each section names its '
|
|
100
|
+
'machinery.*' % (time.strftime('%Y-%m-%d %H:%M', time.gmtime()),
|
|
101
|
+
sim_version))
|
|
102
|
+
w('')
|
|
103
|
+
w('## 1. The registered library (`earth_model`)')
|
|
104
|
+
w('')
|
|
105
|
+
w('%d sites registered from archive-declared coordinates only, spanning '
|
|
106
|
+
'%.1f degrees of latitude and %.0f km of great circle; %d entries '
|
|
107
|
+
'unregistered with reasons on file; %d multi-entry sites reunited by '
|
|
108
|
+
'coordinates alone.'
|
|
109
|
+
% (cen['sites'], cen['latitude_span_deg'],
|
|
110
|
+
cen['great_circle_span_km'], cen['unregistered_entries'],
|
|
111
|
+
cen['multi_entry_sites']))
|
|
112
|
+
w('')
|
|
113
|
+
w('## 3. Gravity kernel validation (`forward_model`)')
|
|
114
|
+
w('')
|
|
115
|
+
w('Standard-constant gravity kernel vs the KTB borehole gravimeter: correlation '
|
|
116
|
+
'%.4f over %d intervals, mean residual %.4f mGal. Caveat, verbatim: '
|
|
117
|
+
'%s' % (nf['correlation'], nf['n'], nf['mean_residual_mgal'],
|
|
118
|
+
k2['circularity_caveat']))
|
|
119
|
+
w('')
|
|
120
|
+
w('## 4. The scoring record (`inverse_engine`)')
|
|
121
|
+
w('')
|
|
122
|
+
w('- Prediction v1 (transferred 504B prior): **REFUTED** at +10%% '
|
|
123
|
+
'(measured 6,228 m/s vs predicted 5,675±109) - diagnosis was the '
|
|
124
|
+
'assumption disclosed on every estimate in advance.')
|
|
125
|
+
w('- Prediction V2 (continental_crystalline family prior): Vp '
|
|
126
|
+
'%.0f-%.0f m/s (mean %.0f) over %d in-support intervals - '
|
|
127
|
+
'**PINNED_AWAITING_DEEP_SONIC**, unsettled by design.'
|
|
128
|
+
% (min(v2), max(v2), statistics.mean(v2), len(ins)))
|
|
129
|
+
w('')
|
|
130
|
+
w('## 5. Standing blind accuracy (`blind_harness`)')
|
|
131
|
+
w('')
|
|
132
|
+
w('| given → target | well | n | MAE %% | coverage |')
|
|
133
|
+
w('|---|---|---|---|---|')
|
|
134
|
+
for row in acc['ok']:
|
|
135
|
+
w('| %s → %s | %s | %d | %.2f | %.2f |'
|
|
136
|
+
% (row['given'], row['target'], row['well'], row['n'],
|
|
137
|
+
row['mae_pct'], row['coverage']))
|
|
138
|
+
w('')
|
|
139
|
+
w('Median 1σ coverage %.2f (honest target ~0.68). %d thin pairs '
|
|
140
|
+
'refused. %s' % (acc['median_coverage'], acc['n_refused'],
|
|
141
|
+
acc['doctrine']))
|
|
142
|
+
w('')
|
|
143
|
+
w('## 6. Correlation (`correlation`)')
|
|
144
|
+
w('')
|
|
145
|
+
w('Time frame: %d age-bearing sites; master chronology %s overlaps %d '
|
|
146
|
+
'others. Depth frame: %s'
|
|
147
|
+
% (tf['n_sites'], tf['master_chronology']['site'],
|
|
148
|
+
tf['master_chronology']['overlaps_others'], df['finding']))
|
|
149
|
+
w('')
|
|
150
|
+
if 'site_map' in renders:
|
|
151
|
+
w('## 7. Figures')
|
|
152
|
+
w('')
|
|
153
|
+
w('')
|
|
154
|
+
w('')
|
|
155
|
+
w('')
|
|
156
|
+
w('')
|
|
157
|
+
w('---')
|
|
158
|
+
w('*What this report will not do: quote unmeasured accuracy, fill '
|
|
159
|
+
'missing data with templates, or present transferred models as site '
|
|
160
|
+
'truth. Refusals above are deliberate outputs of the same doctrine '
|
|
161
|
+
'that produced the numbers.*')
|
|
162
|
+
|
|
163
|
+
report_path = os.path.join(out_dir, 'survey_report.md')
|
|
164
|
+
with open(report_path, 'w', encoding='utf-8', newline='') as f:
|
|
165
|
+
f.write('\n'.join(lines) + '\n')
|
|
166
|
+
|
|
167
|
+
if project_path:
|
|
168
|
+
proj = load_project(project_path)
|
|
169
|
+
proj['renders'] = renders
|
|
170
|
+
proj['report'] = report_path
|
|
171
|
+
with open(project_path, 'w', encoding='utf-8') as f:
|
|
172
|
+
json.dump(proj, f, indent=2)
|
|
173
|
+
|
|
174
|
+
return {'report_path': report_path, 'renders': renders,
|
|
175
|
+
'sections': 7 if 'site_map' in renders else 6}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def build_geology_tab(parent=None):
|
|
179
|
+
"""The operator surface stops drawing only gauges: a Qt widget holding
|
|
180
|
+
the regenerated site map and cross-section. Optional by construction -
|
|
181
|
+
raises a clear ImportError naming PyQt6/matplotlib if absent; nothing
|
|
182
|
+
load-bearing depends on it."""
|
|
183
|
+
try:
|
|
184
|
+
from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QLabel,
|
|
185
|
+
QScrollArea)
|
|
186
|
+
from PyQt6.QtGui import QPixmap
|
|
187
|
+
except ImportError as e:
|
|
188
|
+
raise ImportError(
|
|
189
|
+
'build_geology_tab needs the optional gui extra '
|
|
190
|
+
'(pip install "gea-program[gui]")') from e
|
|
191
|
+
import tempfile
|
|
192
|
+
from .survey_view import render_site_map, render_ktb_inversion
|
|
193
|
+
td = tempfile.mkdtemp(prefix='geology_')
|
|
194
|
+
m = os.path.join(td, 'map.png')
|
|
195
|
+
x = os.path.join(td, 'xsec.png')
|
|
196
|
+
render_site_map(m)
|
|
197
|
+
render_ktb_inversion(x)
|
|
198
|
+
tab = QWidget(parent)
|
|
199
|
+
lay = QVBoxLayout(tab)
|
|
200
|
+
scroll = QScrollArea(tab)
|
|
201
|
+
inner = QWidget()
|
|
202
|
+
ilay = QVBoxLayout(inner)
|
|
203
|
+
for path, title in ((m, 'Earth Model - registered sites'),
|
|
204
|
+
(x, 'Inverse engine - KTB read (V2 unsettled)')):
|
|
205
|
+
cap = QLabel(title)
|
|
206
|
+
img = QLabel()
|
|
207
|
+
img.setPixmap(QPixmap(path))
|
|
208
|
+
ilay.addWidget(cap)
|
|
209
|
+
ilay.addWidget(img)
|
|
210
|
+
scroll.setWidget(inner)
|
|
211
|
+
scroll.setWidgetResizable(True)
|
|
212
|
+
lay.addWidget(scroll)
|
|
213
|
+
return tab
|
gea/qt6_downhole_app.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
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
|
+
"""qt6_downhole_app — PyQt6 GUI for the GEA Downhole Simulator (optional).
|
|
5
|
+
|
|
6
|
+
Template-faithful port (22Aug2026 thread): control panel + embedded matplotlib
|
|
7
|
+
canvas, QTimer-driven stepping at 120 ms, CSV export button. Knob ruling
|
|
8
|
+
applied: the spinboxes are the ENGINEERING TRIMS (k_structural_trim /
|
|
9
|
+
phi_coupling_trim, range 0.50-1.50, default 1.00) — the canonical K_MEX = 25/12
|
|
10
|
+
and Phi_res = 0.84 are locked inside the physics layer and shown read-only.
|
|
11
|
+
|
|
12
|
+
PyQt6 is an OPTIONAL dependency: `pip install PyQt6 matplotlib`.
|
|
13
|
+
Run: python -m gea.qt6_downhole_app
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from .downhole_engine import SimulatorConfig, DownholeEngine
|
|
21
|
+
from .quartz_hpht_extension import canonical_suppression
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from PyQt6.QtCore import QTimer
|
|
25
|
+
from PyQt6.QtWidgets import (QApplication, QDoubleSpinBox, QFormLayout,
|
|
26
|
+
QGroupBox, QHBoxLayout, QLabel, QMainWindow,
|
|
27
|
+
QPushButton, QVBoxLayout, QWidget)
|
|
28
|
+
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
29
|
+
from matplotlib.figure import Figure
|
|
30
|
+
QT_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
QT_AVAILABLE = False
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
if QT_AVAILABLE:
|
|
36
|
+
|
|
37
|
+
class MplCanvas(FigureCanvasQTAgg):
|
|
38
|
+
def __init__(self):
|
|
39
|
+
self.fig = Figure(figsize=(9, 7), facecolor="#0b1220")
|
|
40
|
+
self.ax_p = self.fig.add_subplot(211, facecolor="#101a2e")
|
|
41
|
+
self.ax_t = self.fig.add_subplot(212, facecolor="#101a2e")
|
|
42
|
+
super().__init__(self.fig)
|
|
43
|
+
|
|
44
|
+
class MainWindow(QMainWindow):
|
|
45
|
+
def __init__(self):
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.setWindowTitle("GEA Deep-Well Quartz Simulator (PyQt6) - gea-program")
|
|
48
|
+
self.resize(1480, 920)
|
|
49
|
+
self.cfg = SimulatorConfig()
|
|
50
|
+
self.engine = DownholeEngine(self.cfg)
|
|
51
|
+
self._build_ui()
|
|
52
|
+
self.timer = QTimer(self)
|
|
53
|
+
self.timer.timeout.connect(self._on_timer)
|
|
54
|
+
self.timer.setInterval(120)
|
|
55
|
+
self.statusBar().showMessage("Ready")
|
|
56
|
+
|
|
57
|
+
def _build_ui(self):
|
|
58
|
+
central = QWidget()
|
|
59
|
+
self.setCentralWidget(central)
|
|
60
|
+
main_layout = QHBoxLayout(central)
|
|
61
|
+
|
|
62
|
+
control = QWidget()
|
|
63
|
+
control.setFixedWidth(360)
|
|
64
|
+
clayout = QVBoxLayout(control)
|
|
65
|
+
|
|
66
|
+
gbox = QGroupBox("Canonical GEA primitives (LOCKED)")
|
|
67
|
+
form0 = QFormLayout(gbox)
|
|
68
|
+
form0.addRow("K_MEX", QLabel("25/12 = 2.0833 (PAPER_1522)"))
|
|
69
|
+
form0.addRow("Phi_res", QLabel("0.84 (PAPER_2134)"))
|
|
70
|
+
form0.addRow("rho ratio", QLabel("F_TRZ = 0.1 (PAPER_1160)"))
|
|
71
|
+
form0.addRow("suppression", QLabel(f"{canonical_suppression():.4f} at unity trims"))
|
|
72
|
+
clayout.addWidget(gbox)
|
|
73
|
+
|
|
74
|
+
tbox = QGroupBox("Engineering trims (instrument tuning)")
|
|
75
|
+
form = QFormLayout(tbox)
|
|
76
|
+
self.spin_k = QDoubleSpinBox()
|
|
77
|
+
self.spin_k.setRange(0.50, 1.50)
|
|
78
|
+
self.spin_k.setSingleStep(0.01)
|
|
79
|
+
self.spin_k.setValue(1.00)
|
|
80
|
+
self.spin_phi = QDoubleSpinBox()
|
|
81
|
+
self.spin_phi.setRange(0.50, 1.50)
|
|
82
|
+
self.spin_phi.setSingleStep(0.01)
|
|
83
|
+
self.spin_phi.setValue(1.00)
|
|
84
|
+
form.addRow("k_structural_trim", self.spin_k)
|
|
85
|
+
form.addRow("phi_coupling_trim", self.spin_phi)
|
|
86
|
+
self.spin_k.valueChanged.connect(self._on_trims)
|
|
87
|
+
self.spin_phi.valueChanged.connect(self._on_trims)
|
|
88
|
+
clayout.addWidget(tbox)
|
|
89
|
+
|
|
90
|
+
self.btn_start = QPushButton("Start")
|
|
91
|
+
self.btn_stop = QPushButton("Stop")
|
|
92
|
+
self.btn_export = QPushButton("Export CSV")
|
|
93
|
+
self.btn_start.clicked.connect(self.timer.start)
|
|
94
|
+
self.btn_stop.clicked.connect(self.timer.stop)
|
|
95
|
+
self.btn_export.clicked.connect(self._on_export)
|
|
96
|
+
for b in (self.btn_start, self.btn_stop, self.btn_export):
|
|
97
|
+
clayout.addWidget(b)
|
|
98
|
+
clayout.addStretch(1)
|
|
99
|
+
|
|
100
|
+
self.canvas = MplCanvas()
|
|
101
|
+
main_layout.addWidget(control)
|
|
102
|
+
main_layout.addWidget(self.canvas, stretch=1)
|
|
103
|
+
|
|
104
|
+
def _on_trims(self):
|
|
105
|
+
self.engine.set_global_trims(self.spin_k.value(), self.spin_phi.value())
|
|
106
|
+
|
|
107
|
+
def _on_export(self):
|
|
108
|
+
p = self.engine.export_csv()
|
|
109
|
+
self.statusBar().showMessage(f"Exported: {p.resolve()}")
|
|
110
|
+
|
|
111
|
+
def _on_timer(self):
|
|
112
|
+
e = self.engine
|
|
113
|
+
e.step()
|
|
114
|
+
for ax, hist, label in ((self.canvas.ax_p, e.history_P, "Pressure (psi)"),
|
|
115
|
+
(self.canvas.ax_t, e.history_T, "Temperature (\N{DEGREE SIGN}F)")):
|
|
116
|
+
ax.clear()
|
|
117
|
+
ax.set_facecolor("#101a2e")
|
|
118
|
+
ax.grid(True, alpha=0.3)
|
|
119
|
+
ax.set_ylabel(label, color="#e0f2fe")
|
|
120
|
+
ax.tick_params(colors="#e0f2fe")
|
|
121
|
+
for i, s in enumerate(e.sensors):
|
|
122
|
+
ax.plot(e.history_t, [row[i] for row in hist], lw=1.4,
|
|
123
|
+
label=f"{s.name} ({s.depth_ft:.0f} ft)")
|
|
124
|
+
ax.legend(fontsize=7, loc="upper right")
|
|
125
|
+
s = e.summary()
|
|
126
|
+
self.statusBar().showMessage(
|
|
127
|
+
f"t={s['time_s']}s avg drift={s['avg_drift_pct']}% FS/yr "
|
|
128
|
+
f"suppression={s['canonical_suppression_at_unity_trims']} (unity trims)")
|
|
129
|
+
self.canvas.draw_idle()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def main():
|
|
133
|
+
if not QT_AVAILABLE:
|
|
134
|
+
print("PyQt6/matplotlib not available. Install with: pip install PyQt6 matplotlib")
|
|
135
|
+
return 1
|
|
136
|
+
app = QApplication(sys.argv)
|
|
137
|
+
win = MainWindow()
|
|
138
|
+
win.show()
|
|
139
|
+
win.timer.start()
|
|
140
|
+
return app.exec()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
if __name__ == "__main__":
|
|
144
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,152 @@
|
|
|
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
|
+
"""quartz_hpht_extension - the gauge aging models.
|
|
5
|
+
|
|
6
|
+
Two aging models evaluated at station pressure and temperature: the conventional
|
|
7
|
+
datasheet model (baseline drift scaled by thermal and pressure factors with the
|
|
8
|
+
template knees and exponents) and the program aging model, which multiplies the
|
|
9
|
+
conventional model by a fixed suppression composition of three engineering
|
|
10
|
+
constants (K_MEX = 25/12, PHI_RES = 0.84, F_TRZ = 0.1; ratio 0.9686). The
|
|
11
|
+
suppression composition is an engineering model with no field validation on
|
|
12
|
+
record; the model card says so, and the drift evaluation uses the band between
|
|
13
|
+
the two models, never the program model alone. `k_structural_trim` and
|
|
14
|
+
`phi_coupling_trim` (default 1.0) are instrument-tuning gains. Industry anchors
|
|
15
|
+
carry inline source comments.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
# Engineering constants of the program aging model (template lineage, 22 Aug 2026).
|
|
21
|
+
# They are settings, not measurements; the model card labels this model as
|
|
22
|
+
# having no field validation on record.
|
|
23
|
+
_K_MEX = 25.0 / 12.0 # structural factor (engineering constant)
|
|
24
|
+
_PHI_RES = 0.84 # resonance factor (engineering constant)
|
|
25
|
+
_F_TRZ = 0.1 # vacuum-stability factor (engineering constant)
|
|
26
|
+
_U_I = 2.75e-7 # coupling constant (engineering constant)
|
|
27
|
+
PROGRAM_MODEL_AVAILABLE = True
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def canonical_suppression(k_structural_trim: float = 1.0,
|
|
31
|
+
phi_coupling_trim: float = 1.0) -> float:
|
|
32
|
+
"""The GEA drift-suppression composition, canonical primitives locked.
|
|
33
|
+
|
|
34
|
+
Template lineage (22Aug2026 thread): suppression = vacuum * structural *
|
|
35
|
+
resonance. The three dressings' coefficients are the template's engineering
|
|
36
|
+
fit (DERIVED_HYBRID); the primitive INPUTS are canonical and immutable.
|
|
37
|
+
Trims are external instrument gains, applied multiplicatively and
|
|
38
|
+
disclosed as such.
|
|
39
|
+
"""
|
|
40
|
+
vacuum_stab = 0.58 + 0.32 * (1.0 - _F_TRZ) # rho_SCm/rho_UA = F_TRZ (canonical)
|
|
41
|
+
structural = 0.52 + 0.38 * _K_MEX # K_MEX = 25/12 (canonical)
|
|
42
|
+
resonance = 0.68 + 0.27 * _PHI_RES # Phi_res = 0.84 (canonical)
|
|
43
|
+
return vacuum_stab * structural * resonance * float(k_structural_trim) * float(phi_coupling_trim)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def calculate_quartz_transducer_hpht_program(depth_m: float,
|
|
47
|
+
temp_c: float,
|
|
48
|
+
pressure_psi: float,
|
|
49
|
+
k_structural_trim: float = 1.0,
|
|
50
|
+
phi_coupling_trim: float = 1.0,
|
|
51
|
+
spec=None) -> dict:
|
|
52
|
+
"""Physics-informed quartz HPHT drift model (GEA-stabilized).
|
|
53
|
+
|
|
54
|
+
Returns the template's rich dictionary shape, current-API values.
|
|
55
|
+
Anchors (inline, per charter): 0.215 %FS/yr typical good-quartz baseline
|
|
56
|
+
(industry spec class); 150 C thermal knee / 15,000 psi pressure knee with
|
|
57
|
+
exponents 1.15 / 0.9 (template engineering fit); clip band 0.035-0.48 %FS/yr
|
|
58
|
+
(physical plausibility bounds, template).
|
|
59
|
+
|
|
60
|
+
`spec` (v1.5.0): an optional GaugeSpec (gauge_specs) replacing the
|
|
61
|
+
template anchors with a cited datasheet's baseline/knees/exponents. The
|
|
62
|
+
clip band scales proportionally with the baseline so a datasheet bound
|
|
63
|
+
~20x below the template baseline is not floored by template-scaled clips.
|
|
64
|
+
With spec=None every number is bit-identical to v1.0-1.4.
|
|
65
|
+
"""
|
|
66
|
+
base_drift = 0.215 # anchor: %FS/yr typical good-quartz baseline (industry)
|
|
67
|
+
knee_C, knee_psi = 150.0, 15000.0 # anchors: industry knees (template; ChampionX confirms >=150 C focus)
|
|
68
|
+
exp_T, exp_P = 1.15, 0.9 # template engineering fit
|
|
69
|
+
if spec is not None:
|
|
70
|
+
base_drift = float(spec.baseline_drift_pct_fs_yr)
|
|
71
|
+
knee_C = float(spec.thermal_knee_C)
|
|
72
|
+
knee_psi = float(spec.pressure_knee_psi)
|
|
73
|
+
exp_T = float(spec.thermal_exponent)
|
|
74
|
+
exp_P = float(spec.pressure_exponent)
|
|
75
|
+
|
|
76
|
+
thermal_stress = max(0.0, (temp_c - knee_C) / 80.0) ** exp_T
|
|
77
|
+
pressure_stress = max(0.0, (pressure_psi - knee_psi) / 5000.0) ** exp_P
|
|
78
|
+
|
|
79
|
+
suppression = canonical_suppression(k_structural_trim, phi_coupling_trim)
|
|
80
|
+
|
|
81
|
+
drift = base_drift * (1.0 + 0.55 * thermal_stress + 0.35 * pressure_stress) / suppression
|
|
82
|
+
clip_scale = base_drift / 0.215 # clip band scales with the baseline (template-exact at 0.215)
|
|
83
|
+
drift = min(max(drift, 0.035 * clip_scale), 0.48 * clip_scale)
|
|
84
|
+
|
|
85
|
+
expected_temp_c = 15.0 + (depth_m / 1000.0) * 29.5 # anchor: ~29.5 C/km geothermal gradient
|
|
86
|
+
hydrostatic_psi = depth_m * 3.28084 * 0.465 # anchor: 0.465 psi/ft gradient
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
"value": {
|
|
90
|
+
"drift_pct": round(drift, 4),
|
|
91
|
+
"stability_factor": round(base_drift / drift, 3),
|
|
92
|
+
"suppression": round(suppression, 4),
|
|
93
|
+
"rho_SCm_over_rho_UA": _F_TRZ,
|
|
94
|
+
"U_i": _U_I,
|
|
95
|
+
"K_MEX_canonical": _K_MEX,
|
|
96
|
+
"Phi_res_canonical": _PHI_RES,
|
|
97
|
+
"k_structural_trim": float(k_structural_trim),
|
|
98
|
+
"phi_coupling_trim": float(phi_coupling_trim),
|
|
99
|
+
"expected_temp_c": round(expected_temp_c, 1),
|
|
100
|
+
"hydrostatic_psi": round(hydrostatic_psi, 0),
|
|
101
|
+
"live": PROGRAM_MODEL_AVAILABLE,
|
|
102
|
+
"gauge_spec": spec.name if spec is not None else "template_generic (default)",
|
|
103
|
+
},
|
|
104
|
+
"classification": "DERIVED_HYBRID (PAPER_2149): industry baseline x canonical-GEA suppression",
|
|
105
|
+
"notes": "gea-program port of the 22Aug2026 QCALCGEOM template; knob ruling applied",
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def conventional_drift(temp_c: float, pressure_psi: float, spec=None) -> float:
|
|
110
|
+
"""Conventional-gauge drift: SAME baseline and stress dressing as the GEA
|
|
111
|
+
leg (from the template anchors or the given GaugeSpec), NO GEA suppression
|
|
112
|
+
(suppression = 1). The comparison-mode reference leg.
|
|
113
|
+
"""
|
|
114
|
+
base_drift = 0.215 # anchor: same industry baseline as the program-model leg
|
|
115
|
+
knee_C, knee_psi, exp_T, exp_P = 150.0, 15000.0, 1.15, 0.9
|
|
116
|
+
if spec is not None:
|
|
117
|
+
base_drift = float(spec.baseline_drift_pct_fs_yr)
|
|
118
|
+
knee_C = float(spec.thermal_knee_C)
|
|
119
|
+
knee_psi = float(spec.pressure_knee_psi)
|
|
120
|
+
exp_T = float(spec.thermal_exponent)
|
|
121
|
+
exp_P = float(spec.pressure_exponent)
|
|
122
|
+
thermal_stress = max(0.0, (temp_c - knee_C) / 80.0) ** exp_T
|
|
123
|
+
pressure_stress = max(0.0, (pressure_psi - knee_psi) / 5000.0) ** exp_P
|
|
124
|
+
drift = base_drift * (1.0 + 0.55 * thermal_stress + 0.35 * pressure_stress)
|
|
125
|
+
clip_scale = base_drift / 0.215
|
|
126
|
+
return min(max(drift, 0.035 * clip_scale), 0.48 * clip_scale)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def drift_comparison(depth_m: float, temp_c: float, pressure_psi: float,
|
|
130
|
+
k_structural_trim: float = 1.0,
|
|
131
|
+
phi_coupling_trim: float = 1.0,
|
|
132
|
+
spec=None) -> dict:
|
|
133
|
+
"""Twin-gauge comparison at matched T/P: GEA-stabilized vs conventional.
|
|
134
|
+
|
|
135
|
+
The module's substantive testable claim (PAPER_2256 sec 5): away from the
|
|
136
|
+
clip band, the conventional/GEA drift ratio EQUALS the suppression
|
|
137
|
+
composition — 1.0324 at unity trims. This function is the simulation side
|
|
138
|
+
of the quartz bench test (PAPER_2250 LABORATORY tier).
|
|
139
|
+
"""
|
|
140
|
+
uq = calculate_quartz_transducer_hpht_program(
|
|
141
|
+
depth_m, temp_c, pressure_psi, k_structural_trim, phi_coupling_trim, spec=spec)
|
|
142
|
+
conv = conventional_drift(temp_c, pressure_psi, spec=spec)
|
|
143
|
+
uqd = uq["value"]["drift_pct"]
|
|
144
|
+
sup = uq["value"]["suppression"]
|
|
145
|
+
return {
|
|
146
|
+
"drift_pct": uqd,
|
|
147
|
+
"conventional_drift_pct": round(conv, 4),
|
|
148
|
+
"measured_ratio": round(conv / uqd, 4) if uqd > 0 else None,
|
|
149
|
+
"predicted_ratio_suppression": sup,
|
|
150
|
+
"clip_active": bool(conv >= 0.48 or uqd <= 0.035),
|
|
151
|
+
"note": "measured_ratio == suppression exactly when neither leg clips",
|
|
152
|
+
}
|