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/ports.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
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
|
+
"""ports — the live-stream ports/plug-in layer (v1.8.0 extension).
|
|
5
|
+
|
|
6
|
+
Piece 2 of the two-stream build (Daniel's architecture, PAPER_2256 appendix 6):
|
|
7
|
+
READ-ONLY taps that ingest live-stream data from site logging systems into a
|
|
8
|
+
single normalized form — `LiveStream` — that the reconciler (piece 3) will
|
|
9
|
+
coordinate against the closed stream's predictions.
|
|
10
|
+
|
|
11
|
+
Design rules:
|
|
12
|
+
* READ-ONLY BY DESIGN. Ports ingest; they never write to, poll-configure,
|
|
13
|
+
or control a site system. File readers open files for reading only, and
|
|
14
|
+
the declared network taps are specified read-only.
|
|
15
|
+
* PLUG-IN REGISTRY. Every port is an entry in `PORT_REGISTRY` with an
|
|
16
|
+
explicit status: IMPLEMENTED entries parse today; DECLARED entries name
|
|
17
|
+
the protocol (from the tool library's telemetry_interface declarations)
|
|
18
|
+
but REFUSE to run until real site details exist — the same
|
|
19
|
+
no-invented-behavior pattern as the tool library's user-supplied specs.
|
|
20
|
+
`register_port()` lets a site plug in its own reader without touching
|
|
21
|
+
this module.
|
|
22
|
+
* ROUND-TRIP VERIFIED. The v1.3.0 telemetry layer exports field-historian
|
|
23
|
+
CSVs; the historian port re-ingests them bit-consistently (MISSING ->
|
|
24
|
+
NaN, flags carried). Closed stream -> simulated live file -> port ->
|
|
25
|
+
the same numbers: the ingest path is proven in simulation before it
|
|
26
|
+
ever touches a site.
|
|
27
|
+
|
|
28
|
+
Implemented file formats:
|
|
29
|
+
* historian_csv — wide-format historian export: first column timestamp
|
|
30
|
+
(ISO or numeric), remaining columns channels; blank cells = missing.
|
|
31
|
+
Auto-detects the v1.3.0 telemetry export layout (flag_* columns).
|
|
32
|
+
* las2 — LAS 2.0 well-log files (public standard, Canadian Well Logging
|
|
33
|
+
Society): ~V/~W/~C/~A sections, NULL substitution, unwrapped data.
|
|
34
|
+
Wrapped-mode files are REFUSED (unsupported), never mis-parsed.
|
|
35
|
+
|
|
36
|
+
Headless-safe: numpy only.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import csv
|
|
42
|
+
import math
|
|
43
|
+
from dataclasses import dataclass, field
|
|
44
|
+
from datetime import datetime
|
|
45
|
+
from pathlib import Path
|
|
46
|
+
from typing import Callable, Dict, List, Optional
|
|
47
|
+
|
|
48
|
+
import numpy as np
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# The normalized live-stream model
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
@dataclass
|
|
55
|
+
class StreamChannel:
|
|
56
|
+
name: str
|
|
57
|
+
unit: str
|
|
58
|
+
values: np.ndarray # NaN = missing
|
|
59
|
+
quality: Optional[List[str]] = None # per-sample flags if the source carries them
|
|
60
|
+
|
|
61
|
+
def coverage_pct(self) -> float:
|
|
62
|
+
n = len(self.values)
|
|
63
|
+
return round(100.0 * float(np.sum(~np.isnan(self.values))) / n, 2) if n else 0.0
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class LiveStream:
|
|
68
|
+
"""The normalized ingest product: an index (time OR depth) + channels.
|
|
69
|
+
|
|
70
|
+
index_kind is 'time_s' (elapsed seconds; historian) or 'depth' (LAS).
|
|
71
|
+
This is the object the reconciler hangs on the closed stream.
|
|
72
|
+
"""
|
|
73
|
+
name: str
|
|
74
|
+
source_format: str
|
|
75
|
+
index_kind: str
|
|
76
|
+
index: np.ndarray
|
|
77
|
+
channels: Dict[str, StreamChannel] = field(default_factory=dict)
|
|
78
|
+
meta: Dict[str, str] = field(default_factory=dict)
|
|
79
|
+
|
|
80
|
+
def channel(self, name: str) -> StreamChannel:
|
|
81
|
+
return self.channels[name]
|
|
82
|
+
|
|
83
|
+
def summary(self) -> dict:
|
|
84
|
+
return {
|
|
85
|
+
'name': self.name,
|
|
86
|
+
'source_format': self.source_format,
|
|
87
|
+
'index_kind': self.index_kind,
|
|
88
|
+
'samples': int(len(self.index)),
|
|
89
|
+
'channels': {n: {'unit': c.unit, 'coverage_pct': c.coverage_pct()}
|
|
90
|
+
for n, c in self.channels.items()},
|
|
91
|
+
'meta': dict(self.meta),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
# historian_csv reader (auto-detects the v1.3.0 telemetry export layout)
|
|
97
|
+
# ---------------------------------------------------------------------------
|
|
98
|
+
def _parse_time(s: str, t0: Optional[datetime]) -> tuple:
|
|
99
|
+
try:
|
|
100
|
+
return float(s), t0
|
|
101
|
+
except ValueError:
|
|
102
|
+
dt = datetime.fromisoformat(s)
|
|
103
|
+
if t0 is None:
|
|
104
|
+
t0 = dt
|
|
105
|
+
return (dt - t0).total_seconds(), t0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def read_historian_csv(path) -> LiveStream:
|
|
109
|
+
"""Wide-format historian export: col 0 = timestamp (ISO or numeric),
|
|
110
|
+
remaining columns = channels; blank cells = missing -> NaN. Columns named
|
|
111
|
+
flag_* (the v1.3.0 telemetry layout) become per-gauge quality flags
|
|
112
|
+
attached to that gauge's channels instead of numeric channels."""
|
|
113
|
+
p = Path(path)
|
|
114
|
+
with p.open(newline="") as f:
|
|
115
|
+
rows = list(csv.reader(f))
|
|
116
|
+
if len(rows) < 2:
|
|
117
|
+
raise ValueError(f"{p}: no data rows")
|
|
118
|
+
header = rows[0]
|
|
119
|
+
data = rows[1:]
|
|
120
|
+
n = len(data)
|
|
121
|
+
times = np.zeros(n)
|
|
122
|
+
t0 = None
|
|
123
|
+
for i, r in enumerate(data):
|
|
124
|
+
times[i], t0 = _parse_time(r[0], t0)
|
|
125
|
+
flag_cols = {j: h for j, h in enumerate(header) if h.startswith('flag_')}
|
|
126
|
+
chan_cols = [j for j in range(1, len(header)) if j not in flag_cols]
|
|
127
|
+
channels: Dict[str, StreamChannel] = {}
|
|
128
|
+
for j in chan_cols:
|
|
129
|
+
vals = np.full(n, np.nan)
|
|
130
|
+
for i, r in enumerate(data):
|
|
131
|
+
cell = r[j].strip() if j < len(r) else ''
|
|
132
|
+
if cell:
|
|
133
|
+
try:
|
|
134
|
+
vals[i] = float(cell)
|
|
135
|
+
except ValueError:
|
|
136
|
+
pass # non-numeric cell in a numeric channel -> missing
|
|
137
|
+
name = header[j]
|
|
138
|
+
unit = 'psi' if 'psi' in name.lower() else ('degF' if ('_f_' in name.lower() or name.lower().endswith('_f') or 't_' in name.lower()[:2]) else '')
|
|
139
|
+
channels[name] = StreamChannel(name=name, unit=unit, values=vals)
|
|
140
|
+
for j, h in flag_cols.items():
|
|
141
|
+
tag = h[len('flag_'):] # e.g. 'S1'
|
|
142
|
+
flags = [(r[j].strip() if j < len(r) else '') for r in data]
|
|
143
|
+
for cname, ch in channels.items():
|
|
144
|
+
if cname.endswith('_' + tag) or ('_' + tag + '_') in cname:
|
|
145
|
+
ch.quality = flags
|
|
146
|
+
meta = {'path': str(p), 'columns': str(len(header))}
|
|
147
|
+
if t0 is not None:
|
|
148
|
+
meta['start_time'] = t0.isoformat() # ISO origin of the elapsed-seconds index (client records)
|
|
149
|
+
return LiveStream(name=p.stem, source_format='historian_csv',
|
|
150
|
+
index_kind='time_s', index=times, channels=channels,
|
|
151
|
+
meta=meta)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
# LAS 2.0 reader (public well-log standard; unwrapped mode)
|
|
156
|
+
# ---------------------------------------------------------------------------
|
|
157
|
+
def read_las(path) -> LiveStream:
|
|
158
|
+
"""LAS 2.0 reader: ~Version/~Well/~Parameter/~Curve/~ASCII sections,
|
|
159
|
+
NULL-value substitution -> NaN, depth-indexed curves.
|
|
160
|
+
|
|
161
|
+
WRAP. NO: one line per depth step. WRAP. YES (v1.13.0, driven by the real
|
|
162
|
+
Kennetcook #2 / P-129 catalogue well): records are assembled by
|
|
163
|
+
accumulating values until the curve count is reached - honest parsing
|
|
164
|
+
replaced the earlier refusal once a real wrapped file existed to verify
|
|
165
|
+
against. Records with a wrong value count are DROPPED, not guessed.
|
|
166
|
+
|
|
167
|
+
Meta captures WELL/COMP/FLD/DATE from ~W plus real downhole anchors from
|
|
168
|
+
~P when present (BHT, TMAX, MRT1, TDL, TDD, with units) - the converter's
|
|
169
|
+
measured-BHT tier feeds on these."""
|
|
170
|
+
p = Path(path)
|
|
171
|
+
lines = p.read_text(encoding='utf-8', errors='ignore').splitlines()
|
|
172
|
+
section = ''
|
|
173
|
+
wrap = False
|
|
174
|
+
vers = 2.0
|
|
175
|
+
null_val = -999.25 # LAS convention default
|
|
176
|
+
curves: List[tuple] = [] # (mnemonic, unit)
|
|
177
|
+
data_rows: List[List[float]] = []
|
|
178
|
+
pend: List[float] = [] # wrapped-record accumulator
|
|
179
|
+
meta: Dict[str, str] = {}
|
|
180
|
+
_P_ANCHORS = ('BHT', 'TMAX', 'MRT1', 'TDL', 'TDD')
|
|
181
|
+
for ln in lines:
|
|
182
|
+
s = ln.strip()
|
|
183
|
+
if not s or s.startswith('#'):
|
|
184
|
+
continue
|
|
185
|
+
if s.startswith('~'):
|
|
186
|
+
section = s[1].upper()
|
|
187
|
+
continue
|
|
188
|
+
if section == 'V':
|
|
189
|
+
if s.upper().startswith('WRAP') and '.' in s:
|
|
190
|
+
wrap = s.split('.', 1)[1].strip().upper().startswith('YES')
|
|
191
|
+
if s.upper().startswith('VERS') and '.' in s:
|
|
192
|
+
v = s.split('.', 1)[1].split(':')[0].strip().split()
|
|
193
|
+
if v and _is_float(v[0]):
|
|
194
|
+
vers = float(v[0])
|
|
195
|
+
elif section == 'W':
|
|
196
|
+
if s.upper().startswith('NULL') and '.' in s:
|
|
197
|
+
body = s.split('.', 1)[1]
|
|
198
|
+
val = body.split(':')[0].strip().split()
|
|
199
|
+
if val:
|
|
200
|
+
try:
|
|
201
|
+
null_val = float(val[-1])
|
|
202
|
+
except ValueError:
|
|
203
|
+
pass
|
|
204
|
+
for key in ('WELL', 'COMP', 'FLD', 'DATE'):
|
|
205
|
+
if s.upper().startswith(key):
|
|
206
|
+
if vers < 2.0 and ':' in s:
|
|
207
|
+
# LAS 1.x convention: the VALUE sits AFTER the colon
|
|
208
|
+
meta[key] = s.split(':', 1)[1].strip()
|
|
209
|
+
else:
|
|
210
|
+
meta[key] = s.split(':', 1)[0].split('.', 1)[-1].strip() if '.' in s else s
|
|
211
|
+
elif section == 'P':
|
|
212
|
+
head = s.split(':', 1)[0]
|
|
213
|
+
if '.' in head:
|
|
214
|
+
mnem, rest = head.split('.', 1)
|
|
215
|
+
mnem = mnem.strip().upper()
|
|
216
|
+
if mnem in _P_ANCHORS:
|
|
217
|
+
parts = rest.strip().split()
|
|
218
|
+
if parts:
|
|
219
|
+
unit = parts[0] if not _is_float(parts[0]) else ''
|
|
220
|
+
vals = [x for x in parts if _is_float(x)]
|
|
221
|
+
if vals:
|
|
222
|
+
meta[mnem] = vals[-1]
|
|
223
|
+
if unit:
|
|
224
|
+
meta[mnem + '_UNIT'] = unit
|
|
225
|
+
elif section == 'C':
|
|
226
|
+
head = s.split(':', 1)[0]
|
|
227
|
+
if '.' in head:
|
|
228
|
+
mnem, rest = head.split('.', 1)
|
|
229
|
+
curves.append((mnem.strip(), rest.strip().split()[0] if rest.strip() else ''))
|
|
230
|
+
elif section == 'A':
|
|
231
|
+
try:
|
|
232
|
+
vals = [float(x) for x in s.split()]
|
|
233
|
+
except ValueError:
|
|
234
|
+
continue
|
|
235
|
+
if not wrap:
|
|
236
|
+
data_rows.append(vals)
|
|
237
|
+
else:
|
|
238
|
+
pend.extend(vals)
|
|
239
|
+
while len(pend) >= len(curves) > 0:
|
|
240
|
+
data_rows.append(pend[:len(curves)])
|
|
241
|
+
pend = pend[len(curves):]
|
|
242
|
+
if wrap and pend:
|
|
243
|
+
pass # trailing partial record dropped, not guessed
|
|
244
|
+
if not curves or not data_rows:
|
|
245
|
+
raise ValueError(f"{p}: no curves or no data (need ~Curve and ~ASCII sections)")
|
|
246
|
+
width = len(curves)
|
|
247
|
+
arr = np.full((len(data_rows), width), np.nan)
|
|
248
|
+
for i, r in enumerate(data_rows):
|
|
249
|
+
for j in range(min(width, len(r))):
|
|
250
|
+
arr[i, j] = r[j]
|
|
251
|
+
arr[arr == null_val] = np.nan
|
|
252
|
+
index = arr[:, 0]
|
|
253
|
+
channels = {m: StreamChannel(name=m, unit=u, values=arr[:, j])
|
|
254
|
+
for j, (m, u) in enumerate(curves) if j > 0}
|
|
255
|
+
return LiveStream(name=p.stem, source_format='las2',
|
|
256
|
+
index_kind='depth', index=index, channels=channels, meta=meta)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _is_float(x: str) -> bool:
|
|
260
|
+
try:
|
|
261
|
+
float(x)
|
|
262
|
+
return True
|
|
263
|
+
except ValueError:
|
|
264
|
+
return False
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# The plug-in registry
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
IMPLEMENTED = "IMPLEMENTED"
|
|
271
|
+
DECLARED_SITE_DETAILS_REQUIRED = "DECLARED_SITE_DETAILS_REQUIRED"
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@dataclass(frozen=True)
|
|
275
|
+
class PortSpec:
|
|
276
|
+
name: str
|
|
277
|
+
transport: str
|
|
278
|
+
status: str
|
|
279
|
+
reader: Optional[Callable] = None
|
|
280
|
+
detail: str = ""
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _refuse(name, need):
|
|
284
|
+
def f(*a, **k):
|
|
285
|
+
raise NotImplementedError(
|
|
286
|
+
f"port '{name}' is DECLARED but not implemented - {need}. "
|
|
287
|
+
"The registry names the protocol; it does not invent site behavior. "
|
|
288
|
+
"Provide site details (or a site reader via register_port).")
|
|
289
|
+
return f
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
PORT_REGISTRY: Dict[str, PortSpec] = {
|
|
293
|
+
'historian_csv': PortSpec(
|
|
294
|
+
name='historian_csv', transport='file (wide-format historian CSV export)',
|
|
295
|
+
status=IMPLEMENTED, reader=read_historian_csv,
|
|
296
|
+
detail="round-trip verified against the v1.3.0 telemetry export layout"),
|
|
297
|
+
'las2': PortSpec(
|
|
298
|
+
name='las2', transport='file (LAS 2.0 well log, CWLS public standard)',
|
|
299
|
+
status=IMPLEMENTED, reader=read_las,
|
|
300
|
+
detail="unwrapped mode; NULL substitution; wrapped mode refused"),
|
|
301
|
+
# NOTE (v1.41.0): this base entry is the PRE-IMPORT fallback only. Importing
|
|
302
|
+
# modbus (which the package __init__ always does) UPGRADES this entry in
|
|
303
|
+
# place to the real pymodbus TCP client (reader=read_modbus, status=
|
|
304
|
+
# IMPLEMENTED_REQUIRES_SITE_CONFIG when pymodbus is installed). A static read
|
|
305
|
+
# of this file alone therefore understates the shipped capability - this
|
|
306
|
+
# comment exists so source and runtime tell the same story.
|
|
307
|
+
'modbus_g6': PortSpec(
|
|
308
|
+
name='modbus_g6', transport='Modbus RS485 (G6 interface card; 4-20mA analog alt.)',
|
|
309
|
+
status=DECLARED_SITE_DETAILS_REQUIRED, reader=_refuse('modbus_g6',
|
|
310
|
+
"needs the site's register map and polling parameters (target declared by the "
|
|
311
|
+
"tool library's surface_interface_g6 entry, GEOQ 177 spec-table footnote); "
|
|
312
|
+
"NOTE: modbus upgrades this entry to the real client at package import"),
|
|
313
|
+
detail="READ-ONLY tap; base declaration - upgraded in place by modbus at package import (see modbus.py tail)"),
|
|
314
|
+
'witsml': PortSpec(
|
|
315
|
+
name='witsml', transport='WITSML server (rig-site data exchange standard)',
|
|
316
|
+
status=DECLARED_SITE_DETAILS_REQUIRED, reader=_refuse('witsml',
|
|
317
|
+
"needs the site's server URL, version (1.4.1/2.0), and credentials"),
|
|
318
|
+
detail="READ-ONLY query of log/trajectory objects"),
|
|
319
|
+
'opcua': PortSpec(
|
|
320
|
+
name='opcua', transport='OPC-UA (plant/SCADA historian access)',
|
|
321
|
+
status=DECLARED_SITE_DETAILS_REQUIRED, reader=_refuse('opcua',
|
|
322
|
+
"needs the site's endpoint and node ids"),
|
|
323
|
+
detail="READ-ONLY subscription to gauge tags"),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def register_port(name: str, transport: str, reader: Callable, detail: str = "") -> None:
|
|
328
|
+
"""Plug a site-specific reader in without touching this module.
|
|
329
|
+
The reader must return a LiveStream. Registration is additive only."""
|
|
330
|
+
PORT_REGISTRY[name] = PortSpec(name=name, transport=transport,
|
|
331
|
+
status=IMPLEMENTED, reader=reader, detail=detail)
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def ingest(source, port: str = 'historian_csv') -> LiveStream:
|
|
335
|
+
"""The single entry point: ingest a source through a named port."""
|
|
336
|
+
spec = PORT_REGISTRY[port]
|
|
337
|
+
stream = spec.reader(source)
|
|
338
|
+
if not isinstance(stream, LiveStream):
|
|
339
|
+
raise TypeError(f"port '{port}' returned {type(stream).__name__}, not LiveStream")
|
|
340
|
+
return stream
|