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/__main__.py
ADDED
|
@@ -0,0 +1,661 @@
|
|
|
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
|
+
"""Headless CLI for the GEA Downhole Simulator (v1.6.0 extension).
|
|
5
|
+
|
|
6
|
+
python -m gea run --steps 200 --out run.csv
|
|
7
|
+
python -m gea service-life --years 5 --out curves.csv
|
|
8
|
+
python -m gea telemetry --hours 24 --seed 11 --out field.csv
|
|
9
|
+
python -m gea case-study --td 25000 --out case.md
|
|
10
|
+
|
|
11
|
+
Shared options (all subcommands): --gauges N, --td FT, --profile CSV,
|
|
12
|
+
--spec PRESET|JSON, --kickoff FT --inclination DEG (deviation).
|
|
13
|
+
No display needed anywhere; every subcommand writes a file and prints a
|
|
14
|
+
one-line summary.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _add_well_args(p: argparse.ArgumentParser) -> None:
|
|
25
|
+
p.add_argument("--well", type=str, default=None,
|
|
26
|
+
help="MEASURED catalogue assembly (ktb_hb, site_1027, ...): base P/T "
|
|
27
|
+
"come from the archived well, not gradient templates; overrides --td/--profile")
|
|
28
|
+
p.add_argument("--td", type=float, default=None, help="total depth (MD), ft")
|
|
29
|
+
p.add_argument("--gauges", type=int, default=None, help="number of gauges (evenly spaced)")
|
|
30
|
+
p.add_argument("--profile", type=str, default=None, help="well profile CSV (depth_ft,pressure_psi,temp_F; TVD-indexed)")
|
|
31
|
+
p.add_argument("--spec", type=str, default=None, help="gauge spec: preset name or a datasheet JSON path")
|
|
32
|
+
p.add_argument("--kickoff", type=float, default=None, help="deviation: kickoff MD, ft")
|
|
33
|
+
p.add_argument("--inclination", type=float, default=None, help="deviation: tangent inclination, deg from vertical")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_config(a):
|
|
37
|
+
from . import (SimulatorConfig, load_well_profile_csv, make_sensor_string,
|
|
38
|
+
GAUGE_SPECS, load_gauge_spec_json, DEFAULT_TD_FT)
|
|
39
|
+
from .deviation import DeviationSurvey
|
|
40
|
+
if getattr(a, "well", None):
|
|
41
|
+
from . import demo_config, GAUGE_SPECS as _GS
|
|
42
|
+
kw2 = {}
|
|
43
|
+
if a.spec:
|
|
44
|
+
kw2["gauge_spec"] = (_GS[a.spec] if a.spec in _GS
|
|
45
|
+
else load_gauge_spec_json(a.spec))
|
|
46
|
+
cfg = demo_config(a.well, n_gauges=(a.gauges or 6), **kw2)
|
|
47
|
+
print(f"[well] {a.well}: profile '{cfg.profile.name}' - "
|
|
48
|
+
f"{len(cfg.sensor_depths_ft)} gauges inside the measured window "
|
|
49
|
+
f"{cfg.profile.depths_ft[0]:.0f}-{cfg.profile.depths_ft[-1]:.0f} ft")
|
|
50
|
+
return cfg
|
|
51
|
+
td = a.td if a.td is not None else DEFAULT_TD_FT
|
|
52
|
+
kw = {"td_ft": td}
|
|
53
|
+
if a.gauges is not None:
|
|
54
|
+
kw["sensor_depths_ft"] = make_sensor_string(a.gauges, td_ft=td)
|
|
55
|
+
if a.profile:
|
|
56
|
+
kw["profile"] = load_well_profile_csv(a.profile)
|
|
57
|
+
if a.spec:
|
|
58
|
+
kw["gauge_spec"] = (GAUGE_SPECS[a.spec] if a.spec in GAUGE_SPECS
|
|
59
|
+
else load_gauge_spec_json(a.spec))
|
|
60
|
+
if a.kickoff is not None or a.inclination is not None:
|
|
61
|
+
if a.kickoff is None or a.inclination is None:
|
|
62
|
+
raise SystemExit("--kickoff and --inclination must be given together")
|
|
63
|
+
kw["deviation"] = DeviationSurvey.from_kickoff(a.kickoff, a.inclination, td)
|
|
64
|
+
return SimulatorConfig(**kw)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main(argv=None) -> int:
|
|
68
|
+
ap = argparse.ArgumentParser(prog="gea",
|
|
69
|
+
description="GEA Downhole Simulator - headless CLI")
|
|
70
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
71
|
+
|
|
72
|
+
p_run = sub.add_parser("run", help="run the engine, export the P/T history CSV")
|
|
73
|
+
_add_well_args(p_run)
|
|
74
|
+
p_run.add_argument("--steps", type=int, default=200)
|
|
75
|
+
p_run.add_argument("--out", type=str, default="downhole_run.csv")
|
|
76
|
+
|
|
77
|
+
p_sl = sub.add_parser("service-life", help="accumulate twin-leg drift, export divergence curves")
|
|
78
|
+
_add_well_args(p_sl)
|
|
79
|
+
p_sl.add_argument("--years", type=float, default=5.0)
|
|
80
|
+
p_sl.add_argument("--recal", type=float, default=None, help="recalibration interval, years")
|
|
81
|
+
p_sl.add_argument("--seed", type=int, default=None)
|
|
82
|
+
p_sl.add_argument("--out", type=str, default="service_life.csv")
|
|
83
|
+
|
|
84
|
+
p_tm = sub.add_parser("telemetry", help="field-telemetry acquisition, export historian CSV")
|
|
85
|
+
_add_well_args(p_tm)
|
|
86
|
+
p_tm.add_argument("--hours", type=float, default=24.0)
|
|
87
|
+
p_tm.add_argument("--seed", type=int, default=None)
|
|
88
|
+
p_tm.add_argument("--out", type=str, default="telemetry.csv")
|
|
89
|
+
|
|
90
|
+
p_in = sub.add_parser("ingest", help="ingest a live-stream file through a port (read-only)")
|
|
91
|
+
p_in.add_argument("--file", type=str, required=True, help="source file (historian CSV or LAS)")
|
|
92
|
+
p_in.add_argument("--port", type=str, default="historian_csv",
|
|
93
|
+
help="port name from PORT_REGISTRY (historian_csv | las2 | site plug-ins)")
|
|
94
|
+
|
|
95
|
+
p_w = sub.add_parser("wells", help="list the MEASURED catalogue assemblies (--well targets)")
|
|
96
|
+
p_rep = sub.add_parser("report", help="generate the client survey report (Part 7)")
|
|
97
|
+
p_rep.add_argument("--out", default="survey_report", help="output directory")
|
|
98
|
+
|
|
99
|
+
sub.add_parser("operator", help="launch the operator GUI (requires PyQt6+matplotlib)")
|
|
100
|
+
|
|
101
|
+
sub.add_parser("accept", help="run the simulator ACCEPTANCE suite (product gate)")
|
|
102
|
+
|
|
103
|
+
p_b = sub.add_parser("bench", help="bench-test analysis per BENCH_TEST_PROTOCOL.md (or --selftest)")
|
|
104
|
+
p_b.add_argument("--program-csv", type=str, default=None, help="program-model-leg historian CSV (time_s + pressure column)")
|
|
105
|
+
p_b.add_argument("--conv-csv", type=str, default=None, help="conventional-leg historian CSV")
|
|
106
|
+
p_b.add_argument("--full-scale", type=float, default=30000.0)
|
|
107
|
+
p_b.add_argument("--selftest", action="store_true", help="SIMULATION_SELF_TEST: verify the analysis arithmetic on synthetic legs")
|
|
108
|
+
|
|
109
|
+
p_g = sub.add_parser("gamma", help="lithology-from-GR on a catalogue entry (measured API curves only)")
|
|
110
|
+
p_g.add_argument("--catalog", type=str, default=None, help="catalogue entry (omit to list gamma-bearing entries)")
|
|
111
|
+
p_g.add_argument("--channel", type=str, default=None)
|
|
112
|
+
p_g.add_argument("--cutoff", type=float, default=0.5)
|
|
113
|
+
p_g.add_argument("--gr-clean", type=float, default=None)
|
|
114
|
+
p_g.add_argument("--gr-shale", type=float, default=None)
|
|
115
|
+
|
|
116
|
+
p_rc = sub.add_parser("reconcile", help="two-stream reconciliation: live file vs closed-stream prediction")
|
|
117
|
+
_add_well_args(p_rc)
|
|
118
|
+
p_rc.add_argument("--file", type=str, default=None, help="live-stream file (historian CSV)")
|
|
119
|
+
p_rc.add_argument("--live-catalog", type=str, default=None,
|
|
120
|
+
help="catalogue production entry as the live leg (measured downhole P)")
|
|
121
|
+
p_rc.add_argument("--live-well", type=str, default=None, help="well tag inside the entry (e.g. 15/9-F-12)")
|
|
122
|
+
p_rc.add_argument("--station-md", type=float, default=None,
|
|
123
|
+
help="gauge MD ft for the catalogue live leg (caller-supplied; not in the archived excerpt)")
|
|
124
|
+
p_rc.add_argument("--port", type=str, default="historian_csv")
|
|
125
|
+
|
|
126
|
+
p_cr = sub.add_parser("client-report", help="client-facing report family in scope-of-work outline "
|
|
127
|
+
"(drift: Gauge Drift & Reconciliation Report)")
|
|
128
|
+
_add_well_args(p_cr)
|
|
129
|
+
p_cr.add_argument("--report", type=str, default="drift", choices=["drift", "accuracy"],
|
|
130
|
+
help="which report to generate (drift: Gauge Drift & Reconciliation; "
|
|
131
|
+
"accuracy: Accuracy Statement, MAPE at 90 pct CI)")
|
|
132
|
+
p_cr.add_argument("--ci", type=float, default=0.90, help="accuracy: confidence level")
|
|
133
|
+
p_cr.add_argument("--file", type=str, default=None, help="live-stream file (historian CSV)")
|
|
134
|
+
p_cr.add_argument("--live-catalog", type=str, default=None,
|
|
135
|
+
help="catalogue production entry as the live leg (measured downhole P)")
|
|
136
|
+
p_cr.add_argument("--live-well", type=str, default=None, help="well tag inside the entry (e.g. 15/9-F-12)")
|
|
137
|
+
p_cr.add_argument("--station-md", type=float, default=None,
|
|
138
|
+
help="gauge MD ft for the catalogue live leg (caller-supplied; not in the archived excerpt)")
|
|
139
|
+
p_cr.add_argument("--port", type=str, default="historian_csv")
|
|
140
|
+
p_cr.add_argument("--name", type=str, default=None, help="well name printed on the report")
|
|
141
|
+
p_cr.add_argument("--out", type=str, default="client_report", help="output directory")
|
|
142
|
+
|
|
143
|
+
p_dm = sub.add_parser("drift-monitor", help="drift evaluation as a scheduled job with an evaluation log, "
|
|
144
|
+
"staleness state and re-fit change log")
|
|
145
|
+
_add_well_args(p_dm)
|
|
146
|
+
p_dm.add_argument("--action", type=str, default="evaluate", choices=["evaluate", "status", "approve"])
|
|
147
|
+
p_dm.add_argument("--log-dir", type=str, default="drift_monitor", help="record directory (JSON lines + state)")
|
|
148
|
+
p_dm.add_argument("--now", type=str, default=None, help="clock for this run, ISO UTC (default: wall clock)")
|
|
149
|
+
p_dm.add_argument("--force", action="store_true", help="evaluate even if not due")
|
|
150
|
+
p_dm.add_argument("--file", type=str, default=None, help="live-stream file (historian CSV)")
|
|
151
|
+
p_dm.add_argument("--live-catalog", type=str, default=None)
|
|
152
|
+
p_dm.add_argument("--live-well", type=str, default=None)
|
|
153
|
+
p_dm.add_argument("--station-md", type=float, default=None)
|
|
154
|
+
p_dm.add_argument("--port", type=str, default="historian_csv")
|
|
155
|
+
p_dm.add_argument("--name", type=str, default=None, help="well name on the record")
|
|
156
|
+
p_dm.add_argument("--entry", type=str, default=None, help="approve: change-log entry id")
|
|
157
|
+
p_dm.add_argument("--approver", type=str, default=None, help="approve: approver name and role")
|
|
158
|
+
p_dm.add_argument("--decision", type=str, default="APPLIED", choices=["APPLIED", "REJECTED"])
|
|
159
|
+
p_dm.add_argument("--note", type=str, default="")
|
|
160
|
+
p_dm.add_argument("--report", type=str, default=None, help="evaluate: also write the drift report to this directory")
|
|
161
|
+
|
|
162
|
+
p_wt = sub.add_parser("well-test", help="well-test validation: stable-period detection with config-file "
|
|
163
|
+
"criteria, accept/reject reason codes, approval trail, report")
|
|
164
|
+
p_wt.add_argument("--live-catalog", type=str, default=None, help="catalogue production entry")
|
|
165
|
+
p_wt.add_argument("--live-well", type=str, default=None, help="well tag inside the entry (e.g. 15/9-F-12)")
|
|
166
|
+
p_wt.add_argument("--criteria", type=str, default=None, help="criteria JSON (client-agreed logic)")
|
|
167
|
+
p_wt.add_argument("--write-default-criteria", type=str, default=None, help="write the default criteria file here and exit")
|
|
168
|
+
p_wt.add_argument("--record-dir", type=str, default="well_tests", help="approval trail directory")
|
|
169
|
+
p_wt.add_argument("--approve", type=str, default=None, help="test id to approve/reject")
|
|
170
|
+
p_wt.add_argument("--approver", type=str, default=None)
|
|
171
|
+
p_wt.add_argument("--level", type=int, default=1)
|
|
172
|
+
p_wt.add_argument("--decision", type=str, default="APPROVED", choices=["APPROVED", "REJECTED", "CORRECTED"])
|
|
173
|
+
p_wt.add_argument("--note", type=str, default="")
|
|
174
|
+
p_wt.add_argument("--now", type=str, default=None, help="clock, ISO UTC")
|
|
175
|
+
p_wt.add_argument("--out", type=str, default=None, help="write the Well Test Validation Report to this directory")
|
|
176
|
+
|
|
177
|
+
p_al = sub.add_parser("alarms", help="alarm engine: definitions, state machine with deadband/on-delay, "
|
|
178
|
+
"event log, alarm-management KPIs, Alarm & Event report")
|
|
179
|
+
_add_well_args(p_al)
|
|
180
|
+
p_al.add_argument("--file", type=str, default=None, help="historian CSV to process")
|
|
181
|
+
p_al.add_argument("--port", type=str, default="historian_csv")
|
|
182
|
+
p_al.add_argument("--definitions", type=str, default=None, help="alarm definitions JSON (client settings)")
|
|
183
|
+
p_al.add_argument("--write-default-definitions", type=str, default=None,
|
|
184
|
+
help="write catalogue-derived defaults (over-range + quality) for --file here and exit")
|
|
185
|
+
p_al.add_argument("--event-log", type=str, default=None, help="append-only event log (JSON lines)")
|
|
186
|
+
p_al.add_argument("--positions", type=int, default=1, help="operator positions for the rate KPIs")
|
|
187
|
+
p_al.add_argument("--ack", type=str, default=None, help="acknowledge this alarm id after processing")
|
|
188
|
+
p_al.add_argument("--operator", type=str, default="")
|
|
189
|
+
p_al.add_argument("--now", type=str, default=None, help="clock for operator actions, ISO UTC")
|
|
190
|
+
p_al.add_argument("--name", type=str, default=None)
|
|
191
|
+
p_al.add_argument("--out", type=str, default=None, help="write the Alarm & Event report to this directory")
|
|
192
|
+
|
|
193
|
+
p_mc = sub.add_parser("model-cards", help="one model card per model, generated from the live objects")
|
|
194
|
+
p_mc.add_argument("--out", type=str, default="model_cards", help="output directory")
|
|
195
|
+
p_mc.add_argument("--monitor-log-dir", type=str, default=None, help="drift-monitor record directory for re-fit history")
|
|
196
|
+
|
|
197
|
+
p_sf = sub.add_parser("store-forward", help="store-and-forward simulation: outages, 72 h edge buffer, "
|
|
198
|
+
"chronological rate-controlled replay, gap report, Data Resilience report")
|
|
199
|
+
p_sf.add_argument("--file", type=str, required=True, help="historian CSV (the field samples)")
|
|
200
|
+
p_sf.add_argument("--port", type=str, default="historian_csv")
|
|
201
|
+
p_sf.add_argument("--outage", type=str, action="append", default=[], help="'START,END' ISO UTC; repeatable")
|
|
202
|
+
p_sf.add_argument("--capacity-hours", type=float, default=72.0)
|
|
203
|
+
p_sf.add_argument("--replay-rate", type=float, default=50.0, help="records per second on replay")
|
|
204
|
+
p_sf.add_argument("--edge-latency", type=float, default=2.0, help="seconds from sample to edge arrival")
|
|
205
|
+
p_sf.add_argument("--tags", type=str, default=None, help="comma-separated tag prefixes to include (default all)")
|
|
206
|
+
p_sf.add_argument("--name", type=str, default=None)
|
|
207
|
+
p_sf.add_argument("--out", type=str, default=None, help="write the Data Resilience report to this directory")
|
|
208
|
+
|
|
209
|
+
p_cfg = sub.add_parser("config", help="version-controlled configuration: commit, history, export, rollback")
|
|
210
|
+
p_cfg.add_argument("--store", type=str, default="config_store")
|
|
211
|
+
p_cfg.add_argument("--action", type=str, default="summary", choices=["commit", "history", "export", "rollback", "summary"])
|
|
212
|
+
p_cfg.add_argument("--name", type=str, default=None, help="configuration name (e.g. well_test_criteria)")
|
|
213
|
+
p_cfg.add_argument("--file", type=str, default=None, help="commit: JSON file to commit; export: destination path")
|
|
214
|
+
p_cfg.add_argument("--author", type=str, default=None)
|
|
215
|
+
p_cfg.add_argument("--note", type=str, default="")
|
|
216
|
+
p_cfg.add_argument("--version", type=int, default=None, help="export/rollback: version number")
|
|
217
|
+
p_cfg.add_argument("--now", type=str, default=None)
|
|
218
|
+
|
|
219
|
+
p_sb = sub.add_parser("sbom", help="software bill of materials from the running environment (eight fields)")
|
|
220
|
+
p_sb.add_argument("--out", type=str, default="sbom")
|
|
221
|
+
|
|
222
|
+
p_sla = sub.add_parser("sla-report", help="monthly SLA measurement from the program's records")
|
|
223
|
+
p_sla.add_argument("--month", type=str, required=True, help="YYYY-MM")
|
|
224
|
+
p_sla.add_argument("--monitor-log-dir", type=str, default=None)
|
|
225
|
+
p_sla.add_argument("--store-forward-json", type=str, default=None, help="Data Resilience machine JSON")
|
|
226
|
+
p_sla.add_argument("--alarm-log", type=str, default=None)
|
|
227
|
+
p_sla.add_argument("--well-test-dir", type=str, default=None)
|
|
228
|
+
p_sla.add_argument("--accuracy-json", type=str, default=None)
|
|
229
|
+
p_sla.add_argument("--config-store", type=str, default=None)
|
|
230
|
+
p_sla.add_argument("--name", type=str, default=None)
|
|
231
|
+
p_sla.add_argument("--out", type=str, default="sla_report")
|
|
232
|
+
|
|
233
|
+
p_fat = sub.add_parser("fat-sat", help="render the acceptance suite as a FAT or SAT protocol with signature block")
|
|
234
|
+
p_fat.add_argument("--kind", type=str, default="FAT", choices=["FAT", "SAT"])
|
|
235
|
+
p_fat.add_argument("--sections", type=str, default=None, help="comma-separated section keys (default: client set)")
|
|
236
|
+
p_fat.add_argument("--name", type=str, default=None)
|
|
237
|
+
p_fat.add_argument("--out", type=str, default="fat_sat")
|
|
238
|
+
|
|
239
|
+
p_db = sub.add_parser("dashboard", help="run the client report family for the given wells and build the web view (index.html)")
|
|
240
|
+
p_db.add_argument("--catalog-well", type=str, action="append", default=[],
|
|
241
|
+
help="'ENTRY:WELL_TAG:STATION_MD_FT' (repeatable), e.g. volve_f12_f14_production_excerpt:15/9-F-12:10000")
|
|
242
|
+
p_db.add_argument("--file", type=str, action="append", default=[], help="historian CSV as a well (repeatable)")
|
|
243
|
+
p_db.add_argument("--td", type=float, default=None, help="total depth (MD), ft, for the well model")
|
|
244
|
+
p_db.add_argument("--spec", type=str, default=None, help="gauge datasheet preset or JSON")
|
|
245
|
+
p_db.add_argument("--outage", type=str, action="append", default=[], help="'START,END' for the resilience simulation on file wells")
|
|
246
|
+
p_db.add_argument("--month", type=str, default=None, help="YYYY-MM for the SLA report")
|
|
247
|
+
p_db.add_argument("--criteria", type=str, default=None, help="well-test criteria JSON")
|
|
248
|
+
p_db.add_argument("--monitor-root", type=str, default=None, help="drift-monitor record root (one dir per well)")
|
|
249
|
+
p_db.add_argument("--sat", action="store_true", help="also run and include the SAT protocol")
|
|
250
|
+
p_db.add_argument("--name", type=str, default="site")
|
|
251
|
+
p_db.add_argument("--out", type=str, default="dashboard")
|
|
252
|
+
|
|
253
|
+
p_cs = sub.add_parser("case-study", help="depth sweep, write the one-page markdown case")
|
|
254
|
+
_add_well_args(p_cs)
|
|
255
|
+
p_cs.add_argument("--points", type=int, default=12)
|
|
256
|
+
p_cs.add_argument("--name", type=str, default="case-study well")
|
|
257
|
+
p_cs.add_argument("--horizon", type=float, default=5.0)
|
|
258
|
+
p_cs.add_argument("--out", type=str, default="downhole_case_study.md")
|
|
259
|
+
|
|
260
|
+
a = ap.parse_args(argv)
|
|
261
|
+
|
|
262
|
+
from . import (DownholeEngine, ServiceLifeConfig, ServiceLifeSimulator,
|
|
263
|
+
TelemetryConfig, TelemetryRecorder, CaseStudyConfig,
|
|
264
|
+
case_study, write_markdown, GAUGE_SPECS, load_gauge_spec_json,
|
|
265
|
+
load_well_profile_csv, DEFAULT_TD_FT)
|
|
266
|
+
|
|
267
|
+
if a.cmd == "run":
|
|
268
|
+
eng = DownholeEngine(_build_config(a))
|
|
269
|
+
for _ in range(a.steps):
|
|
270
|
+
eng.step()
|
|
271
|
+
out = eng.export_csv(a.out)
|
|
272
|
+
s = eng.summary()
|
|
273
|
+
print(f"run: {s['sensors']} gauges x {a.steps} steps -> {out} "
|
|
274
|
+
f"(avg drift {s['avg_drift_pct']} %FS/yr, suppression {s['canonical_suppression_at_unity_trims']})")
|
|
275
|
+
elif a.cmd == "service-life":
|
|
276
|
+
eng = DownholeEngine(_build_config(a))
|
|
277
|
+
cfg = ServiceLifeConfig(years=a.years, recalibration_interval_years=a.recal, seed=a.seed)
|
|
278
|
+
sim = ServiceLifeSimulator(engine=eng, config=cfg).run()
|
|
279
|
+
out = sim.export_csv(a.out)
|
|
280
|
+
d = sim.divergence_summary()
|
|
281
|
+
print(f"service-life: {a.years:g} yr -> {out} "
|
|
282
|
+
f"(sep {d['predicted_separation_rate_psi_yr']} psi/yr, ratio pred {d['predicted_ratio_suppression']})")
|
|
283
|
+
elif a.cmd == "telemetry":
|
|
284
|
+
eng = DownholeEngine(_build_config(a))
|
|
285
|
+
rec = TelemetryRecorder(engine=eng, config=TelemetryConfig(duration_hours=a.hours, seed=a.seed)).run()
|
|
286
|
+
out = rec.export_csv(a.out)
|
|
287
|
+
s = rec.telemetry_summary()
|
|
288
|
+
print(f"telemetry: {s['samples']} samples -> {out} "
|
|
289
|
+
f"(uptime {s['uptime_pct']}%, stuck P/R {s['stuck_score']['precision']}/{s['stuck_score']['recall']})")
|
|
290
|
+
elif a.cmd == "ingest":
|
|
291
|
+
from . import ingest as _ingest
|
|
292
|
+
st = _ingest(a.file, port=a.port)
|
|
293
|
+
import json as _json
|
|
294
|
+
print(_json.dumps(st.summary(), indent=1))
|
|
295
|
+
elif a.cmd == "bench":
|
|
296
|
+
import json as _json
|
|
297
|
+
from .bench import bench_analysis, bench_selftest
|
|
298
|
+
if a.selftest:
|
|
299
|
+
print(_json.dumps(bench_selftest(), indent=1))
|
|
300
|
+
elif a.csv and a.conv_csv:
|
|
301
|
+
from . import ingest as _ingest
|
|
302
|
+
import numpy as _np
|
|
303
|
+
def _leg(path):
|
|
304
|
+
st = _ingest(path, port="historian_csv")
|
|
305
|
+
pc = [c for c in st.channels if "P" in c.upper()]
|
|
306
|
+
if not pc:
|
|
307
|
+
raise SystemExit(f"{path}: no pressure channel found")
|
|
308
|
+
return st.index, st.channels[pc[0]].values
|
|
309
|
+
tu, pu = _leg(a.csv)
|
|
310
|
+
tc, pc_ = _leg(a.conv_csv)
|
|
311
|
+
print(_json.dumps(bench_analysis(tu, pu, tc, pc_, full_scale_psi=a.full_scale), indent=1))
|
|
312
|
+
else:
|
|
313
|
+
raise SystemExit("bench needs --program-csv AND --conv-csv, or --selftest")
|
|
314
|
+
elif a.cmd == "gamma":
|
|
315
|
+
import json as _json
|
|
316
|
+
from .gamma import gamma_entries, gamma_report
|
|
317
|
+
if not a.catalog:
|
|
318
|
+
for n, chans in sorted(gamma_entries().items()):
|
|
319
|
+
print(f"{n}: {', '.join(chans)}")
|
|
320
|
+
return 0
|
|
321
|
+
print(_json.dumps(gamma_report(a.catalog, channel=a.channel, cutoff=a.cutoff,
|
|
322
|
+
gr_clean=a.gr_clean, gr_shale=a.gr_shale), indent=1))
|
|
323
|
+
elif a.cmd == "accept":
|
|
324
|
+
from .acceptance_tests import main as _accept
|
|
325
|
+
return _accept()
|
|
326
|
+
elif a.cmd == "operator":
|
|
327
|
+
from .operator_app import launch_operator_app
|
|
328
|
+
return launch_operator_app()
|
|
329
|
+
elif a.cmd == "report":
|
|
330
|
+
from .project import generate_report
|
|
331
|
+
r = generate_report(a.out)
|
|
332
|
+
print("report:", r["report_path"])
|
|
333
|
+
for k, v in r["renders"].items():
|
|
334
|
+
print(" %s: %s" % (k, v))
|
|
335
|
+
elif a.cmd == "wells":
|
|
336
|
+
from . import BUILTIN_ASSEMBLIES
|
|
337
|
+
for name, maker in sorted(BUILTIN_ASSEMBLIES.items()):
|
|
338
|
+
try:
|
|
339
|
+
asm = maker()
|
|
340
|
+
except Exception:
|
|
341
|
+
# operator-tier assemblies on machines without the private
|
|
342
|
+
# data: listed honestly, never crashing the listing (v1.77.0)
|
|
343
|
+
print(f"{name}: OPERATOR-TIER assembly - private data not "
|
|
344
|
+
"present on this machine (tier is per-machine, never required)")
|
|
345
|
+
continue
|
|
346
|
+
roles = {r: f"{c.entry}/{c.channel} {c.coverage()[0]:.0f}-{c.coverage()[1]:.0f} m"
|
|
347
|
+
for r, c in asm.components.items()}
|
|
348
|
+
bridge = "engine-ready" if "temperature" in asm.components else \
|
|
349
|
+
"no measured T (bridge refuses; lookups still live)"
|
|
350
|
+
print(f"{name}: {asm.site}")
|
|
351
|
+
for r, d in roles.items():
|
|
352
|
+
print(f" {r:12s} {d}")
|
|
353
|
+
if asm.attachments:
|
|
354
|
+
print(f" attachments: {', '.join(sorted(asm.attachments))}")
|
|
355
|
+
print(f" [{bridge}]")
|
|
356
|
+
elif a.cmd == "reconcile":
|
|
357
|
+
from . import ingest as _ingest, Reconciler
|
|
358
|
+
station_map = None
|
|
359
|
+
if a.live_catalog:
|
|
360
|
+
from . import production_live_stream
|
|
361
|
+
if not a.live_well or a.station_md is None:
|
|
362
|
+
raise SystemExit("--live-catalog needs --live-well and --station-md "
|
|
363
|
+
"(the archived excerpt does not state the gauge depth; "
|
|
364
|
+
"the CLI will not invent one)")
|
|
365
|
+
stream, station_map = production_live_stream(a.live_catalog, a.live_well, a.station_md)
|
|
366
|
+
print(f"[live] {stream.name} | {len(stream.index)} samples | "
|
|
367
|
+
f"NaN days dropped: {stream.meta.get('nan_days_dropped')}")
|
|
368
|
+
elif a.file:
|
|
369
|
+
stream = _ingest(a.file, port=a.port)
|
|
370
|
+
else:
|
|
371
|
+
raise SystemExit("reconcile needs --file OR --live-catalog")
|
|
372
|
+
rep = Reconciler(_build_config(a)).reconcile(stream, station_map=station_map)
|
|
373
|
+
import json as _json
|
|
374
|
+
print(_json.dumps(rep, indent=1))
|
|
375
|
+
elif a.cmd == "dashboard":
|
|
376
|
+
from . import GAUGE_SPECS, load_gauge_spec_json, DEFAULT_TD_FT
|
|
377
|
+
from .dashboard import orchestrate
|
|
378
|
+
cws = []
|
|
379
|
+
for spec in a.catalog_well:
|
|
380
|
+
parts = spec.rsplit(":", 2)
|
|
381
|
+
if len(parts) != 3:
|
|
382
|
+
raise SystemExit(f"--catalog-well needs ENTRY:WELL_TAG:STATION_MD_FT, got {spec!r}")
|
|
383
|
+
cws.append({"entry": parts[0], "well": parts[1], "md_ft": float(parts[2])})
|
|
384
|
+
fws = [{"path": f} for f in a.file]
|
|
385
|
+
gs = None
|
|
386
|
+
if a.spec:
|
|
387
|
+
gs = GAUGE_SPECS[a.spec] if a.spec in GAUGE_SPECS else load_gauge_spec_json(a.spec)
|
|
388
|
+
r = orchestrate(a.out, cws, fws, td_ft=a.td if a.td is not None else DEFAULT_TD_FT, gauge_spec=gs,
|
|
389
|
+
outages=a.outage or None, month=a.month, site_name=a.name, criteria_path=a.criteria,
|
|
390
|
+
monitor_root=a.monitor_root, run_sat=a.sat)
|
|
391
|
+
print(f"dashboard: {r['index']}")
|
|
392
|
+
for w, reps in r["reports"].items():
|
|
393
|
+
print(f" {w}: {', '.join(reps)}")
|
|
394
|
+
print(f" site: {', '.join(r['site_reports'])}")
|
|
395
|
+
elif a.cmd == "config":
|
|
396
|
+
import json as _json
|
|
397
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
398
|
+
from .config_versioning import ConfigStore
|
|
399
|
+
cs = ConfigStore(a.store)
|
|
400
|
+
now = _dt.fromisoformat(a.now.replace("Z", "")).replace(tzinfo=_tz.utc) if a.now else None
|
|
401
|
+
if a.action == "commit":
|
|
402
|
+
if not (a.name and a.file and a.author):
|
|
403
|
+
raise SystemExit("config commit needs --name --file --author")
|
|
404
|
+
print(_json.dumps(cs.import_file(a.name, a.file, a.author, a.note, now=now), indent=1, default=str))
|
|
405
|
+
elif a.action == "history":
|
|
406
|
+
print(_json.dumps(cs.history(a.name), indent=1, default=str))
|
|
407
|
+
elif a.action == "export":
|
|
408
|
+
print("exported:", cs.export(a.name, a.file, a.version))
|
|
409
|
+
elif a.action == "rollback":
|
|
410
|
+
if not (a.name and a.version and a.author):
|
|
411
|
+
raise SystemExit("config rollback needs --name --version --author")
|
|
412
|
+
print(_json.dumps(cs.rollback(a.name, a.version, a.author, a.note, now=now), indent=1, default=str))
|
|
413
|
+
else:
|
|
414
|
+
print(_json.dumps(cs.summary(), indent=1))
|
|
415
|
+
elif a.cmd == "sbom":
|
|
416
|
+
from .sbom import generate, write as _write_sbom
|
|
417
|
+
sb = generate()
|
|
418
|
+
paths = _write_sbom(sb, a.out)
|
|
419
|
+
print(f"sbom: {sb['n_components']} components")
|
|
420
|
+
for c in sb["components"]:
|
|
421
|
+
print(f" {c['name']} {c['version']} | {c['licence']} | {c['relationship']}")
|
|
422
|
+
for kk, vv in paths.items():
|
|
423
|
+
print(f" {kk}: {vv}")
|
|
424
|
+
elif a.cmd == "sla-report":
|
|
425
|
+
from . import __version__ as _v
|
|
426
|
+
from .sla_report import measure
|
|
427
|
+
from .sbom import generate as _sbom
|
|
428
|
+
from .client_reports import monthly_sla_report, write as _write_report
|
|
429
|
+
m = measure(a.month, monitor_log_dir=a.monitor_log_dir, store_forward_json=a.store_forward_json, alarm_log=a.alarm_log,
|
|
430
|
+
well_test_dir=a.well_test_dir, accuracy_json=a.accuracy_json, config_dir=a.config_store)
|
|
431
|
+
csum = None
|
|
432
|
+
if a.config_store:
|
|
433
|
+
from .config_versioning import ConfigStore
|
|
434
|
+
csum = ConfigStore(a.config_store).summary()
|
|
435
|
+
doc = monthly_sla_report(m, site_name=a.name or "site", program_version=_v, config_summary=csum, sbom=_sbom())
|
|
436
|
+
paths = _write_report(doc, a.out, basename=f"sla_report_{a.month}")
|
|
437
|
+
print(f"sla-report {a.month}: " + ", ".join(f"{k} {v}" for k, v in sorted(m["status_counts"].items())))
|
|
438
|
+
for kk, vv in paths.items():
|
|
439
|
+
print(f" {kk}: {vv}")
|
|
440
|
+
elif a.cmd == "fat-sat":
|
|
441
|
+
from . import __version__ as _v
|
|
442
|
+
from .fat_sat import run_protocol
|
|
443
|
+
from .client_reports import fat_sat_report, write as _write_report
|
|
444
|
+
proto = run_protocol(a.kind, sections=[x.strip() for x in a.sections.split(",")] if a.sections else None)
|
|
445
|
+
doc = fat_sat_report(proto, site_name=a.name or "", program_version=_v)
|
|
446
|
+
paths = _write_report(doc, a.out, basename=f"{a.kind.lower()}_protocol")
|
|
447
|
+
print(f"{a.kind}: {proto['n_steps']} steps, {proto['n_pass']} pass, {proto['n_fail']} fail, "
|
|
448
|
+
f"{proto['internal_checks_excluded']} internal checks excluded - {proto['result']}")
|
|
449
|
+
for kk, vv in paths.items():
|
|
450
|
+
print(f" {kk}: {vv}")
|
|
451
|
+
elif a.cmd == "store-forward":
|
|
452
|
+
import json as _json
|
|
453
|
+
from . import ingest as _ingest, __version__ as _v
|
|
454
|
+
from .sample_record import TagCatalogue, records_from_stream
|
|
455
|
+
from .store_forward import simulate, parse_outages, BufferConfig
|
|
456
|
+
stream = _ingest(a.file, port=a.port)
|
|
457
|
+
cat = TagCatalogue.from_stream(stream)
|
|
458
|
+
recs = records_from_stream(stream, cat)
|
|
459
|
+
if a.tags:
|
|
460
|
+
pref = tuple(x.strip() for x in a.tags.split(","))
|
|
461
|
+
recs = [r for r in recs if r.tag_id.startswith(pref)]
|
|
462
|
+
cad = next((t.cadence_s for t in cat.tags.values() if t.cadence_s), 60.0)
|
|
463
|
+
sim = simulate(recs, parse_outages(a.outage), BufferConfig(capacity_hours=a.capacity_hours, cadence_s=cad,
|
|
464
|
+
replay_rate_per_s=a.replay_rate), edge_latency_s=a.edge_latency)
|
|
465
|
+
print(_json.dumps({k: v for k, v in sim.items() if k in ("stats", "outages", "final_backlog", "replay_slots")}, indent=1))
|
|
466
|
+
if a.out:
|
|
467
|
+
from .client_reports import data_resilience_report, write as _write_report
|
|
468
|
+
doc = data_resilience_report(sim, site_name=a.name or stream.name, program_version=_v)
|
|
469
|
+
paths = _write_report(doc, a.out, basename="data_resilience_report")
|
|
470
|
+
for kk, vv in paths.items():
|
|
471
|
+
print(f" {kk}: {vv}")
|
|
472
|
+
elif a.cmd == "model-cards":
|
|
473
|
+
import json as _json, os as _os
|
|
474
|
+
from .model_card import build_cards, card_index
|
|
475
|
+
from .client_reports import model_card_report, write as _write_report
|
|
476
|
+
cards = build_cards(monitor_log_dir=a.monitor_log_dir)
|
|
477
|
+
_os.makedirs(a.out, exist_ok=True)
|
|
478
|
+
for c in cards:
|
|
479
|
+
paths = _write_report(model_card_report(c), a.out, basename=f"model_card_{c.model_id}")
|
|
480
|
+
print(f"model-card {c.model_id}: {paths['html']}")
|
|
481
|
+
with open(_os.path.join(a.out, "model_cards_index.json"), "w", encoding="utf-8") as f:
|
|
482
|
+
_json.dump(card_index(cards), f, indent=1)
|
|
483
|
+
print(f" index: {_os.path.join(a.out, 'model_cards_index.json')} ({len(cards)} cards)")
|
|
484
|
+
elif a.cmd == "alarms":
|
|
485
|
+
import json as _json
|
|
486
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
487
|
+
from . import ingest as _ingest, __version__ as _v
|
|
488
|
+
from .sample_record import TagCatalogue, records_from_stream
|
|
489
|
+
from .alarm_engine import (AlarmEngine, defaults_from_catalogue, load_alarm_definitions,
|
|
490
|
+
write_alarm_definitions)
|
|
491
|
+
if not a.file:
|
|
492
|
+
raise SystemExit("alarms needs --file")
|
|
493
|
+
cfg = _build_config(a)
|
|
494
|
+
stream = _ingest(a.file, port=a.port)
|
|
495
|
+
cat = TagCatalogue.from_stream(stream, gauge_spec=getattr(cfg, "gauge_spec", None))
|
|
496
|
+
if a.write_default_definitions:
|
|
497
|
+
print("definitions:", write_alarm_definitions(defaults_from_catalogue(cat), a.write_default_definitions))
|
|
498
|
+
return 0
|
|
499
|
+
defs = load_alarm_definitions(a.definitions) if a.definitions else defaults_from_catalogue(cat)
|
|
500
|
+
eng = AlarmEngine(defs, event_log_path=a.event_log, operator_positions=a.positions)
|
|
501
|
+
evs = eng.process(records_from_stream(stream, cat))
|
|
502
|
+
if a.ack:
|
|
503
|
+
now = _dt.fromisoformat(a.now.replace("Z", "")).replace(tzinfo=_tz.utc) if a.now else None
|
|
504
|
+
if now is None:
|
|
505
|
+
raise SystemExit("--ack needs --now (the acknowledgement timestamp)")
|
|
506
|
+
print(_json.dumps(eng.acknowledge(a.ack, a.operator, now), indent=1))
|
|
507
|
+
k = eng.kpis()
|
|
508
|
+
print(f"alarms: {len(defs)} definitions, {len(evs)} events this run, "
|
|
509
|
+
f"{k.get('n_activations', 0)} activations, {len(eng.active())} active at end")
|
|
510
|
+
if a.out:
|
|
511
|
+
from .client_reports import alarm_event_report, write as _write_report
|
|
512
|
+
doc = alarm_event_report(eng, well_name=a.name or stream.name, program_version=_v)
|
|
513
|
+
paths = _write_report(doc, a.out, basename="alarm_event_report")
|
|
514
|
+
for kk, vv in paths.items():
|
|
515
|
+
print(f" {kk}: {vv}")
|
|
516
|
+
elif a.cmd == "well-test":
|
|
517
|
+
import json as _json
|
|
518
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
519
|
+
from . import CATALOG, __version__ as _v
|
|
520
|
+
from .well_test_validation import (WellTestValidator, ApprovalTrail, load_criteria,
|
|
521
|
+
write_default_criteria, volve_channel_map)
|
|
522
|
+
if a.write_default_criteria:
|
|
523
|
+
print("criteria:", write_default_criteria(a.write_default_criteria))
|
|
524
|
+
return 0
|
|
525
|
+
now = None
|
|
526
|
+
if a.now:
|
|
527
|
+
now = _dt.fromisoformat(a.now.replace("Z", ""))
|
|
528
|
+
now = now.replace(tzinfo=_tz.utc) if now.tzinfo is None else now
|
|
529
|
+
crit = load_criteria(a.criteria)
|
|
530
|
+
trail = ApprovalTrail(a.record_dir, levels=crit.get("approval_levels"))
|
|
531
|
+
if a.approve:
|
|
532
|
+
if not a.approver:
|
|
533
|
+
raise SystemExit("--approve needs --approver")
|
|
534
|
+
print(_json.dumps(trail.approve(a.approve, a.approver, a.level, a.decision, a.note, now=now), indent=1))
|
|
535
|
+
print(_json.dumps(trail.status_of(a.approve), indent=1))
|
|
536
|
+
if not (a.live_catalog and a.live_well):
|
|
537
|
+
if a.approve:
|
|
538
|
+
return 0
|
|
539
|
+
raise SystemExit("well-test needs --live-catalog and --live-well (or --approve)")
|
|
540
|
+
src = CATALOG[a.live_catalog].stream()
|
|
541
|
+
det = WellTestValidator(crit, volve_channel_map(a.live_well)).detect(src)
|
|
542
|
+
print(f"well-test: {det['n_accepted']} accepted, {det['n_rejected']} rejected candidates, "
|
|
543
|
+
f"{det['eligible_samples']}/{det['n_samples']} eligible samples")
|
|
544
|
+
for t in det["tests"]:
|
|
545
|
+
print(f" {t['test_id']} {t['start_utc'][:10]}..{t['end_utc'][:10]} n={t['n']} "
|
|
546
|
+
+ " ".join(f"{k}={v:,.1f}" for k, v in t["virtual_rates"].items())
|
|
547
|
+
+ f" [{trail.status_of(t['test_id'])['status']}]")
|
|
548
|
+
if a.out:
|
|
549
|
+
from .client_reports import well_test_report, write as _write_report
|
|
550
|
+
doc = well_test_report(det, approvals=trail, well_name=f"{a.live_catalog} {a.live_well}",
|
|
551
|
+
program_version=_v, evaluated_at=now)
|
|
552
|
+
paths = _write_report(doc, a.out, basename="well_test_validation")
|
|
553
|
+
for k, v in paths.items():
|
|
554
|
+
print(f" {k}: {v}")
|
|
555
|
+
elif a.cmd == "drift-monitor":
|
|
556
|
+
import json as _json
|
|
557
|
+
from datetime import datetime as _dt, timezone as _tz
|
|
558
|
+
from . import ingest as _ingest, __version__ as _v
|
|
559
|
+
from .drift_monitor import DriftMonitor
|
|
560
|
+
now = None
|
|
561
|
+
if a.now:
|
|
562
|
+
now = _dt.fromisoformat(a.now.replace("Z", ""))
|
|
563
|
+
now = now.replace(tzinfo=_tz.utc) if now.tzinfo is None else now
|
|
564
|
+
cfg = _build_config(a)
|
|
565
|
+
mon = DriftMonitor(cfg, a.log_dir, well_name=a.name or a.live_well or (a.file or "well"))
|
|
566
|
+
if a.action == "status":
|
|
567
|
+
print(_json.dumps(mon.status(now), indent=1))
|
|
568
|
+
elif a.action == "approve":
|
|
569
|
+
if not a.entry or not a.approver:
|
|
570
|
+
raise SystemExit("approve needs --entry and --approver")
|
|
571
|
+
print(_json.dumps(mon.approve(a.entry, a.approver, now=now, decision=a.decision, note=a.note), indent=1))
|
|
572
|
+
else:
|
|
573
|
+
station_map = None
|
|
574
|
+
if a.live_catalog:
|
|
575
|
+
from . import production_live_stream
|
|
576
|
+
if not a.live_well or a.station_md is None:
|
|
577
|
+
raise SystemExit("drift-monitor --live-catalog needs --live-well and --station-md")
|
|
578
|
+
stream, station_map = production_live_stream(a.live_catalog, a.live_well, a.station_md)
|
|
579
|
+
elif a.file:
|
|
580
|
+
stream = _ingest(a.file, port=a.port)
|
|
581
|
+
else:
|
|
582
|
+
raise SystemExit("drift-monitor evaluate needs --file OR --live-catalog")
|
|
583
|
+
r = mon.run_scheduled(stream, station_map=station_map, now=now, force=a.force)
|
|
584
|
+
out = {k: v for k, v in r.items() if k != "evaluation"}
|
|
585
|
+
if r["action"] == "EVALUATED":
|
|
586
|
+
out["classification_counts"] = r["evaluation"]["classification_counts"]
|
|
587
|
+
print(_json.dumps(out, indent=1))
|
|
588
|
+
if a.report and r["action"] == "EVALUATED":
|
|
589
|
+
from .client_reports import gauge_drift_report, write as _write_report
|
|
590
|
+
stt = mon.status(now)
|
|
591
|
+
stt["history"] = mon.history()
|
|
592
|
+
stt["change_log"] = mon.change_log()
|
|
593
|
+
doc = gauge_drift_report(r["evaluation"], mon.corrected_stream(stream), well_name=mon.well_name,
|
|
594
|
+
program_version=_v, gauge_spec=getattr(cfg, "gauge_spec", None),
|
|
595
|
+
monitor_status=stt, evaluated_at=now)
|
|
596
|
+
paths = _write_report(doc, a.report)
|
|
597
|
+
print(" report:", paths["html"])
|
|
598
|
+
elif a.cmd == "client-report" and a.report == "accuracy":
|
|
599
|
+
from . import __version__ as _v
|
|
600
|
+
from .accuracy_statement import library_backtest
|
|
601
|
+
from .client_reports import accuracy_statement_report, write as _write_report
|
|
602
|
+
bt = library_backtest(ci=a.ci)
|
|
603
|
+
doc = accuracy_statement_report(bt, program_version=_v)
|
|
604
|
+
paths = _write_report(doc, a.out, basename="accuracy_statement")
|
|
605
|
+
print(f"client-report (accuracy): {doc.report_id} - {bt['n_ok']} scored, "
|
|
606
|
+
f"{bt['bands'].get('MEETS_TARGET', 0)} meet target, {bt['n_pending']} pending")
|
|
607
|
+
for k, v in paths.items():
|
|
608
|
+
print(f" {k}: {v}")
|
|
609
|
+
elif a.cmd == "client-report":
|
|
610
|
+
from . import ingest as _ingest, Reconciler, __version__ as _v
|
|
611
|
+
from .client_reports import gauge_drift_report, write as _write_report
|
|
612
|
+
station_map = None
|
|
613
|
+
well_name = a.name
|
|
614
|
+
if a.live_catalog:
|
|
615
|
+
from . import production_live_stream
|
|
616
|
+
if not a.live_well or a.station_md is None:
|
|
617
|
+
raise SystemExit("client-report --live-catalog needs --live-well and --station-md "
|
|
618
|
+
"(the archived excerpt does not state the gauge depth; "
|
|
619
|
+
"the CLI will not invent one)")
|
|
620
|
+
stream, station_map = production_live_stream(a.live_catalog, a.live_well, a.station_md)
|
|
621
|
+
well_name = well_name or f"{a.live_catalog} {a.live_well}"
|
|
622
|
+
elif a.file:
|
|
623
|
+
stream = _ingest(a.file, port=a.port)
|
|
624
|
+
well_name = well_name or stream.name
|
|
625
|
+
else:
|
|
626
|
+
raise SystemExit("client-report needs --file OR --live-catalog")
|
|
627
|
+
cfg = _build_config(a)
|
|
628
|
+
ev = Reconciler(cfg).reconcile(stream, station_map=station_map)
|
|
629
|
+
doc = gauge_drift_report(ev, stream, well_name=well_name, program_version=_v,
|
|
630
|
+
gauge_spec=getattr(cfg, 'gauge_spec', None))
|
|
631
|
+
paths = _write_report(doc, a.out)
|
|
632
|
+
print(f"client-report ({a.report}): {doc.report_id} - "
|
|
633
|
+
f"{'MODEL DRIFT DETECTED' if doc.data['drift_detected'] else 'NO MODEL DRIFT DETECTED'}")
|
|
634
|
+
for k, v in paths.items():
|
|
635
|
+
print(f" {k}: {v}")
|
|
636
|
+
elif a.cmd == "case-study":
|
|
637
|
+
from .deviation import DeviationSurvey
|
|
638
|
+
if getattr(a, "well", None):
|
|
639
|
+
from . import demo_config
|
|
640
|
+
_cfg = demo_config(a.well)
|
|
641
|
+
td = _cfg.td_ft
|
|
642
|
+
kw = {"td_ft": td, "n_depth_points": a.points,
|
|
643
|
+
"well_name": a.well, "horizon_years": a.horizon,
|
|
644
|
+
"profile": _cfg.profile}
|
|
645
|
+
else:
|
|
646
|
+
td = a.td if a.td is not None else DEFAULT_TD_FT
|
|
647
|
+
kw = {"td_ft": td, "n_depth_points": a.points, "well_name": a.name, "horizon_years": a.horizon}
|
|
648
|
+
if a.profile and "profile" not in kw:
|
|
649
|
+
kw["profile"] = load_well_profile_csv(a.profile)
|
|
650
|
+
if a.spec:
|
|
651
|
+
kw["gauge_spec"] = (GAUGE_SPECS[a.spec] if a.spec in GAUGE_SPECS
|
|
652
|
+
else load_gauge_spec_json(a.spec))
|
|
653
|
+
if a.kickoff is not None and a.inclination is not None:
|
|
654
|
+
kw["deviation"] = DeviationSurvey.from_kickoff(a.kickoff, a.inclination, td)
|
|
655
|
+
out = write_markdown(case_study(CaseStudyConfig(**kw)), a.out)
|
|
656
|
+
print(f"case-study: -> {out}")
|
|
657
|
+
return 0
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
if __name__ == "__main__":
|
|
661
|
+
sys.exit(main())
|