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/survey_cmd.py ADDED
@@ -0,0 +1,264 @@
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
+ """survey_cmd - THE ONE-COMMAND USER PATH (Daniel's build order,
5
+ 2026-09-08: "build the survey command").
6
+
7
+ gea survey mywell.las
8
+ gea survey --demo
9
+
10
+ A person with a LAS file and zero knowledge of this repository's history
11
+ gets one readable answer: what the ground under this well looks like, how
12
+ sure the tool is, and what it refused to guess. The chain is nothing new -
13
+ it is the SAME engine the acceptance suite already gates (LAS 2.0 reader,
14
+ K2 forward kernel, family-prior strata estimation, cited WGS84 reference
15
+ QC) - walked end-to-end behind a single door.
16
+
17
+ HONESTY CONTRACT (unchanged from every layer below):
18
+ * every estimate carries n, spread, and support - or it is refused;
19
+ * every exclusion (nulls, washouts) is counted and printed;
20
+ * every number names its provenance;
21
+ * the prior family is an ASSUMPTION and the report says so;
22
+ * nothing is invented for missing channels - the tool says what it
23
+ cannot do with this file and what channel would unlock it.
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import statistics
29
+ from typing import Dict, List, Optional, Tuple
30
+
31
+ from .ports import read_las
32
+ from .profile_catalog import CATALOG
33
+ from .forward_model import predict_delta_g_mgal, implied_density_gcc
34
+ from .inverse_engine import (PRIOR_FAMILIES, WASHOUT_RHO_GCC,
35
+ _site_native_pairs, _conditional_from_pairs)
36
+ from .gravity_reference import site_reference_gravity
37
+
38
+ DEMO_ENTRY = 'ktb_hb_complog_6020_excerpt'
39
+
40
+ # channel-name synonyms (case-insensitive prefix match on the mnemonic)
41
+ _SYNONYMS = {
42
+ 'density': ('RHOB', 'RHOZ', 'DEN', 'RHO'),
43
+ 'sonic': ('DTCO', 'DT', 'AC'),
44
+ 'gravity': ('GRAV', 'GRV'),
45
+ 'gamma': ('SGR', 'GR', 'HSGR'),
46
+ }
47
+
48
+
49
+ def _find_channel(channels: Dict, kind: str) -> Optional[str]:
50
+ for key in channels:
51
+ mnem = key.split()[0].split('(')[0].strip().upper()
52
+ for syn in _SYNONYMS[kind]:
53
+ if mnem == syn or mnem.startswith(syn):
54
+ return key
55
+ return None
56
+
57
+
58
+ def _floats(channels, key) -> List[float]:
59
+ return [float(v) for v in channels[key].values]
60
+
61
+
62
+ def run_survey(path: Optional[str] = None, demo: bool = False,
63
+ family: str = 'continental_crystalline',
64
+ lat: Optional[float] = None,
65
+ elev: Optional[float] = None) -> Tuple[str, Dict]:
66
+ """Run the end-to-end survey chain; returns (report_text, machine_dict)."""
67
+ L: List[str] = []
68
+ out: Dict = {'refusals': [], 'exclusions': {}}
69
+ w = L.append
70
+
71
+ if demo:
72
+ st = CATALOG[DEMO_ENTRY].stream()
73
+ src_label = ('bundled demo: %s (KTB main hole composite-log '
74
+ 'excerpt, GFZ public archive)' % DEMO_ENTRY)
75
+ else:
76
+ if not path:
77
+ raise SystemExit('survey: give a LAS file, or use --demo')
78
+ st = read_las(path)
79
+ src_label = '%s (LAS 2.0, read by the las2 port)' % path
80
+
81
+ ch = st.channels
82
+ depth = [float(v) for v in st.index]
83
+ keys = {k: _find_channel(ch, k) for k in _SYNONYMS}
84
+ out['source'] = src_label
85
+ out['channels_found'] = {k: v for k, v in keys.items() if v}
86
+ out['n_stations'] = len(depth)
87
+
88
+ w('GEA SURVEY - one well, one honest answer')
89
+ w('=' * 60)
90
+ w('source: %s' % src_label)
91
+ w('stations: %d depth %.1f - %.1f m' % (len(depth),
92
+ min(depth), max(depth)))
93
+ w('channels recognized: %s' % (', '.join(
94
+ '%s -> %s' % (k, v) for k, v in keys.items() if v) or 'NONE'))
95
+ w('')
96
+
97
+ fam = PRIOR_FAMILIES.get(family)
98
+ if fam is None:
99
+ raise SystemExit('survey: unknown prior family %r (choose from %s)'
100
+ % (family, sorted(PRIOR_FAMILIES)))
101
+
102
+ # ---- density column: measured, or implied from gravity ---------------
103
+ rho: Optional[List[float]] = None
104
+ rho_provenance = ''
105
+ if keys['density']:
106
+ rho = _floats(ch, keys['density'])
107
+ rho_provenance = ('measured density channel %s' % keys['density'])
108
+ elif keys['gravity']:
109
+ grav = _floats(ch, keys['gravity'])
110
+ rho = [float('nan')] * len(depth)
111
+ for i in range(1, len(depth)):
112
+ dz = depth[i] - depth[i - 1]
113
+ if dz > 0:
114
+ rho[i] = implied_density_gcc(grav[i] - grav[i - 1], dz)
115
+ rho_provenance = ('density IMPLIED from interstation gravity via the '
116
+ 'K2 GEA kernel (forward_model)')
117
+ else:
118
+ out['refusals'].append(
119
+ 'no density (RHOB) and no gravity (GRAV) channel - the tool '
120
+ 'cannot characterize strata from this file; add either channel '
121
+ 'to unlock the chain')
122
+ w('REFUSED: %s' % out['refusals'][-1])
123
+ out['report'] = '\n'.join(L)
124
+ return '\n'.join(L), out
125
+
126
+ # washout / null exclusion (disclosed)
127
+ keep = [i for i, r in enumerate(rho)
128
+ if r == r and r > WASHOUT_RHO_GCC]
129
+ n_excluded = len(depth) - len(keep)
130
+ out['exclusions']['washout_or_null_stations'] = n_excluded
131
+ w('density column: %s' % rho_provenance)
132
+ w(' %d stations kept; %d excluded (nulls / washouts RHOB <= %.1f '
133
+ 'g/cc) - hole artifacts, not rock' % (len(keep), n_excluded,
134
+ WASHOUT_RHO_GCC))
135
+ rho_k = [rho[i] for i in keep]
136
+ if len(rho_k) < 5:
137
+ out['refusals'].append('fewer than 5 usable stations - refusing to '
138
+ 'summarize strata on this little support')
139
+ w('REFUSED: %s' % out['refusals'][-1])
140
+ out['report'] = '\n'.join(L)
141
+ return '\n'.join(L), out
142
+
143
+ w(' density %.2f - %.2f g/cc (mean %.2f, n=%d)'
144
+ % (min(rho_k), max(rho_k), statistics.mean(rho_k), len(rho_k)))
145
+ out['density_gcc'] = {'min': min(rho_k), 'max': max(rho_k),
146
+ 'mean': statistics.mean(rho_k), 'n': len(rho_k)}
147
+ w('')
148
+
149
+ # ---- forward gravity signature (K2 GEA kernel) ----------------------
150
+ dzs = [depth[keep[j]] - depth[keep[j - 1]] for j in range(1, len(keep))]
151
+ dz_med = statistics.median(dzs) if dzs else 0.0
152
+ if dz_med > 0:
153
+ dgs = [predict_delta_g_mgal(r, dz_med) for r in rho_k]
154
+ w('predicted interstation gravity signature (K2 GEA kernel, '
155
+ 'standard constants): %.4f to %.4f mGal per %.2f m station'
156
+ % (min(dgs), max(dgs), dz_med))
157
+ out['k2_dg_mgal'] = {'min': min(dgs), 'max': max(dgs),
158
+ 'dz_m': dz_med}
159
+ w('')
160
+
161
+ # ---- Vp estimate via the chosen prior family -------------------------
162
+ pairs, washouts = (None, 0)
163
+ if fam['kind'] == 'site_pairs':
164
+ pairs, washouts = _site_native_pairs(fam['source'])
165
+ if pairs:
166
+ ests = [_conditional_from_pairs(pairs, r) for r in rho_k]
167
+ oks = [e for e in ests if e.get('status') == 'OK'
168
+ and e.get('estimate') and not e.get('extrapolation')]
169
+ n_extrap = sum(1 for e in ests if e.get('extrapolation'))
170
+ if oks:
171
+ vals = [e['estimate'] for e in oks]
172
+ stds = [e['std'] for e in oks if e.get('std')]
173
+ w('Vp (compressional velocity) estimate:')
174
+ w(' %d - %d m/s (mean %d, typical 1-sigma %d, n=%d '
175
+ 'in-support stations; %d extrapolating stations WITHHELD)'
176
+ % (min(vals), max(vals), statistics.mean(vals),
177
+ statistics.mean(stds) if stds else 0, len(oks), n_extrap))
178
+ w(' ASSUMPTION (printed, not hidden): prior family = %s - %s'
179
+ % (family, fam['note']))
180
+ out['vp_m_s'] = {'min': min(vals), 'max': max(vals),
181
+ 'mean': statistics.mean(vals), 'n': len(oks),
182
+ 'withheld_extrapolating': n_extrap,
183
+ 'prior_family': family}
184
+ else:
185
+ out['refusals'].append('all stations outside the prior '
186
+ 'family support - no Vp estimate rather '
187
+ 'than an extrapolated one')
188
+ w('REFUSED: %s' % out['refusals'][-1])
189
+ # honesty cross-check where the file carries its own sonic
190
+ if keys['sonic']:
191
+ son = _floats(ch, keys['sonic'])
192
+ meas = [1e6 / son[i] for i in keep if son[i] > 0]
193
+ if meas and oks:
194
+ mm = statistics.mean(meas)
195
+ pm = statistics.mean(vals)
196
+ w(' CROSS-CHECK: this file carries its own sonic (%s) - '
197
+ 'measured mean Vp %d m/s vs estimated %d (%+.1f%%); the '
198
+ 'tool grades itself when the data allows'
199
+ % (keys['sonic'], mm, pm, (pm - mm) / mm * 100))
200
+ out['vp_cross_check_pct'] = (pm - mm) / mm * 100
201
+ w('')
202
+
203
+ # ---- cited external reference QC ------------------------------------
204
+ if lat is not None and elev is not None:
205
+ ref = site_reference_gravity(lat, elev)
206
+ w('site reference gravity (CITED external standard - WGS84 '
207
+ 'Somigliana + free-air, NGA TR8350.2): %.5f m/s2 at %.4f N, '
208
+ '%.0f m' % (ref['reference_gravity_ms2'], lat, elev))
209
+ out['site_reference'] = ref
210
+ else:
211
+ w('site reference QC: skipped (give --lat and --elev to check '
212
+ 'this survey against the cited WGS84 reference)')
213
+ w('')
214
+
215
+ # ---- rock candidates (K4 geological landmark family) -----------------
216
+ from .rock_inventory import classify_density
217
+ votes = {}
218
+ for r in rho_k:
219
+ for h in classify_density(r)['candidates'][:3]:
220
+ votes[h['name']] = votes.get(h['name'], 0) + 1
221
+ ranked = sorted(votes.items(), key=lambda kv: -kv[1])[:3]
222
+ if ranked:
223
+ w('rock candidates (K4 landmark family, density-only, RANKED not '
224
+ 'certain):')
225
+ w(' ' + ', '.join('%s (%d/%d stations)' % (n, c, len(rho_k))
226
+ for n, c in ranked))
227
+ w(' HONESTY: density ranges overlap - these are candidates, not '
228
+ 'an identification; density-degenerate twins (e.g. '
229
+ 'amphibolite/basalt) are indistinguishable by design')
230
+ out['rock_candidates'] = ranked
231
+ else:
232
+ w('rock candidates: none - the density column is outside the K4 '
233
+ 'inventory (stated, not guessed)')
234
+ out['rock_candidates'] = []
235
+ if keys['sonic']:
236
+ from .rock_inventory import classify_joint
237
+ son2 = _floats(ch, keys['sonic'])
238
+ jvotes = {}
239
+ for i in keep:
240
+ if son2[i] > 0:
241
+ for h in classify_joint(rho[i], 1e6 / son2[i])['candidates'][:2]:
242
+ jvotes[h['name']] = jvotes.get(h['name'], 0) + 1
243
+ jranked = sorted(jvotes.items(), key=lambda kv: -kv[1])[:3]
244
+ if jranked:
245
+ w(' TWO-CHANNEL (density + sonic) shortlist - the sharper one: '
246
+ + ', '.join('%s (%d)' % (n, c) for n, c in jranked))
247
+ w(' the second channel splits the density-degenerate twins '
248
+ '(amphibolite/basalt Vp tiers are disjoint)')
249
+ out['rock_candidates_joint'] = jranked
250
+ w('')
251
+
252
+ # ---- what the tool will NOT do ---------------------------------------
253
+ w('what this tool refused to guess:')
254
+ w(' - a SINGLE confident rock name: density-only identification is '
255
+ 'a ranked shortlist with disclosed overlap, never a certainty')
256
+ if not keys['gamma']:
257
+ w(' - lithology proxies from gamma: no gamma channel in this file')
258
+ for r_ in out['refusals']:
259
+ w(' - %s' % r_)
260
+ w('')
261
+ w('every number above carries its provenance; the assumptions are '
262
+ 'printed where they act. This report is honest or it is nothing.')
263
+ out['report'] = '\n'.join(L)
264
+ return '\n'.join(L), out
gea/survey_view.py ADDED
@@ -0,0 +1,138 @@
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
+ """survey_view - the first HONEST renderer for the surveying tool
5
+ (v1.80.0: 'one honest map/cross-section renderer would make everything built
6
+ this week visible for the first time').
7
+
8
+ Two figures, both drawn ONLY from gate-verified objects, both labeled with
9
+ what they are NOT:
10
+
11
+ render_site_map(path) - every Earth Model site on a lat/lon frame.
12
+ Sites only - no basemap, no coastlines: the
13
+ map shows exactly what the library has
14
+ touched and nothing it has not.
15
+ render_ktb_inversion(path) - the KTB gravity inversion as a cross-section:
16
+ implied density vs depth, layer-boundary
17
+ candidates, and the Prediction-V2 Vp band
18
+ with its honest spread, statused
19
+ PINNED_AWAITING_DEEP_SONIC on the figure
20
+ itself.
21
+
22
+ Headless by construction (matplotlib Agg); no display required. If matplotlib
23
+ is absent the functions raise a clear ImportError naming the optional
24
+ dependency - the renderer is presentation, never load-bearing: no catalogue
25
+ entry, gate pin, or acceptance check REQUIRES it to exist (the v0.406.0
26
+ red-gate lesson applied in advance).
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ from typing import Optional
32
+
33
+
34
+ def _mpl():
35
+ try:
36
+ import matplotlib
37
+ matplotlib.use('Agg')
38
+ import matplotlib.pyplot as plt
39
+ return plt
40
+ except ImportError as e:
41
+ raise ImportError(
42
+ "survey_view needs the optional 'matplotlib' package "
43
+ "(pip install matplotlib). Rendering is presentation-only; "
44
+ "nothing in the gate or acceptance suite requires it.") from e
45
+
46
+
47
+ def render_site_map(path: str) -> dict:
48
+ """The library on one frame: every registered site, marked by kind of
49
+ ground (multi-entry sites emphasized). Returns a summary dict."""
50
+ plt = _mpl()
51
+ from .earth_model import EarthModel
52
+ em = EarthModel()
53
+ fig, ax = plt.subplots(figsize=(11, 5.5))
54
+ ones, multis = [], []
55
+ for s in em.sites.values():
56
+ (multis if len(s.entries) > 1 else ones).append(s)
57
+ ax.scatter([s.longitude for s in ones], [s.latitude for s in ones],
58
+ s=28, c='#2b6cb0', label='site (single entry)', zorder=3)
59
+ ax.scatter([s.longitude for s in multis], [s.latitude for s in multis],
60
+ s=90, c='#c05621', marker='*',
61
+ label='multi-entry site (reunited by coordinates)', zorder=4)
62
+ for s in multis:
63
+ ax.annotate('%d entries' % len(s.entries),
64
+ (s.longitude, s.latitude), fontsize=7,
65
+ xytext=(4, 4), textcoords='offset points')
66
+ ax.set_xlim(-180, 180)
67
+ ax.set_ylim(-90, 90)
68
+ ax.axhline(0, lw=0.4, color='0.75')
69
+ ax.axvline(0, lw=0.4, color='0.75')
70
+ ax.grid(True, lw=0.3, alpha=0.5)
71
+ ax.set_xlabel('longitude (deg)')
72
+ ax.set_ylabel('latitude (deg)')
73
+ cen = em.census()
74
+ ax.set_title('GEA Surveying Tool - Earth Model: %d registered sites, '
75
+ '%.0f km great-circle span' % (cen['sites'],
76
+ cen['great_circle_span_km']))
77
+ ax.text(0.01, 0.02,
78
+ 'HONESTY: sites only - no basemap; every point is an '
79
+ 'archive-declared coordinate; %d entries remain unregistered '
80
+ '(reasons on file)' % cen['unregistered_entries'],
81
+ transform=ax.transAxes, fontsize=7, color='0.35')
82
+ ax.legend(loc='lower right', fontsize=8)
83
+ fig.tight_layout()
84
+ fig.savefig(path, dpi=150)
85
+ plt.close(fig)
86
+ return {'sites_drawn': cen['sites'], 'multi_entry': len(multis),
87
+ 'path': path}
88
+
89
+
90
+ def render_ktb_inversion(path: str) -> dict:
91
+ """The inverse engine's KTB read as a cross-section: implied density
92
+ column, boundary candidates, and the Prediction-V2 band - honest spread
93
+ and unsettled status printed on the figure."""
94
+ plt = _mpl()
95
+ from .inverse_engine import invert_gravity_column
96
+ r = invert_gravity_column(prior_family='continental_crystalline')
97
+ est = r['estimates']
98
+ z = [e.depth_m for e in est]
99
+ rho = [e.implied_density_gcc for e in est]
100
+ ok = [e for e in est if e.posteriors['vp']['status'] == 'OK'
101
+ and not e.posteriors['vp']['extrapolation']]
102
+ vz = [e.depth_m for e in ok]
103
+ vp = [e.posteriors['vp']['estimate'] for e in ok]
104
+ vs = [e.posteriors['vp']['std'] for e in ok]
105
+
106
+ fig, (a1, a2) = plt.subplots(1, 2, figsize=(9, 8), sharey=True)
107
+ a1.plot(rho, z, lw=0.8, color='#2b6cb0')
108
+ for b in r['boundary_candidates']:
109
+ a1.axhline(b['depth_m'], lw=0.6, color='#c05621', alpha=0.7)
110
+ a1.invert_yaxis()
111
+ a1.set_xlabel('implied density (g/cc)\nfrom measured gravity + GEA constants')
112
+ a1.set_ylabel('depth (m)')
113
+ a1.set_title('KTB implied density\n(%d intervals; %d boundary candidates)'
114
+ % (r['n_intervals'], len(r['boundary_candidates'])))
115
+ a1.grid(True, lw=0.3, alpha=0.5)
116
+
117
+ a2.plot(vp, vz, lw=0.9, color='#276749')
118
+ a2.fill_betweenx(vz, [v - s for v, s in zip(vp, vs)],
119
+ [v + s for v, s in zip(vp, vs)],
120
+ color='#276749', alpha=0.18,
121
+ label='honest spread (+/- 1 sigma of prior support)')
122
+ a2.set_xlabel('predicted Vp (m/s)\ncontinental_crystalline family prior')
123
+ a2.set_title('Prediction V2 - Vp column\nSTATUS: PINNED_AWAITING_DEEP_SONIC')
124
+ a2.grid(True, lw=0.3, alpha=0.5)
125
+ a2.legend(loc='lower left', fontsize=7)
126
+ fig.suptitle('GEA Surveying Tool - the inverse engine reads the KTB '
127
+ 'gravity column (every value re-verified by the fidelity gate)',
128
+ fontsize=10)
129
+ fig.text(0.5, 0.005,
130
+ 'HONESTY: prior = 19 site-native pairs from a 10 m window '
131
+ '(entry 52); the previous prediction from a transferred prior '
132
+ 'was REFUTED (+10%) and is on the permanent record',
133
+ ha='center', fontsize=7, color='0.35')
134
+ fig.tight_layout(rect=(0, 0.02, 1, 0.97))
135
+ fig.savefig(path, dpi=150)
136
+ plt.close(fig)
137
+ return {'intervals_drawn': r['n_intervals'], 'vp_points': len(ok),
138
+ 'path': path}