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/__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())