well-log-toolkit 0.1.150__py3-none-any.whl → 0.1.151__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.
@@ -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
- ) -> dict:
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
- ) -> dict:
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,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: well-log-toolkit
3
- Version: 0.1.150
3
+ Version: 0.1.151
4
4
  Summary: Fast LAS file processing with lazy loading and filtering for well log analysis
5
5
  Author-email: Kristian dF Kollsgård <kkollsg@gmail.com>
6
6
  License: MIT
@@ -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=F6C8SXSD53FtG16-u3z298ViAj-5cMeNLSAoU77MyXY,140694
4
+ well_log_toolkit/manager.py,sha256=L0FseLmqCPbjgtbkmFaEox5YULF6MpEO4kDuh7vbmsw,162353
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.150.dist-info/METADATA,sha256=I5J-CdMD7XIL0uhVhHnergVpoFxblRISMCT5KumaiU8,63473
13
- well_log_toolkit-0.1.150.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
- well_log_toolkit-0.1.150.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
- well_log_toolkit-0.1.150.dist-info/RECORD,,
12
+ well_log_toolkit-0.1.151.dist-info/METADATA,sha256=nf13dYGxWVXABJbsUutg4QfwQzAG2wjqoXRR-B7GVHc,63473
13
+ well_log_toolkit-0.1.151.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
14
+ well_log_toolkit-0.1.151.dist-info/top_level.txt,sha256=BMOo7OKLcZEnjo0wOLMclwzwTbYKYh31I8RGDOGSBdE,17
15
+ well_log_toolkit-0.1.151.dist-info/RECORD,,