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/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}
|