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.
Files changed (163) hide show
  1. gea/BENCH_TEST_PROTOCOL.md +97 -0
  2. gea/IMPORT_RECORD.md +61 -0
  3. gea/__init__.py +160 -0
  4. gea/__main__.py +661 -0
  5. gea/acceptance_tests.py +1315 -0
  6. gea/accuracy_statement.py +181 -0
  7. gea/alarm_engine.py +307 -0
  8. gea/bench.py +144 -0
  9. gea/blind_harness.py +108 -0
  10. gea/case_study.py +229 -0
  11. gea/catalog/acex_lomonosov_age_depth_model.provenance.json +23 -0
  12. gea/catalog/acex_lomonosov_age_depth_model.txt +28 -0
  13. gea/catalog/agassiz77_canada_temperature.csv +68 -0
  14. gea/catalog/agassiz77_canada_temperature.provenance.json +17 -0
  15. gea/catalog/barbados_110_consolidation.provenance.json +24 -0
  16. gea/catalog/barbados_110_consolidation.txt +91 -0
  17. gea/catalog/bengal_u1452_grain_size.provenance.json +20 -0
  18. gea/catalog/bengal_u1452_grain_size.txt +252 -0
  19. gea/catalog/blake_164_methane_isotopes.provenance.json +22 -0
  20. gea/catalog/blake_164_methane_isotopes.txt +68 -0
  21. gea/catalog/chicxulub_m0077a_pwave_velocity.provenance.json +23 -0
  22. gea/catalog/chicxulub_m0077a_pwave_velocity.txt +735 -0
  23. gea/catalog/collingwood_1_28_ks_complete.las +128 -0
  24. gea/catalog/collingwood_1_28_ks_complete.provenance.json +17 -0
  25. gea/catalog/costa_rica_odp_friction_envelope.provenance.json +24 -0
  26. gea/catalog/costa_rica_odp_friction_envelope.txt +57 -0
  27. gea/catalog/dead_sea_5017_debrite_xrf_ms.provenance.json +21 -0
  28. gea/catalog/dead_sea_5017_debrite_xrf_ms.txt +94 -0
  29. gea/catalog/dsdp_504b_physical_properties.provenance.json +24 -0
  30. gea/catalog/dsdp_504b_physical_properties.txt +82 -0
  31. gea/catalog/dsdp_504b_sound_velocity.provenance.json +21 -0
  32. gea/catalog/dsdp_504b_sound_velocity.txt +81 -0
  33. gea/catalog/elgygytgyn_5011_turbidites.provenance.json +20 -0
  34. gea/catalog/elgygytgyn_5011_turbidites.txt +193 -0
  35. gea/catalog/epica_domec_co2_800kyr.provenance.json +21 -0
  36. gea/catalog/epica_domec_co2_800kyr.txt +265 -0
  37. gea/catalog/fram_909_organic_petrography.provenance.json +22 -0
  38. gea/catalog/fram_909_organic_petrography.txt +40 -0
  39. gea/catalog/gbr_325_coral_uth_ages.provenance.json +23 -0
  40. gea/catalog/gbr_325_coral_uth_ages.txt +71 -0
  41. gea/catalog/gisp2_greenland_temperature.csv +599 -0
  42. gea/catalog/gisp2_greenland_temperature.provenance.json +17 -0
  43. gea/catalog/gom_308_t2p_insitu.provenance.json +21 -0
  44. gea/catalog/gom_308_t2p_insitu.txt +40 -0
  45. gea/catalog/guaymas_385_dom_d13c.provenance.json +21 -0
  46. gea/catalog/guaymas_385_dom_d13c.txt +103 -0
  47. gea/catalog/hikurangi_u1520_friction_insitu.provenance.json +26 -0
  48. gea/catalog/hikurangi_u1520_friction_insitu.txt +74 -0
  49. gea/catalog/hydrate_ridge_204_ncr_hydrate.provenance.json +22 -0
  50. gea/catalog/hydrate_ridge_204_ncr_hydrate.txt +60 -0
  51. gea/catalog/iodp_u1324_pore_pressure.provenance.json +22 -0
  52. gea/catalog/iodp_u1324_pore_pressure.txt +42 -0
  53. gea/catalog/jfast_c0019_slow_slip_events.provenance.json +28 -0
  54. gea/catalog/jfast_c0019_slow_slip_events.txt +35 -0
  55. gea/catalog/kennetcook_2_p129_excerpt.las +139 -0
  56. gea/catalog/kennetcook_2_p129_excerpt.provenance.json +17 -0
  57. gea/catalog/ktb_hb_bhgm_density.dat +227 -0
  58. gea/catalog/ktb_hb_bhgm_density.provenance.json +22 -0
  59. gea/catalog/ktb_hb_complog_6020_excerpt.provenance.json +20 -0
  60. gea/catalog/ktb_hb_complog_6020_excerpt.txt +72 -0
  61. gea/catalog/ktb_hb_hlog246_temperature.dat +1636 -0
  62. gea/catalog/ktb_hb_hlog246_temperature.provenance.json +22 -0
  63. gea/catalog/ktb_hb_rockmech_compress.dat +33 -0
  64. gea/catalog/ktb_hb_rockmech_compress.provenance.json +22 -0
  65. gea/catalog/ktb_hb_tvd_0_9080_excerpt.dat +2817 -0
  66. gea/catalog/ktb_hb_tvd_0_9080_excerpt.provenance.json +20 -0
  67. gea/catalog/ktb_vb_rockmech_compress.dat +125 -0
  68. gea/catalog/ktb_vb_rockmech_compress.provenance.json +22 -0
  69. gea/catalog/ktb_vb_vlog251_temperature.dat +1127 -0
  70. gea/catalog/ktb_vb_vlog251_temperature.provenance.json +24 -0
  71. gea/catalog/l06_06_nl_survey.csv +201 -0
  72. gea/catalog/l06_06_nl_survey.provenance.json +17 -0
  73. gea/catalog/l07_01_nl_excerpt.las +90 -0
  74. gea/catalog/l07_01_nl_excerpt.provenance.json +17 -0
  75. gea/catalog/mariana_1200_serpentinite_geochem.provenance.json +21 -0
  76. gea/catalog/mariana_1200_serpentinite_geochem.txt +63 -0
  77. gea/catalog/med_160_sapropels.provenance.json +22 -0
  78. gea/catalog/med_160_sapropels.txt +43 -0
  79. gea/catalog/nankai_megasplay_shear_strength.provenance.json +26 -0
  80. gea/catalog/nankai_megasplay_shear_strength.txt +42 -0
  81. gea/catalog/odp_1027b_thermal_conductivity.provenance.json +21 -0
  82. gea/catalog/odp_1027b_thermal_conductivity.txt +52 -0
  83. gea/catalog/odp_1027c_cork_temperature.provenance.json +20 -0
  84. gea/catalog/odp_1027c_cork_temperature.txt +26 -0
  85. gea/catalog/odp_1165b_thermal_conductivity.provenance.json +22 -0
  86. gea/catalog/odp_1165b_thermal_conductivity.txt +102 -0
  87. gea/catalog/odp_1274a_mantle_peridotite_mad.provenance.json +27 -0
  88. gea/catalog/odp_1274a_mantle_peridotite_mad.txt +44 -0
  89. gea/catalog/odp_504b_dike_elastic_moduli.provenance.json +27 -0
  90. gea/catalog/odp_504b_dike_elastic_moduli.txt +85 -0
  91. gea/catalog/odp_504b_leg137_borehole_fluids.provenance.json +19 -0
  92. gea/catalog/odp_504b_leg137_borehole_fluids.txt +68 -0
  93. gea/catalog/odp_735b_gabbro_elastic_moduli.provenance.json +28 -0
  94. gea/catalog/odp_735b_gabbro_elastic_moduli.txt +127 -0
  95. gea/catalog/peru_201_sulfate_reduction.provenance.json +22 -0
  96. gea/catalog/peru_201_sulfate_reduction.txt +322 -0
  97. gea/catalog/scorpio_e1_sa_excerpt.las +113 -0
  98. gea/catalog/scorpio_e1_sa_excerpt.provenance.json +17 -0
  99. gea/catalog/sumatra_362_cohesion.provenance.json +21 -0
  100. gea/catalog/sumatra_362_cohesion.txt +38 -0
  101. gea/catalog/university_6_17_no1_tx_excerpt.las +119 -0
  102. gea/catalog/university_6_17_no1_tx_excerpt.provenance.json +17 -0
  103. gea/catalog/ursa_308_xrd_mineralogy.provenance.json +21 -0
  104. gea/catalog/ursa_308_xrd_mineralogy.txt +46 -0
  105. gea/catalog/volve_15_9_19_sr_excerpt.las +183 -0
  106. gea/catalog/volve_15_9_19_sr_excerpt.provenance.json +16 -0
  107. gea/catalog/volve_15_9_19a_core_excerpt.csv +88 -0
  108. gea/catalog/volve_15_9_19a_core_excerpt.provenance.json +18 -0
  109. gea/catalog/volve_f12_f14_production_excerpt.csv +167 -0
  110. gea/catalog/volve_f12_f14_production_excerpt.provenance.json +21 -0
  111. gea/catalog/walvis_208_petm_carbonate.provenance.json +21 -0
  112. gea/catalog/walvis_208_petm_carbonate.txt +268 -0
  113. gea/catalog/woodlark_1109_rock_eval.provenance.json +23 -0
  114. gea/catalog/woodlark_1109_rock_eval.txt +30 -0
  115. gea/cli.py +125 -0
  116. gea/client_reports.py +943 -0
  117. gea/config_versioning.py +133 -0
  118. gea/correlation.py +155 -0
  119. gea/dashboard.py +390 -0
  120. gea/deviation.py +70 -0
  121. gea/downhole_engine.py +395 -0
  122. gea/drift_monitor.py +310 -0
  123. gea/earth_model.py +230 -0
  124. gea/example_register_map.json +14 -0
  125. gea/fat_sat.py +68 -0
  126. gea/follower.py +98 -0
  127. gea/forward_model.py +139 -0
  128. gea/gamma.py +176 -0
  129. gea/gauge_specs.py +112 -0
  130. gea/gravity_reference.py +116 -0
  131. gea/inverse_engine.py +215 -0
  132. gea/matplotlib_demo.py +85 -0
  133. gea/modbus.py +229 -0
  134. gea/model_card.py +248 -0
  135. gea/operator_app.py +442 -0
  136. gea/ports.py +340 -0
  137. gea/profile_catalog.py +773 -0
  138. gea/project.py +213 -0
  139. gea/qt6_downhole_app.py +144 -0
  140. gea/quartz_hpht_extension.py +152 -0
  141. gea/reconciler.py +206 -0
  142. gea/rock_inventory.py +404 -0
  143. gea/sample_record.py +430 -0
  144. gea/sample_well_profile.csv +15 -0
  145. gea/sbom.py +116 -0
  146. gea/segy.py +181 -0
  147. gea/service_life.py +173 -0
  148. gea/shell.py +107 -0
  149. gea/sla_report.py +199 -0
  150. gea/store_forward.py +234 -0
  151. gea/strata_join.py +186 -0
  152. gea/survey_cmd.py +264 -0
  153. gea/survey_view.py +138 -0
  154. gea/telemetry.py +306 -0
  155. gea/tool_library.py +260 -0
  156. gea/well_assembler.py +457 -0
  157. gea/well_test_validation.py +369 -0
  158. gea_program-0.1.0.dist-info/METADATA +138 -0
  159. gea_program-0.1.0.dist-info/RECORD +163 -0
  160. gea_program-0.1.0.dist-info/WHEEL +5 -0
  161. gea_program-0.1.0.dist-info/entry_points.txt +2 -0
  162. gea_program-0.1.0.dist-info/licenses/LICENSE +373 -0
  163. 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('![site map](site_map.png)')
154
+ w('')
155
+ w('![inversion cross-section](inversion_cross_section.png)')
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
@@ -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
+ }