well-log-toolkit 0.1.150__tar.gz → 0.1.151__tar.gz
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-0.1.150 → well_log_toolkit-0.1.151}/PKG-INFO +1 -1
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/pyproject.toml +1 -1
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/manager.py +509 -4
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/PKG-INFO +1 -1
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/README.md +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/setup.cfg +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/__init__.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/exceptions.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/las_file.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/operations.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/property.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/regression.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/statistics.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/utils.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/visualization.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit/well.py +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/SOURCES.txt +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/dependency_links.txt +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/requires.txt +0 -0
- {well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/top_level.txt +0 -0
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "well-log-toolkit"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.151"
|
|
8
8
|
description = "Fast LAS file processing with lazy loading and filtering for well log analysis"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
@@ -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.
|
|
@@ -1299,7 +1804,7 @@ class _ManagerPropertyProxy:
|
|
|
1299
1804
|
arithmetic: Optional[bool] = None,
|
|
1300
1805
|
precision: int = 6,
|
|
1301
1806
|
nested: bool = False
|
|
1302
|
-
) ->
|
|
1807
|
+
) -> SumsAvgResult:
|
|
1303
1808
|
"""
|
|
1304
1809
|
Compute hierarchical statistics grouped by filters across all wells.
|
|
1305
1810
|
|
|
@@ -1406,7 +1911,7 @@ class _ManagerPropertyProxy:
|
|
|
1406
1911
|
if well_result is not None:
|
|
1407
1912
|
result[well_name] = well_result
|
|
1408
1913
|
|
|
1409
|
-
return _sanitize_for_json(result)
|
|
1914
|
+
return SumsAvgResult(_sanitize_for_json(result))
|
|
1410
1915
|
|
|
1411
1916
|
def _compute_sums_avg_for_well(
|
|
1412
1917
|
self,
|
|
@@ -1759,7 +2264,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1759
2264
|
weighted: Optional[bool] = None,
|
|
1760
2265
|
arithmetic: Optional[bool] = None,
|
|
1761
2266
|
precision: int = 6
|
|
1762
|
-
) ->
|
|
2267
|
+
) -> SumsAvgResult:
|
|
1763
2268
|
"""
|
|
1764
2269
|
Compute statistics for multiple properties across all wells.
|
|
1765
2270
|
|
|
@@ -1817,7 +2322,7 @@ class _ManagerMultiPropertyProxy:
|
|
|
1817
2322
|
if well_result is not None:
|
|
1818
2323
|
result[well_name] = well_result
|
|
1819
2324
|
|
|
1820
|
-
return _sanitize_for_json(result)
|
|
2325
|
+
return SumsAvgResult(_sanitize_for_json(result))
|
|
1821
2326
|
|
|
1822
2327
|
def _compute_sums_avg_for_well(
|
|
1823
2328
|
self,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/requires.txt
RENAMED
|
File without changes
|
{well_log_toolkit-0.1.150 → well_log_toolkit-0.1.151}/well_log_toolkit.egg-info/top_level.txt
RENAMED
|
File without changes
|