well-log-toolkit 0.1.150__py3-none-any.whl → 0.1.152__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.
- well_log_toolkit/manager.py +520 -5
- {well_log_toolkit-0.1.150.dist-info → well_log_toolkit-0.1.152.dist-info}/METADATA +1 -1
- {well_log_toolkit-0.1.150.dist-info → well_log_toolkit-0.1.152.dist-info}/RECORD +5 -5
- {well_log_toolkit-0.1.150.dist-info → well_log_toolkit-0.1.152.dist-info}/WHEEL +0 -0
- {well_log_toolkit-0.1.150.dist-info → well_log_toolkit-0.1.152.dist-info}/top_level.txt +0 -0
well_log_toolkit/manager.py
CHANGED
|
@@ -48,6 +48,511 @@ def _sanitize_for_json(obj):
|
|
|
48
48
|
return obj
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
class SumsAvgResult(dict):
|
|
52
|
+
"""
|
|
53
|
+
Dictionary subclass for sums_avg results with reporting capabilities.
|
|
54
|
+
|
|
55
|
+
Behaves exactly like a regular dict but adds the `.report()` method
|
|
56
|
+
for generating formatted reports with cross-well aggregation.
|
|
57
|
+
|
|
58
|
+
Examples
|
|
59
|
+
--------
|
|
60
|
+
>>> results = manager.properties(['PHIE', 'PERM']).filter('Facies').filter_intervals("Zones").sums_avg()
|
|
61
|
+
>>> results['Well_A'] # Normal dict access
|
|
62
|
+
>>> results.report(zones=[...], groups={...}, columns=[...]) # Generate report
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def report(
|
|
66
|
+
self,
|
|
67
|
+
zones: list[str],
|
|
68
|
+
groups: dict[str, list[str]],
|
|
69
|
+
columns: list[dict],
|
|
70
|
+
print_report: bool = True
|
|
71
|
+
) -> Optional[dict]:
|
|
72
|
+
"""
|
|
73
|
+
Generate a structured report with cross-well aggregation.
|
|
74
|
+
|
|
75
|
+
Parameters
|
|
76
|
+
----------
|
|
77
|
+
zones : list[str]
|
|
78
|
+
Zone names to include in the report
|
|
79
|
+
groups : dict[str, list[str]]
|
|
80
|
+
Facies grouping, e.g., {"NonNet": ["NonNet", "Slump"], "Net": ["Channel Sand"]}
|
|
81
|
+
columns : list[dict]
|
|
82
|
+
Column specifications, each with:
|
|
83
|
+
- property (str, required): Property name in results
|
|
84
|
+
- stat (str, required): Statistic to extract (mean, std_dev, p10, etc.)
|
|
85
|
+
- label (str, optional): Display name (defaults to stat)
|
|
86
|
+
- format (str, optional): Number format (e.g., ".4f")
|
|
87
|
+
- unit (str, optional): Display unit for printing
|
|
88
|
+
- factor (float, optional): Multiplier for value conversion (default 1.0)
|
|
89
|
+
- agg (str, optional): Cross-well aggregation method:
|
|
90
|
+
- "arithmetic" (default for mean): thickness-weighted arithmetic mean
|
|
91
|
+
- "geometric": thickness-weighted geometric mean (for permeability)
|
|
92
|
+
- "pooled" (default for std_dev): pooled standard deviation
|
|
93
|
+
- "sum": simple sum (for thickness)
|
|
94
|
+
print_report : bool, default True
|
|
95
|
+
If True, print formatted report. If False, return structured data.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
dict or None
|
|
100
|
+
If print_report=False, returns structured data dict.
|
|
101
|
+
If print_report=True, prints report and returns None.
|
|
102
|
+
|
|
103
|
+
Raises
|
|
104
|
+
------
|
|
105
|
+
ValueError
|
|
106
|
+
If pooled aggregation is requested but corresponding mean column is missing.
|
|
107
|
+
|
|
108
|
+
Examples
|
|
109
|
+
--------
|
|
110
|
+
>>> results.report(
|
|
111
|
+
... zones=["Sand 3_SST", "Sand 2_SST"],
|
|
112
|
+
... groups={"NonNet": ["NonNet", "Slump"], "Net": ["Channel Sand", "LowQ Sand"]},
|
|
113
|
+
... columns=[
|
|
114
|
+
... {"property": "CPI_PHIE_2025", "stat": "mean", "label": "por", "format": ".4f"},
|
|
115
|
+
... {"property": "CPI_PHIE_2025", "stat": "std_dev", "label": "std", "format": ".4f"},
|
|
116
|
+
... {"property": "CPI_PERM_CALC_2025", "stat": "mean", "label": "perm", "agg": "geometric", "unit": "mD", "format": ".2f"},
|
|
117
|
+
... ]
|
|
118
|
+
... )
|
|
119
|
+
"""
|
|
120
|
+
# Validate columns
|
|
121
|
+
self._validate_columns(columns)
|
|
122
|
+
|
|
123
|
+
# Generate structured data
|
|
124
|
+
report_data = self._generate_report_data(zones, groups, columns)
|
|
125
|
+
|
|
126
|
+
if print_report:
|
|
127
|
+
self._print_report(report_data, columns)
|
|
128
|
+
return None
|
|
129
|
+
else:
|
|
130
|
+
return report_data
|
|
131
|
+
|
|
132
|
+
def _validate_columns(self, columns: list[dict]) -> None:
|
|
133
|
+
"""Validate column specifications."""
|
|
134
|
+
# Check required fields
|
|
135
|
+
for i, col in enumerate(columns):
|
|
136
|
+
if 'property' not in col:
|
|
137
|
+
raise ValueError(f"Column {i} missing required 'property' field")
|
|
138
|
+
if 'stat' not in col:
|
|
139
|
+
raise ValueError(f"Column {i} missing required 'stat' field")
|
|
140
|
+
|
|
141
|
+
# Check pooled std_dev has corresponding mean
|
|
142
|
+
for col in columns:
|
|
143
|
+
agg = col.get('agg')
|
|
144
|
+
stat = col.get('stat')
|
|
145
|
+
prop = col.get('property')
|
|
146
|
+
|
|
147
|
+
# Default agg for std_dev is pooled
|
|
148
|
+
if stat == 'std_dev' and agg is None:
|
|
149
|
+
agg = 'pooled'
|
|
150
|
+
|
|
151
|
+
if agg == 'pooled':
|
|
152
|
+
# Find corresponding mean column
|
|
153
|
+
has_mean = any(
|
|
154
|
+
c.get('property') == prop and c.get('stat') == 'mean'
|
|
155
|
+
for c in columns
|
|
156
|
+
)
|
|
157
|
+
if not has_mean:
|
|
158
|
+
raise ValueError(
|
|
159
|
+
f"Column with property='{prop}' and stat='std_dev' uses pooled aggregation, "
|
|
160
|
+
f"but no corresponding mean column found. Add a column with "
|
|
161
|
+
f"property='{prop}' and stat='mean'."
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _get_column_defaults(self, col: dict) -> dict:
|
|
165
|
+
"""Get column spec with defaults applied."""
|
|
166
|
+
stat = col.get('stat', 'mean')
|
|
167
|
+
|
|
168
|
+
# Default aggregation based on stat type
|
|
169
|
+
if stat == 'std_dev':
|
|
170
|
+
default_agg = 'pooled'
|
|
171
|
+
else:
|
|
172
|
+
default_agg = 'arithmetic'
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
'property': col['property'],
|
|
176
|
+
'stat': stat,
|
|
177
|
+
'label': col.get('label', stat),
|
|
178
|
+
'format': col.get('format', '.4f'),
|
|
179
|
+
'unit': col.get('unit', ''),
|
|
180
|
+
'factor': col.get('factor', 1.0),
|
|
181
|
+
'agg': col.get('agg', default_agg),
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
def _extract_value(self, facies_data: dict, col: dict) -> Optional[float]:
|
|
185
|
+
"""Extract a value from facies data based on column spec."""
|
|
186
|
+
col = self._get_column_defaults(col)
|
|
187
|
+
prop = col['property']
|
|
188
|
+
stat = col['stat']
|
|
189
|
+
factor = col['factor']
|
|
190
|
+
|
|
191
|
+
if prop not in facies_data:
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
prop_data = facies_data[prop]
|
|
195
|
+
if not isinstance(prop_data, dict) or stat not in prop_data:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
value = prop_data[stat]
|
|
199
|
+
if value is None:
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
return value * factor
|
|
203
|
+
|
|
204
|
+
def _generate_report_data(
|
|
205
|
+
self,
|
|
206
|
+
zones: list[str],
|
|
207
|
+
groups: dict[str, list[str]],
|
|
208
|
+
columns: list[dict]
|
|
209
|
+
) -> dict:
|
|
210
|
+
"""Generate structured report data from results."""
|
|
211
|
+
report = {}
|
|
212
|
+
|
|
213
|
+
# Collect data for aggregation
|
|
214
|
+
# Structure: {zone: {group: {facies: {label: [values], "thick": [thicknesses]}}}}
|
|
215
|
+
aggregation_data = {}
|
|
216
|
+
|
|
217
|
+
# Process each well
|
|
218
|
+
for well_name, well_data in self.items():
|
|
219
|
+
if well_name == "Summary":
|
|
220
|
+
continue # Skip if already has summary
|
|
221
|
+
|
|
222
|
+
well_report = {}
|
|
223
|
+
|
|
224
|
+
for zone_name in zones:
|
|
225
|
+
if zone_name not in well_data:
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
zone_data = well_data[zone_name]
|
|
229
|
+
zone_report = {}
|
|
230
|
+
|
|
231
|
+
# Calculate total zone thickness from all facies
|
|
232
|
+
zone_thickness = 0.0
|
|
233
|
+
for facies_name, facies_data in zone_data.items():
|
|
234
|
+
if isinstance(facies_data, dict) and 'thickness' in facies_data:
|
|
235
|
+
zone_thickness += facies_data['thickness']
|
|
236
|
+
|
|
237
|
+
zone_report['thickness'] = zone_thickness
|
|
238
|
+
|
|
239
|
+
# Process each group
|
|
240
|
+
for group_name, facies_list in groups.items():
|
|
241
|
+
existing_facies = [f for f in facies_list if f in zone_data]
|
|
242
|
+
if not existing_facies:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
group_report = {}
|
|
246
|
+
group_thickness = sum(
|
|
247
|
+
zone_data[f]['thickness'] for f in existing_facies
|
|
248
|
+
if isinstance(zone_data[f], dict) and 'thickness' in zone_data[f]
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
group_report['thickness'] = group_thickness
|
|
252
|
+
group_report['fraction'] = group_thickness / zone_thickness if zone_thickness > 0 else 0.0
|
|
253
|
+
|
|
254
|
+
# Process each facies in the group
|
|
255
|
+
for facies_name in existing_facies:
|
|
256
|
+
facies_data = zone_data[facies_name]
|
|
257
|
+
if not isinstance(facies_data, dict):
|
|
258
|
+
continue
|
|
259
|
+
|
|
260
|
+
facies_thickness = facies_data.get('thickness', 0.0)
|
|
261
|
+
if facies_thickness <= 0:
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
facies_report = {
|
|
265
|
+
'thickness': facies_thickness,
|
|
266
|
+
'fraction': facies_thickness / group_thickness if group_thickness > 0 else 0.0,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
# Extract column values
|
|
270
|
+
for col in columns:
|
|
271
|
+
col_def = self._get_column_defaults(col)
|
|
272
|
+
label = col_def['label']
|
|
273
|
+
value = self._extract_value(facies_data, col)
|
|
274
|
+
facies_report[label] = value
|
|
275
|
+
|
|
276
|
+
group_report[facies_name] = facies_report
|
|
277
|
+
|
|
278
|
+
# Collect for aggregation
|
|
279
|
+
if zone_name not in aggregation_data:
|
|
280
|
+
aggregation_data[zone_name] = {}
|
|
281
|
+
if group_name not in aggregation_data[zone_name]:
|
|
282
|
+
aggregation_data[zone_name][group_name] = {}
|
|
283
|
+
if facies_name not in aggregation_data[zone_name][group_name]:
|
|
284
|
+
aggregation_data[zone_name][group_name][facies_name] = {
|
|
285
|
+
'thick': [], 'values': {}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
agg_facies = aggregation_data[zone_name][group_name][facies_name]
|
|
289
|
+
agg_facies['thick'].append(facies_thickness)
|
|
290
|
+
|
|
291
|
+
for col in columns:
|
|
292
|
+
col_def = self._get_column_defaults(col)
|
|
293
|
+
label = col_def['label']
|
|
294
|
+
value = self._extract_value(facies_data, col)
|
|
295
|
+
if label not in agg_facies['values']:
|
|
296
|
+
agg_facies['values'][label] = []
|
|
297
|
+
agg_facies['values'][label].append(value)
|
|
298
|
+
|
|
299
|
+
zone_report[group_name] = group_report
|
|
300
|
+
|
|
301
|
+
if zone_report:
|
|
302
|
+
well_report[zone_name] = zone_report
|
|
303
|
+
|
|
304
|
+
if well_report:
|
|
305
|
+
report[well_name] = well_report
|
|
306
|
+
|
|
307
|
+
# Generate Summary
|
|
308
|
+
summary = self._generate_summary(aggregation_data, columns, zones, groups)
|
|
309
|
+
if summary:
|
|
310
|
+
report['Summary'] = summary
|
|
311
|
+
|
|
312
|
+
return report
|
|
313
|
+
|
|
314
|
+
def _generate_summary(
|
|
315
|
+
self,
|
|
316
|
+
aggregation_data: dict,
|
|
317
|
+
columns: list[dict],
|
|
318
|
+
zones: list[str],
|
|
319
|
+
groups: dict[str, list[str]]
|
|
320
|
+
) -> dict:
|
|
321
|
+
"""Generate cross-well summary using thickness-weighted aggregation."""
|
|
322
|
+
summary = {}
|
|
323
|
+
|
|
324
|
+
# Build lookup for mean values needed by pooled std
|
|
325
|
+
# {(zone, group, facies, property): grand_mean}
|
|
326
|
+
grand_means = {}
|
|
327
|
+
|
|
328
|
+
# First pass: compute all arithmetic means (needed for pooled std)
|
|
329
|
+
for zone_name, zone_agg in aggregation_data.items():
|
|
330
|
+
for group_name, group_agg in zone_agg.items():
|
|
331
|
+
for facies_name, facies_agg in group_agg.items():
|
|
332
|
+
thicks = np.array(facies_agg['thick'])
|
|
333
|
+
total_thick = np.sum(thicks)
|
|
334
|
+
if total_thick <= 0:
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
for col in columns:
|
|
338
|
+
col_def = self._get_column_defaults(col)
|
|
339
|
+
if col_def['stat'] == 'mean':
|
|
340
|
+
label = col_def['label']
|
|
341
|
+
prop = col_def['property']
|
|
342
|
+
values = facies_agg['values'].get(label, [])
|
|
343
|
+
|
|
344
|
+
valid_mask = [v is not None for v in values]
|
|
345
|
+
if not any(valid_mask):
|
|
346
|
+
continue
|
|
347
|
+
|
|
348
|
+
valid_vals = np.array([v for v, m in zip(values, valid_mask) if m])
|
|
349
|
+
valid_thicks = np.array([t for t, m in zip(thicks, valid_mask) if m])
|
|
350
|
+
valid_total = np.sum(valid_thicks)
|
|
351
|
+
|
|
352
|
+
if valid_total > 0:
|
|
353
|
+
grand_mean = np.sum(valid_vals * valid_thicks) / valid_total
|
|
354
|
+
grand_means[(zone_name, group_name, facies_name, prop)] = grand_mean
|
|
355
|
+
|
|
356
|
+
# Second pass: compute all aggregated values
|
|
357
|
+
for zone_name in zones:
|
|
358
|
+
if zone_name not in aggregation_data:
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
zone_agg = aggregation_data[zone_name]
|
|
362
|
+
zone_summary = {'thickness': 0.0}
|
|
363
|
+
|
|
364
|
+
for group_name, facies_list in groups.items():
|
|
365
|
+
if group_name not in zone_agg:
|
|
366
|
+
continue
|
|
367
|
+
|
|
368
|
+
group_agg = zone_agg[group_name]
|
|
369
|
+
group_summary = {'thickness': 0.0}
|
|
370
|
+
|
|
371
|
+
for facies_name in facies_list:
|
|
372
|
+
if facies_name not in group_agg:
|
|
373
|
+
continue
|
|
374
|
+
|
|
375
|
+
facies_agg = group_agg[facies_name]
|
|
376
|
+
thicks = np.array(facies_agg['thick'])
|
|
377
|
+
total_thick = np.sum(thicks)
|
|
378
|
+
|
|
379
|
+
if total_thick <= 0:
|
|
380
|
+
continue
|
|
381
|
+
|
|
382
|
+
facies_summary = {'thickness': total_thick, 'fraction': 0.0}
|
|
383
|
+
|
|
384
|
+
for col in columns:
|
|
385
|
+
col_def = self._get_column_defaults(col)
|
|
386
|
+
label = col_def['label']
|
|
387
|
+
prop = col_def['property']
|
|
388
|
+
agg_method = col_def['agg']
|
|
389
|
+
|
|
390
|
+
values = facies_agg['values'].get(label, [])
|
|
391
|
+
valid_mask = [v is not None for v in values]
|
|
392
|
+
|
|
393
|
+
if not any(valid_mask):
|
|
394
|
+
facies_summary[label] = None
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
valid_vals = np.array([v for v, m in zip(values, valid_mask) if m])
|
|
398
|
+
valid_thicks = np.array([t for t, m in zip(thicks, valid_mask) if m])
|
|
399
|
+
valid_total = np.sum(valid_thicks)
|
|
400
|
+
|
|
401
|
+
if valid_total <= 0:
|
|
402
|
+
facies_summary[label] = None
|
|
403
|
+
continue
|
|
404
|
+
|
|
405
|
+
if agg_method == 'arithmetic':
|
|
406
|
+
# Thickness-weighted arithmetic mean
|
|
407
|
+
agg_value = np.sum(valid_vals * valid_thicks) / valid_total
|
|
408
|
+
|
|
409
|
+
elif agg_method == 'geometric':
|
|
410
|
+
# Thickness-weighted geometric mean
|
|
411
|
+
# Filter out non-positive values for log
|
|
412
|
+
pos_mask = valid_vals > 0
|
|
413
|
+
if not np.any(pos_mask):
|
|
414
|
+
facies_summary[label] = None
|
|
415
|
+
continue
|
|
416
|
+
pos_vals = valid_vals[pos_mask]
|
|
417
|
+
pos_thicks = valid_thicks[pos_mask]
|
|
418
|
+
pos_total = np.sum(pos_thicks)
|
|
419
|
+
agg_value = np.exp(np.sum(np.log(pos_vals) * pos_thicks) / pos_total)
|
|
420
|
+
|
|
421
|
+
elif agg_method == 'pooled':
|
|
422
|
+
# Pooled standard deviation
|
|
423
|
+
# Requires the grand mean from the corresponding mean column
|
|
424
|
+
grand_mean = grand_means.get((zone_name, group_name, facies_name, prop))
|
|
425
|
+
if grand_mean is None:
|
|
426
|
+
facies_summary[label] = None
|
|
427
|
+
continue
|
|
428
|
+
|
|
429
|
+
# Get the corresponding std values (these are the per-well stds)
|
|
430
|
+
# and the per-well means
|
|
431
|
+
mean_label = None
|
|
432
|
+
for c in columns:
|
|
433
|
+
c_def = self._get_column_defaults(c)
|
|
434
|
+
if c_def['property'] == prop and c_def['stat'] == 'mean':
|
|
435
|
+
mean_label = c_def['label']
|
|
436
|
+
break
|
|
437
|
+
|
|
438
|
+
if mean_label is None:
|
|
439
|
+
facies_summary[label] = None
|
|
440
|
+
continue
|
|
441
|
+
|
|
442
|
+
mean_values = facies_agg['values'].get(mean_label, [])
|
|
443
|
+
std_values = values # current column values (stds)
|
|
444
|
+
|
|
445
|
+
# Both must be valid
|
|
446
|
+
combined_mask = [
|
|
447
|
+
m is not None and s is not None
|
|
448
|
+
for m, s in zip(mean_values, std_values)
|
|
449
|
+
]
|
|
450
|
+
if not any(combined_mask):
|
|
451
|
+
facies_summary[label] = None
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
combined_means = np.array([v for v, m in zip(mean_values, combined_mask) if m])
|
|
455
|
+
combined_stds = np.array([v for v, m in zip(std_values, combined_mask) if m])
|
|
456
|
+
combined_thicks = np.array([t for t, m in zip(thicks, combined_mask) if m])
|
|
457
|
+
combined_total = np.sum(combined_thicks)
|
|
458
|
+
|
|
459
|
+
# Pooled variance formula:
|
|
460
|
+
# var_pooled = sum(thick * (std^2 + (mean - grand_mean)^2)) / total_thick
|
|
461
|
+
pooled_var = np.sum(
|
|
462
|
+
combined_thicks * (combined_stds**2 + (combined_means - grand_mean)**2)
|
|
463
|
+
) / combined_total
|
|
464
|
+
agg_value = np.sqrt(pooled_var)
|
|
465
|
+
|
|
466
|
+
elif agg_method == 'sum':
|
|
467
|
+
agg_value = np.sum(valid_vals)
|
|
468
|
+
|
|
469
|
+
else:
|
|
470
|
+
# Default to arithmetic
|
|
471
|
+
agg_value = np.sum(valid_vals * valid_thicks) / valid_total
|
|
472
|
+
|
|
473
|
+
facies_summary[label] = agg_value
|
|
474
|
+
|
|
475
|
+
group_summary[facies_name] = facies_summary
|
|
476
|
+
group_summary['thickness'] += total_thick
|
|
477
|
+
|
|
478
|
+
# Update facies fractions based on group total
|
|
479
|
+
group_thick = group_summary['thickness']
|
|
480
|
+
for facies_name in facies_list:
|
|
481
|
+
if facies_name in group_summary and isinstance(group_summary[facies_name], dict):
|
|
482
|
+
f_thick = group_summary[facies_name].get('thickness', 0)
|
|
483
|
+
group_summary[facies_name]['fraction'] = f_thick / group_thick if group_thick > 0 else 0.0
|
|
484
|
+
|
|
485
|
+
if group_summary['thickness'] > 0:
|
|
486
|
+
zone_summary[group_name] = group_summary
|
|
487
|
+
zone_summary['thickness'] += group_summary['thickness']
|
|
488
|
+
|
|
489
|
+
# Calculate group fractions
|
|
490
|
+
zone_thick = zone_summary['thickness']
|
|
491
|
+
for group_name in groups:
|
|
492
|
+
if group_name in zone_summary and isinstance(zone_summary[group_name], dict):
|
|
493
|
+
g_thick = zone_summary[group_name].get('thickness', 0)
|
|
494
|
+
zone_summary[group_name]['fraction'] = g_thick / zone_thick if zone_thick > 0 else 0.0
|
|
495
|
+
|
|
496
|
+
if zone_summary['thickness'] > 0:
|
|
497
|
+
summary[zone_name] = zone_summary
|
|
498
|
+
|
|
499
|
+
return summary
|
|
500
|
+
|
|
501
|
+
def _print_report(self, report_data: dict, columns: list[dict]) -> None:
|
|
502
|
+
"""Print formatted report."""
|
|
503
|
+
indent = " "
|
|
504
|
+
|
|
505
|
+
# Prepare column formatting
|
|
506
|
+
col_defs = [self._get_column_defaults(c) for c in columns]
|
|
507
|
+
|
|
508
|
+
for well_name, well_data in report_data.items():
|
|
509
|
+
print(f"\n{well_name}")
|
|
510
|
+
|
|
511
|
+
for zone_name, zone_data in well_data.items():
|
|
512
|
+
if not isinstance(zone_data, dict):
|
|
513
|
+
continue
|
|
514
|
+
|
|
515
|
+
zone_thick = zone_data.get('thickness', 0)
|
|
516
|
+
print(f"{indent}{zone_name:<13} iso: {zone_thick:>6.2f}m")
|
|
517
|
+
|
|
518
|
+
for group_name, group_data in zone_data.items():
|
|
519
|
+
if group_name == 'thickness' or not isinstance(group_data, dict):
|
|
520
|
+
continue
|
|
521
|
+
|
|
522
|
+
group_thick = group_data.get('thickness', 0)
|
|
523
|
+
group_frac = group_data.get('fraction', 0)
|
|
524
|
+
print(f"{indent*2}-- {group_name:<10} fraction: {group_frac:>7.4f} iso: {group_thick:>6.2f}m")
|
|
525
|
+
|
|
526
|
+
for facies_name, facies_data in group_data.items():
|
|
527
|
+
if facies_name in ('thickness', 'fraction') or not isinstance(facies_data, dict):
|
|
528
|
+
continue
|
|
529
|
+
|
|
530
|
+
f_thick = facies_data.get('thickness', 0)
|
|
531
|
+
f_frac = facies_data.get('fraction', 0)
|
|
532
|
+
|
|
533
|
+
# Build column values string
|
|
534
|
+
col_strs = []
|
|
535
|
+
for col_def in col_defs:
|
|
536
|
+
label = col_def['label']
|
|
537
|
+
fmt = col_def['format']
|
|
538
|
+
unit = col_def['unit']
|
|
539
|
+
value = facies_data.get(label)
|
|
540
|
+
|
|
541
|
+
if value is not None:
|
|
542
|
+
formatted = f"{value:{fmt}}"
|
|
543
|
+
if unit:
|
|
544
|
+
col_strs.append(f"{label}: {formatted:>8}{unit}")
|
|
545
|
+
else:
|
|
546
|
+
col_strs.append(f"{label}: {formatted:>8}")
|
|
547
|
+
else:
|
|
548
|
+
col_strs.append(f"{label}: {'N/A':>8}")
|
|
549
|
+
|
|
550
|
+
col_str = " ".join(col_strs)
|
|
551
|
+
print(f"{indent*2}| {facies_name:<15} frac: {f_frac:>7.4f} iso: {f_thick:>6.2f}m {col_str}")
|
|
552
|
+
|
|
553
|
+
print("")
|
|
554
|
+
|
|
555
|
+
|
|
51
556
|
def _flatten_to_dataframe(nested_dict: dict, property_name: str) -> pd.DataFrame:
|
|
52
557
|
"""
|
|
53
558
|
Flatten nested dictionary results into a DataFrame.
|
|
@@ -171,11 +676,15 @@ class _ManagerPropertyProxy:
|
|
|
171
676
|
well_intervals = intervals
|
|
172
677
|
elif isinstance(intervals, dict):
|
|
173
678
|
# Well-specific intervals
|
|
679
|
+
# Check original name, sanitized name, and well_-prefixed sanitized name
|
|
174
680
|
well_intervals = None
|
|
681
|
+
prefixed_name = f"well_{well.sanitized_name}"
|
|
175
682
|
if well.name in intervals:
|
|
176
683
|
well_intervals = intervals[well.name]
|
|
177
684
|
elif well.sanitized_name in intervals:
|
|
178
685
|
well_intervals = intervals[well.sanitized_name]
|
|
686
|
+
elif prefixed_name in intervals:
|
|
687
|
+
well_intervals = intervals[prefixed_name]
|
|
179
688
|
if well_intervals is None:
|
|
180
689
|
return None # Skip wells not in the dict
|
|
181
690
|
else:
|
|
@@ -1299,7 +1808,7 @@ class _ManagerPropertyProxy:
|
|
|
1299
1808
|
arithmetic: Optional[bool] = None,
|
|
1300
1809
|
precision: int = 6,
|
|
1301
1810
|
nested: bool = False
|
|
1302
|
-
) ->
|
|
1811
|
+
) -> SumsAvgResult:
|
|
1303
1812
|
"""
|
|
1304
1813
|
Compute hierarchical statistics grouped by filters across all wells.
|
|
1305
1814
|
|
|
@@ -1406,7 +1915,7 @@ class _ManagerPropertyProxy:
|
|
|
1406
1915
|
if well_result is not None:
|
|
1407
1916
|
result[well_name] = well_result
|
|
1408
1917
|
|
|
1409
|
-
return _sanitize_for_json(result)
|
|
1918
|
+
return SumsAvgResult(_sanitize_for_json(result))
|
|
1410
1919
|
|
|
1411
1920
|
def _compute_sums_avg_for_well(
|
|
1412
1921
|
self,
|
|
@@ -1759,7 +2268,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1759
2268
|
weighted: Optional[bool] = None,
|
|
1760
2269
|
arithmetic: Optional[bool] = None,
|
|
1761
2270
|
precision: int = 6
|
|
1762
|
-
) ->
|
|
2271
|
+
) -> SumsAvgResult:
|
|
1763
2272
|
"""
|
|
1764
2273
|
Compute statistics for multiple properties across all wells.
|
|
1765
2274
|
|
|
@@ -1817,7 +2326,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1817
2326
|
if well_result is not None:
|
|
1818
2327
|
result[well_name] = well_result
|
|
1819
2328
|
|
|
1820
|
-
return _sanitize_for_json(result)
|
|
2329
|
+
return SumsAvgResult(_sanitize_for_json(result))
|
|
1821
2330
|
|
|
1822
2331
|
def _compute_sums_avg_for_well(
|
|
1823
2332
|
self,
|
|
@@ -1838,7 +2347,9 @@ class _ManagerMultiPropertyProxy:
|
|
|
1838
2347
|
return None # Skip wells that don't have this saved filter
|
|
1839
2348
|
elif isinstance(intervals, dict):
|
|
1840
2349
|
# Well-specific intervals - check if this well is in the dict
|
|
1841
|
-
|
|
2350
|
+
# Check original name, sanitized name, and well_-prefixed sanitized name
|
|
2351
|
+
prefixed_name = f"well_{well.sanitized_name}"
|
|
2352
|
+
if well.name not in intervals and well.sanitized_name not in intervals and prefixed_name not in intervals:
|
|
1842
2353
|
return None # Skip wells not in the dict
|
|
1843
2354
|
|
|
1844
2355
|
# Collect results for each property
|
|
@@ -1916,11 +2427,15 @@ class _ManagerMultiPropertyProxy:
|
|
|
1916
2427
|
well_intervals = intervals
|
|
1917
2428
|
elif isinstance(intervals, dict):
|
|
1918
2429
|
# Well-specific intervals
|
|
2430
|
+
# Check original name, sanitized name, and well_-prefixed sanitized name
|
|
1919
2431
|
well_intervals = None
|
|
2432
|
+
prefixed_name = f"well_{well.sanitized_name}"
|
|
1920
2433
|
if well.name in intervals:
|
|
1921
2434
|
well_intervals = intervals[well.name]
|
|
1922
2435
|
elif well.sanitized_name in intervals:
|
|
1923
2436
|
well_intervals = intervals[well.sanitized_name]
|
|
2437
|
+
elif prefixed_name in intervals:
|
|
2438
|
+
well_intervals = intervals[prefixed_name]
|
|
1924
2439
|
if well_intervals is None:
|
|
1925
2440
|
return None # Skip wells not in the dict
|
|
1926
2441
|
elif isinstance(intervals, list):
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
well_log_toolkit/__init__.py,sha256=ilJAIIhh68pYfD9I3V53juTEJpoMN8oHpcpEFNpuXAQ,3793
|
|
2
2
|
well_log_toolkit/exceptions.py,sha256=X_fzC7d4yaBFO9Vx74dEIB6xmI9Agi6_bTU3MPxn6ko,985
|
|
3
3
|
well_log_toolkit/las_file.py,sha256=Tj0mRfX1aX2s6uug7BBlY1m_mu3G50EGxHGzD0eEedE,53876
|
|
4
|
-
well_log_toolkit/manager.py,sha256=
|
|
4
|
+
well_log_toolkit/manager.py,sha256=vlOPAJmKNnuZNykZdLwLUPBNwH7BWrKY22hx7FOW0S0,163031
|
|
5
5
|
well_log_toolkit/operations.py,sha256=z8j8fGBOwoJGUQFy-Vawjq9nm3OD_dUt0oaNh8yuG7o,18515
|
|
6
6
|
well_log_toolkit/property.py,sha256=XY3BAN76CY6KY8na4iyoz6P-inhDyb821o3gN7ZC3q4,104184
|
|
7
7
|
well_log_toolkit/regression.py,sha256=JDcRxaODJnFikAdPJyTq8eUV7iY0vCDmvnGufqlojxs,31625
|
|
@@ -9,7 +9,7 @@ well_log_toolkit/statistics.py,sha256=cpUbaRGlqyqpGWKtETk9XpXWrMJIIjVacdqEqIBkvq
|
|
|
9
9
|
well_log_toolkit/utils.py,sha256=O2KPq4htIoUlL74V2zKftdqqTjRfezU9M-568zPLme0,6866
|
|
10
10
|
well_log_toolkit/visualization.py,sha256=nnpmFmbj44TbP0fsnLMR1GaKRkqKCEpI6Fd8Cp0oqBc,204716
|
|
11
11
|
well_log_toolkit/well.py,sha256=n6XfaGSjGtyXCIaAr0ytslIK0DMUY_fSPQ_VCqj8jaU,106173
|
|
12
|
-
well_log_toolkit-0.1.
|
|
13
|
-
well_log_toolkit-0.1.
|
|
14
|
-
well_log_toolkit-0.1.
|
|
15
|
-
well_log_toolkit-0.1.
|
|
12
|
+
well_log_toolkit-0.1.152.dist-info/METADATA,sha256=jITXa53PORO2xidQsVqm1hvIOPlINDnxlOKctH1YqK0,63473
|
|
13
|
+
well_log_toolkit-0.1.152.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
14
|
+
well_log_toolkit-0.1.152.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
|
|
15
|
+
well_log_toolkit-0.1.152.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|