From 29ecdc9942240da00f46a038ad53cf52c7765723 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 10 Jul 2026 09:55:07 +0000 Subject: [PATCH] Mark LIBRARY_MERGE_PLAN complete; consolidate the Shier affine map - LIBRARY_MERGE_PLAN.md: add a completion status note (all eleven domain trains merged, PRs verified byte-identical; scope deltas vs the plan sketches documented, notably data_qc_io's units/metrics/align/pay files superseded by existing canon; non-bit-reproducible corpus forms stay local by design). - petrolib/data_qc_io/scale.py: normalize_to_reference now applies its affine map via ml_stats.affine_rescale instead of duplicating the expression. The two arrangements multiply in commuted order, which is bit-identical in IEEE arithmetic; verified over 600 randomized cases (np.array_equal) across the explicit-endpoint, percentile, and min/max paths, plus golden_diff on the three adopting directories (src2015_04, src2016_12, src2021_12). Gates: ruff + mypy clean; 252 tests pass. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01B2BX1XL8422gebCU1oVoQC --- LIBRARY_MERGE_PLAN.md | 16 ++++++++++++++++ petrolib/data_qc_io/scale.py | 11 +++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) diff --git a/LIBRARY_MERGE_PLAN.md b/LIBRARY_MERGE_PLAN.md index 821c42d..cccd6c0 100644 --- a/LIBRARY_MERGE_PLAN.md +++ b/LIBRARY_MERGE_PLAN.md @@ -10,6 +10,22 @@ implementations of shared concepts), and an evaluation of three competing migrat architectures scored independently for migration safety, end-state quality, and single-maintainer execution realism. +> **Status — July 2026: executed to completion.** All eleven domain trains are merged: +> `flow_transport`, `nmr`, `porosity_lithology`, `acoustic_geomech`, `geochem_fluids`, +> `em_dielectric`, `nuclear`, `inversion_numerics`, the depth/imaging group +> (`depth_matching`, `depth_correction`, `borehole_image`, `wellbore_geometry`), +> `integrity_drilling`, and `data_qc_io` — plus the earlier foundation modules +> (`constants`, `units`, `ml_stats`, `saturation_resistivity`, `capillary_pressure`, +> `relperm_wettability`, `testing`). Every adoption PR was verified byte-identical on +> stdout against the pre-change article, alongside the four regression gates below. +> Scope deltas vs. the section-7 sketches are documented in the PR bodies; the largest: +> the planned `data_qc_io` `units.py`/`metrics.py`/`align.py`/`pay.py` files were +> dropped because `petrolib.units`, `petrolib.ml_stats`, `petrolib.depth_matching`, +> and `petrolib.porosity_lithology` already covered them (`detect_bed_boundaries` +> lives in `data_qc_io.filt`). Corpus forms that are not bit-reproducible from the +> canonical API (e.g. `+1e-10`/`+1e-12` guard variants, scipy-kernel smoothers, +> legacy `RandomState` generators) deliberately remain local to their articles. + --- ## 1. Summary of the recommendation diff --git a/petrolib/data_qc_io/scale.py b/petrolib/data_qc_io/scale.py index fc0acb0..7dfe7e7 100644 --- a/petrolib/data_qc_io/scale.py +++ b/petrolib/data_qc_io/scale.py @@ -11,6 +11,8 @@ import numpy as np from numpy.typing import ArrayLike, NDArray +from .. import ml_stats + _Float = NDArray[np.float64] @@ -34,16 +36,17 @@ def normalize_to_reference( ``in_lo``/``in_hi`` are the input-well endpoints; when omitted they are taken as the ``pct`` percentiles of ``x`` (the Shier 5th/95th convention), or the min/max when ``pct=None`` (the image-histogram convention). No - clipping is applied. Sources: src2021_12/article01 (normalize_reference), - src2016_12/article6 (histogram_normalize), src2015_04/article4 - (histogram_scale). + clipping is applied. The affine map itself is + :func:`petrolib.ml_stats.affine_rescale` (bit-identical arrangement). + Sources: src2021_12/article01 (normalize_reference), src2016_12/article6 + (histogram_normalize), src2015_04/article4 (histogram_scale). """ v = _arr(x) if in_lo is None: in_lo = float(np.percentile(v, pct[0])) if pct is not None else float(np.min(v)) if in_hi is None: in_hi = float(np.percentile(v, pct[1])) if pct is not None else float(np.max(v)) - return np.asarray(ref_lo + (ref_hi - ref_lo) * (v - in_lo) / (in_hi - in_lo)) + return ml_stats.affine_rescale(v, src_lo=in_lo, src_hi=in_hi, dst_lo=ref_lo, dst_hi=ref_hi) def match_moments(x: ArrayLike, target_mean: float, target_std: float) -> _Float: