diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bfcb80..1856728 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,12 @@ on: permissions: contents: read +# Exact patches keep collection, workers and aggregation on the same interpreter. +# The official runtime manifest has newer 3.10 builds for Linux than macOS ARM64/Windows. +env: + CI_PYTHON_LINUX: '3.10.22' + CI_PYTHON_MACOS_WINDOWS: '3.10.11' + # Two test workers per platform. Each job has its own machine, checkout, # interpreter, locked environment, install destinations and temporary directories. # Collection and aggregation never pool coverage from different platforms. @@ -34,7 +40,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: '3.10' + python-version: ${{ matrix.os == 'ubuntu-24.04' && env.CI_PYTHON_LINUX || env.CI_PYTHON_MACOS_WINDOWS }} cache: pip cache-dependency-path: requirements-dev.lock - name: Install locked verification dependencies @@ -85,7 +91,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: '3.10' + python-version: ${{ matrix.os == 'ubuntu-24.04' && env.CI_PYTHON_LINUX || env.CI_PYTHON_MACOS_WINDOWS }} cache: pip cache-dependency-path: requirements-dev.lock - name: Install locked verification dependencies @@ -141,7 +147,7 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: - python-version: '3.10' + python-version: ${{ matrix.os == 'ubuntu-24.04' && env.CI_PYTHON_LINUX || env.CI_PYTHON_MACOS_WINDOWS }} cache: pip cache-dependency-path: requirements-dev.lock - name: Install locked verification dependencies diff --git a/LESSONS/20261005T175124Z-SPECTRUM-FITTING-001-coordinator-prominence-interval.md b/LESSONS/20261005T175124Z-SPECTRUM-FITTING-001-coordinator-prominence-interval.md new file mode 100644 index 0000000..347c2fc --- /dev/null +++ b/LESSONS/20261005T175124Z-SPECTRUM-FITTING-001-coordinator-prominence-interval.md @@ -0,0 +1,18 @@ +# Sequence prominence can hide a scalar-validation defect + +- ID: 20261005T175124Z-SPECTRUM-FITTING-001-coordinator-prominence-interval +- Date: 2026-10-05T17:51:24Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: SciPy accepts a two-value prominence sequence as an interval. A + nominal invalid-scalar test using an interval that removes every peak can pass + through the separate no-peaks ValueError even if the fitter ignores scalar + validation. +- Evidence: B round 1 found this in S23; the historical A report records its + correction. A source-free SciPy 1.18.1 probe with + `x=linspace(-5,5,101); y=3*exp(-x*x/2)` returned zero peaks for `[1,2]` and one + for `[0,10]`. The [primary definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.find_peaks.html) + documents interval prominence. No product mutation was needed. +- Lesson: For scalar-only wrapper validation, choose a sequence that the + underlying API would accept and that preserves an otherwise valid peak. Keep + unrelated sample, shape, ordering, and discovery preconditions valid. diff --git a/LESSONS/20261005T183608Z-SPECTRUM-FITTING-001-coordinator-nested-base-tools.md b/LESSONS/20261005T183608Z-SPECTRUM-FITTING-001-coordinator-nested-base-tools.md new file mode 100644 index 0000000..26fb659 --- /dev/null +++ b/LESSONS/20261005T183608Z-SPECTRUM-FITTING-001-coordinator-nested-base-tools.md @@ -0,0 +1,9 @@ +# A copied virtual environment can retain an unsuitable base interpreter + +- ID: 20261005T183608Z-SPECTRUM-FITTING-001-coordinator-nested-base-tools +- Date: 2026-10-05T18:36:08Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: A copied locked Python 3.13 environment passed dependency and copied-nested preflight checks, but real system-site nested environments inherited the original Conda base's Black 23.3.0 and unaudited startup inputs. The required Black version was 26.5.1. +- Evidence: Candidate dd31c2c237ac119d1ba1a3008f6b16e7e48b41f8 full receipt full-w9crt1ji reported 18 verification-reuse failures, including 12 Black version mismatches. The unchanged verification-reuse tests passed all 59 cases on a disposable non-Conda CPython 3.12.14 base with the same applicable hashed lock; tools/verification_reuse.py measured 78/78 statements and 46/46 branches, without exclusions. A real nested system-site environment inherited pinned tools and installed/imported a wheel. +- Lesson: Before an expensive full run, verify the actual base interpreter, its startup inputs, and the standard nested-environment creation path used by tests. Matching the selected environment's pins and a copied-nested probe does not establish those properties. Preserve the original environment and fix a disposable verification environment rather than weakening cache eligibility or version checks. diff --git a/LESSONS/20261005T185328Z-SPECTRUM-FITTING-001-coordinator-positive-width-difference-step.md b/LESSONS/20261005T185328Z-SPECTRUM-FITTING-001-coordinator-positive-width-difference-step.md new file mode 100644 index 0000000..59c2fff --- /dev/null +++ b/LESSONS/20261005T185328Z-SPECTRUM-FITTING-001-coordinator-positive-width-difference-step.md @@ -0,0 +1,9 @@ +# A positive physical width can yield an unrepresentable difference step + +- ID: 20261005T185328Z-SPECTRUM-FITTING-001-coordinator-positive-width-difference-step +- Date: 2026-10-05T18:53:28Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: The smallest positive float is an approved positive finite width, but halving it for a central-difference step rounds to zero. Gaussian and Lorentzian public fits leaked LinAlgError during uncertainty calculation, even after every new-helper statement and branch had been measured by ordinary fixtures. +- Evidence: Candidate c7ee4099e5276c9218a2d5bd90891ba7f017c6c2; clean Python 3.12.14. A 309-case focused diagnostic measured the helper's 155/155 statements and 66/66 branches. A separate public call with x=linspace(-1,1,41), y[20]=2 and other samples zero, full guess [2,0,nextafter(0,1)], and no background returned LinAlgError for both profiles. Inputs stayed unchanged. R3 accepts a verified result or RuntimeError for numerical failure, so the leaked ValueError subclass is a product defect. Raw public probe and interrupted full evidence are retained in the task conversation's external evidence directory. +- Lesson: Check representability of numerical perturbations as well as physical parameter positivity. Exercise valid domain limits independently, preserve numerical warnings, and honor the public failure-type boundary. Complete measured branch coverage does not establish behavior at every floating-point magnitude. diff --git a/LESSONS/20261005T200454Z-SPECTRUM-FITTING-001-coordinator-numerical-observer-boundaries.md b/LESSONS/20261005T200454Z-SPECTRUM-FITTING-001-coordinator-numerical-observer-boundaries.md new file mode 100644 index 0000000..3b11438 --- /dev/null +++ b/LESSONS/20261005T200454Z-SPECTRUM-FITTING-001-coordinator-numerical-observer-boundaries.md @@ -0,0 +1,19 @@ +# Numerical observers must preserve permitted implementations + +- ID: 20261005T200454Z-SPECTRUM-FITTING-001-coordinator-numerical-observer-boundaries +- Date: 2026-10-05T20:04:54Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: Dependency-failure observers rejected valid stacked/complex SVD + operands, and a covariance observer assumed float64 despite no output-dtype + contract. Real physical outputs do not require real-only internal operands. +- Evidence: B6 reproduced valid stacked `(1,201,3)` rejection; the coordinator's + complex128 decomposition matched analytic real covariance at rtol1e-12 while + the old observer rejected it. B7 accepted corrected tests SHA-256 + 50bb168312355b5c3035f534195260f4a0081a6f171cdd6a6de52235774837dd + after46 independent checks. Native wider-range execution was unavailable on + this ARM host; higher-range reasoning remains explicitly distinguished from + native execution in the historical test report. +- Lesson: Fault observers should constrain the public contract rather than + numerical backend representation. Determine representability from the actual + output dtype. Check legitimate alternate algorithms as well as wrong outputs. diff --git a/LESSONS/20261005T205314Z-SPECTRUM-FITTING-001-coordinator-background-covariance-rounding.md b/LESSONS/20261005T205314Z-SPECTRUM-FITTING-001-coordinator-background-covariance-rounding.md new file mode 100644 index 0000000..49ce2b5 --- /dev/null +++ b/LESSONS/20261005T205314Z-SPECTRUM-FITTING-001-coordinator-background-covariance-rounding.md @@ -0,0 +1,9 @@ +# Large background offsets can corrupt finite covariance + +- ID: 20261005T205314Z-SPECTRUM-FITTING-001-coordinator-background-covariance-rounding +- Date: 2026-10-05T20:53:14Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: A well-conditioned Gaussian fit with a constant background of 1e12 data units returned finite covariance with sigma variance about 15.3 times too small and no warning, despite complete measured coverage. +- Evidence: Independent D public reproduction on commit820e17dc31d9121b7b1ff2f5c839c0e77d8ad70f, /private/tmp/dphtools-spectrum-D-review-bxbulkvc/covariance_probe.py; coordinator reproduction exit1. Expected sigma variance5.77897e-7 x-unit squared; returned3.77959e-8. Normalized Jacobian condition3.257. Zero-background control agrees. Evidence applies to local CPython3.12.14/macOS ARM; hosted results are not established. +- Lesson: For covariance regressions, vary the background magnitude and compare the complete physical covariance with independently derived derivatives and actual RSS/(N-P). Finiteness and rank alone do not establish derivative accuracy after subtraction of large offsets. Preserve permitted numerical failure rather than accepting misleading finite uncertainty. diff --git a/LESSONS/20261005T214425Z-SPECTRUM-FITTING-001-coordinator-physical-acceptance-scope.md b/LESSONS/20261005T214425Z-SPECTRUM-FITTING-001-coordinator-physical-acceptance-scope.md new file mode 100644 index 0000000..ea50925 --- /dev/null +++ b/LESSONS/20261005T214425Z-SPECTRUM-FITTING-001-coordinator-physical-acceptance-scope.md @@ -0,0 +1,9 @@ +# Numerical stress validity does not establish physical acceptance scope + +- ID: 20261005T214425Z-SPECTRUM-FITTING-001-coordinator-physical-acceptance-scope +- Date: 2026-10-05T21:44:25Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: A covariance regression using a 1e12-data-unit offset, a height-three peak and noise around 0.01 data units was mathematically resolved but did not establish a physically representative measurement. The owner excluded this extreme offset from required acceptance. +- Evidence: Historical checkpoint e5bc47e1448c5466f981cfb4dd29150e8539552f has two failing extreme-offset cases; public contract R4 records the owner's physical-plausibility clarification and retains ordinary fit/covariance requirements. The original numerical finding remains recorded in the 20261005T205314Z background-covariance lesson. +- Lesson: Establish units, background provenance and a plausible noise model before making a stress fixture a required scientific acceptance case. Keep robustness checks distinct from representative fit and uncertainty checks; a local covariance oracle does not validate repeated-measurement confidence intervals. An owner scope correction must retain the original evidence and receive fresh test review. diff --git a/LESSONS/20261005T222923Z-SPECTRUM-FITTING-001-coordinator-physical-unit-default-fit.md b/LESSONS/20261005T222923Z-SPECTRUM-FITTING-001-coordinator-physical-unit-default-fit.md new file mode 100644 index 0000000..6104b7a --- /dev/null +++ b/LESSONS/20261005T222923Z-SPECTRUM-FITTING-001-coordinator-physical-unit-default-fit.md @@ -0,0 +1,9 @@ +# Physical unit conversion can expose an unfinished default fit + +- ID: 20261005T222923Z-SPECTRUM-FITTING-001-coordinator-physical-unit-default-fit +- Date: 2026-10-05T22:29:23Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: The resolved R4 spectrum fitted correctly in nanometres, but default LM returned the initial peak guesses as successful when the same coordinates and widths were converted to metres. Its covariance still matched the local recipe at the unfinished parameters. +- Evidence: Candidate f63ebf2de3fc65c047b00a3631f84b8bf0c68437 passed 2118 tests and full owned coverage. Independent D2 public reproduction found RSS 1.19075 V² in metres versus 0.0170722 V² for nanometre/default and metre/realcustom fits; a feasible generating model had RSS 0.0172294 V². The observed center perturbation was 14.9 nm against a 0.85 nm starting width. The narrow public unit-gap packet and complete independent receipts remain in the task conversation. +- Lesson: Include physically identical unit-converted inputs in scientific acceptance. Verify fit quality and stationarity alongside covariance; a solver success flag and mathematically consistent local covariance do not establish that optimization finished. diff --git a/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-measured-gap-witness.md b/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-measured-gap-witness.md new file mode 100644 index 0000000..30dc6e1 --- /dev/null +++ b/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-measured-gap-witness.md @@ -0,0 +1,9 @@ +# Confirm that a public witness reaches the measured coverage gap + +- ID: 20261006T054452Z-SPECTRUM-FITTING-001-coordinator-measured-gap-witness +- Date: 2026-10-06T05:44:52Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: Two public numerical-covariance failures had different outcomes. The first supplied witness exercised a nonfinite Jacobian but left the originally missing statement and branch unmeasured. +- Evidence: C3 full receipt full-hq3ox68t at candidate 1c72cadf8f31a7a5925b06eb7183aa5319e5fb52: 3338/3339 statements and 857/858 branches; retained focused2 coverage still missed _spectrum_fit.py line67/branch66→67, while steps-boundary-private-measurement.json and focused3 confirmed that outcome was exercised. +- Lesson: Before handing a blind test author a semantic witness, privately confirm that the public reproduction exercises the actual missing outcome. Keep implementation and raw measurement outside the blind packet. diff --git a/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-warning-provenance.md b/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-warning-provenance.md new file mode 100644 index 0000000..a71a1eb --- /dev/null +++ b/LESSONS/20261006T054452Z-SPECTRUM-FITTING-001-coordinator-warning-provenance.md @@ -0,0 +1,9 @@ +# Keyword matching does not establish numerical warning relevance + +- ID: 20261006T054452Z-SPECTRUM-FITTING-001-coordinator-warning-provenance +- Date: 2026-10-06T05:44:52Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: A test diagnostic classifier accepted display-only warnings containing fit/optimizer words and rejected conventional informative linear-algebra failures. +- Evidence: B13 public review /private/tmp/dphtools-spectrum-B17-sqf9i07c/review.md and independent-diagnosis-diagnostics.json: Fit legend unavailable and Optimizer progress display failed were accepted, while Singular matrix and SVD did not converge were rejected. Tests checkpoint 173f8958ff2d2fc35508226143c83876197a80b755254bc0924dc4fc1c56261d was not accepted. +- Lesson: Test warning preservation through known caller-controlled numerical provenance when the contract permits it. Do not claim a generic English substring classifier establishes arbitrary diagnostic relevance; review the actual numerical context separately. diff --git a/LESSONS/20261006T155643Z-SPECTRUM-FITTING-001-coordinator-python-patch-skew.md b/LESSONS/20261006T155643Z-SPECTRUM-FITTING-001-coordinator-python-patch-skew.md new file mode 100644 index 0000000..5842d74 --- /dev/null +++ b/LESSONS/20261006T155643Z-SPECTRUM-FITTING-001-coordinator-python-patch-skew.md @@ -0,0 +1,9 @@ +# Floating Python patches can split one CI workflow + +- ID: 20261006T155643Z-SPECTRUM-FITTING-001-coordinator-python-patch-skew +- Date: 2026-10-06T15:56:43Z +- Task/role: SPECTRUM-FITTING-001 / coordinator +- Status: confirmed +- Observation: Separate Linux jobs requesting Python `3.10` resolved to 3.10.21 and 3.10.22 in the same workflow, causing exact manifest validation to reject a worker before tests ran. +- Evidence: [Run 37483079692](https://github.com/david-hoffman/dphtools/actions/runs/37483079692), candidate `0634499296e143bab6a16daba3f504b5c6e4391d`: collection manifest and failed worker/aggregation receipts differ only at `environment/version`. `tools/verification_shards.py` also seals the workflow run and attempt. +- Lesson: Pin one supported exact interpreter patch per platform consistently across collection, workers and aggregation. Preserve identity checks. Run a fresh complete workflow after repair; failed-job-only retries cannot reuse a collection sealed to an earlier attempt. diff --git a/docs/examples/spectrum-fitting/MULTI-PEAK.md b/docs/examples/spectrum-fitting/MULTI-PEAK.md new file mode 100644 index 0000000..1ade6c8 --- /dev/null +++ b/docs/examples/spectrum-fitting/MULTI-PEAK.md @@ -0,0 +1,85 @@ +# More peaks and partial overlap + +These are three synthetic voltage spectra fitted through the public `spectrum_fit` function. They extend the [single-peak examples](README.md). Each spectrum uses one profile family, a linear background, and the default Levenberg–Marquardt optimizer. These plots are human gut checks, not acceptance tests. + +![Six Gaussian, seven Lorentzian and eight Voigt peaks, with decompositions and residuals](multi-peak-overview.png) + +## What is simulated + +Wavelengths span 500–530 nm, sampled every 0.025 nm (1,201 point samples). Component peak heights range from 0.40 to 1.45 V. Widths and heights vary within each spectrum. The linear background starts at 0.22–0.30 V and has a slope of 0.006–0.009 V/nm. Independent additive Gaussian read noise has standard deviation 0.01 V. These are generic simulated detector signals; they do not describe a particular instrument or chemical species. + +| Example | Components | Starting information | Residual RMS (V) | +|---|---:|---|---:| +| gauss | 6 | Automatic detection; prominence 0.12 V, distance 40 samples (1 nm) | 0.010417 | +| lorentz | 7 | Seven centers, displaced by 0.08–0.12 nm; heights/widths estimated by the fitter | 0.009723 | +| voigt | 8 | Eight full rows; centers displaced by 0.08–0.10 nm, heights/widths perturbed by 10–15% | 0.009654 | + +RMS means root mean square. All three calls returned without warnings and preserved the input arrays. Each fit returned the illustrated component count. The starting guesses in the last two cases are deliberately close to the generating parameters, so these examples show local fitting with informed guesses; they do not establish a large basin of convergence. + +## How to read the plots + +The top panel shows grey noisy samples, black dashed noiseless truth, and the blue returned total fit. The middle panel shows individual components: solid colored curves are fitted components; dashed curves in the same colors are generating components. The dark dash-dot line is the fitted background. Components are drawn above zero, without adding the background to each one. The bottom panel shows blue data-minus-fit residuals and the black noiseless-truth-minus-fit curve. The shaded ±0.01 V band is a noise-scale guide, not a confidence band. + +Amplitude is the height of an individual component in V, not its area or the total signal at its center. Gaussian sigma is its standard deviation; Lorentzian gamma is its half width at half maximum. Voigt uses both, in nm. + +Automatic detection looks for local maxima. Overlapping components may not create separate maxima. The Voigt spectrum contains eight generating components but only six broad visible features; the eight-component fit is supplied with all eight initial parameter rows. A small total residual does not by itself establish a unique or accurate decomposition. Compare the solid and dashed individual components, especially near 510 nm. + +## Detailed plots and parameters + +Parameter entries below are **generating truth → returned fit**, sorted by fitted center. They describe these recorded examples; they are not acceptance tolerances or confidence intervals. + +### Six Gaussian peaks: automatic detection + +![Six Gaussian peaks: automatic detection, including individual components and residuals](gauss-six.png) + +| Peak | Height (V) | Center (nm) | Sigma (nm) | +| --- | --- | --- | --- | +| 1 | 1.2000 → 1.2023 | 502.0000 → 501.9999 | 0.2500 → 0.2508 | +| 2 | 0.5500 → 0.5481 | 506.0000 → 505.9999 | 0.5000 → 0.5005 | +| 3 | 0.9000 → 0.9035 | 510.0000 → 509.9987 | 0.3500 → 0.3493 | +| 4 | 0.6500 → 0.6528 | 514.0000 → 513.9997 | 0.7000 → 0.7017 | +| 5 | 1.4500 → 1.4497 | 520.0000 → 520.0000 | 0.4000 → 0.3994 | +| 6 | 0.4500 → 0.4488 | 526.0000 → 525.9970 | 0.5500 → 0.5484 | + +Background at 500 nm: 0.2500 → 0.2485 V. Slope: 0.00900 → 0.00909 V/nm. + +### Seven Lorentzian peaks: center guesses + +![Seven Lorentzian peaks: center guesses, including individual components and residuals](lorentz-seven.png) + +| Peak | Height (V) | Center (nm) | Gamma (nm) | +| --- | --- | --- | --- | +| 1 | 0.7000 → 0.7071 | 501.8000 → 501.8018 | 0.3200 → 0.3169 | +| 2 | 1.2000 → 1.1979 | 505.2000 → 505.1997 | 0.5000 → 0.5022 | +| 3 | 0.8000 → 0.8009 | 508.4000 → 508.4005 | 0.6200 → 0.6230 | +| 4 | 0.5000 → 0.5019 | 510.0000 → 510.0055 | 0.5500 → 0.5505 | +| 5 | 1.0500 → 1.0493 | 515.5000 → 515.5022 | 0.3500 → 0.3521 | +| 6 | 0.7000 → 0.7011 | 520.5000 → 520.5005 | 0.6000 → 0.6006 | +| 7 | 0.4000 → 0.4053 | 526.2000 → 526.2025 | 0.4000 → 0.3996 | + +Background at 500 nm: 0.3000 → 0.2987 V. Slope: 0.00600 → 0.00599 V/nm. + +### Eight Voigt peaks: overlapping pairs with full guesses + +![Eight Voigt peaks: overlapping pairs with full guesses, including individual components and residuals](voigt-eight.png) + +| Peak | Height (V) | Center (nm) | Sigma (nm) | Gamma (nm) | +| --- | --- | --- | --- | --- | +| 1 | 1.0500 → 1.0492 | 502.0000 → 501.9980 | 0.2500 → 0.2530 | 0.1500 → 0.1428 | +| 2 | 0.6500 → 0.6461 | 506.0000 → 506.0011 | 0.4000 → 0.4082 | 0.2200 → 0.2073 | +| 3 | 0.8500 → 0.7710 | 509.7000 → 509.6656 | 0.3000 → 0.2773 | 0.2400 → 0.2538 | +| 4 | 0.5500 → 0.6010 | 510.4500 → 510.3795 | 0.3500 → 0.3975 | 0.2000 → 0.1808 | +| 5 | 0.9500 → 0.9472 | 516.0000 → 515.9988 | 0.6000 → 0.6090 | 0.2200 → 0.2065 | +| 6 | 0.7000 → 0.7016 | 520.2000 → 520.1956 | 0.3500 → 0.3332 | 0.1800 → 0.1937 | +| 7 | 0.4500 → 0.4574 | 521.0000 → 520.9914 | 0.3000 → 0.2921 | 0.2000 → 0.2139 | +| 8 | 1.2000 → 1.1968 | 526.0000 → 525.9981 | 0.4500 → 0.4548 | 0.2500 → 0.2456 | + +Background at 500 nm: 0.2200 → 0.2225 V. Slope: 0.00800 → 0.00790 V/nm. + +## Data and provenance + +Generated using runtime source candidate `576602a6497ce1b893c2adce61c8a0a56136a592` with CPython 3.12.14, NumPy 2.5.3, SciPy 1.18.1 and Matplotlib 3.11.2. Product code and numerical tests are unchanged. The public numerical conventions are documented in the [approved contract](../../tasks/SPECTRUM-FITTING-001-contract.md). + +[multi-peak-data.npz](multi-peak-data.npz) stores each example’s wavelengths, samples, generating peaks/background, noise, supplied guesses where applicable, returned parameters, fitted signals, residuals and full covariance in physical units. Keys begin with `gauss_six_`, `lorentz_seven_` or `voigt_eight_`. + +[multi-peak-manifest.json](multi-peak-manifest.json) records all generating parameters, fixed random seeds, starting guesses, solver options, returned values, warnings, library versions, runtime hashes and asset SHA-256 hashes. Covariance is a local approximation; these examples make no uncertainty-calibration claim. diff --git a/docs/examples/spectrum-fitting/README.md b/docs/examples/spectrum-fitting/README.md new file mode 100644 index 0000000..3ad1579 --- /dev/null +++ b/docs/examples/spectrum-fitting/README.md @@ -0,0 +1,79 @@ +# Spectrum fitting: visual examples + +These simulated spectra and returned fits are visual gut checks, not acceptance tests. Open [spectrum-review.html](spectrum-review.html) locally for the eight figures and six parameter tables. The page uses relative asset links and needs no server. + +Each single-peak case below contains one positive Gaussian, Lorentzian, or Voigt peak with a constant or linear background. The component peak height is 1.2 V; the background starts at 0.4 V at 500 nm. The linear background adds 0.015 V/nm. Samples span 500–510 nm at 0.05 nm intervals, with independent additive read noise of standard deviation 0.01 V. + +The same simulated voltage samples and physically equivalent starting guesses were fitted with wavelength coordinates supplied in nanometres (nm) or metres (m). All plots and parameter tables display wavelengths and widths in nm, signals in V, and slopes in V/nm. + +- Grey dots: simulated measured voltage samples. +- Black dashed signal curve: the known noiseless generating model. +- Blue: default Levenberg–Marquardt (LM) fit using nm coordinates. +- Red: default LM fit using m coordinates. It remains plotted where it overlaps the other fits. +- Green dotted: a configured custom solver using m coordinates, with SciPy's Trust Region Reflective method. + +Residuals are measured data minus returned fit. The black residual curve is noiseless truth minus returned fit. Shading at ±0.01 V is a read-noise reference, not a confidence band. Gaussian sigma is a standard deviation; Lorentzian gamma is a half width at half maximum; Voigt uses both widths. + +## More peaks and partial overlap + +[Multi-peak examples](MULTI-PEAK.md) show six Gaussian peaks found automatically, seven Lorentzian peaks fitted from center guesses, and eight Voigt peaks fitted from full parameter guesses. They include varied peak heights/widths, sloping backgrounds, individual fitted and true components, and residuals. The crowded Voigt pairs illustrate why a good total fit alone does not establish an accurate decomposition. Each fit uses the existing default optimizer and nm coordinates. + +![Multi-peak spectra, individual components and residuals](multi-peak-overview.png) + +## Overviews + +![Gaussian, Lorentzian, and Voigt fits with constant backgrounds](constant-overview.png) + +![Gaussian, Lorentzian, and Voigt fits with linear backgrounds](linear-overview.png) + +## Individual comparisons + +
+Gaussian: constant background + +![Gaussian fits with a constant background](gauss_constant-detail.png) + +
+ +
+Gaussian: linear background + +![Gaussian fits with a linear background](gauss_linear-detail.png) + +
+ +
+Lorentzian: constant background + +![Lorentzian fits with a constant background](lorentz_constant-detail.png) + +
+ +
+Lorentzian: linear background + +![Lorentzian fits with a linear background](lorentz_linear-detail.png) + +
+ +
+Voigt: constant background + +![Voigt fits with a constant background](voigt_constant-detail.png) + +
+ +
+Voigt: linear background + +![Voigt fits with a linear background](voigt_linear-detail.png) + +
+ +## Data and provenance + +The source runtime product commit is `f3a655709920f6c7f13a392c5c6265ae8d2f045e`. The plots were generated from preparation `1c72cadf8f31a7a5925b06eb7183aa5319e5fb52`, before later tests/docs-only corrections. The preserved image and numerical-data bytes were not regenerated for later candidates. + +The recorded runtime was CPython 3.12.14, NumPy 2.5.3, SciPy 1.18.1, and Matplotlib 3.11.2. All 18 recorded public fitting calls returned fits with no numerical warnings. These examples do not establish covariance accuracy or replace the numerical verification gate. + +[spectrum-review-data.npz](spectrum-review-data.npz) contains the samples, truth, initial guesses, returned parameters, fitted signals, residuals, and covariance arrays in each call's physical units. [spectrum-review-manifest.json](spectrum-review-manifest.json) records units, provenance, and SHA-256 hashes. diff --git a/docs/examples/spectrum-fitting/constant-overview.png b/docs/examples/spectrum-fitting/constant-overview.png new file mode 100644 index 0000000..2f08319 Binary files /dev/null and b/docs/examples/spectrum-fitting/constant-overview.png differ diff --git a/docs/examples/spectrum-fitting/gauss-six.png b/docs/examples/spectrum-fitting/gauss-six.png new file mode 100644 index 0000000..5efa8ba Binary files /dev/null and b/docs/examples/spectrum-fitting/gauss-six.png differ diff --git a/docs/examples/spectrum-fitting/gauss_constant-detail.png b/docs/examples/spectrum-fitting/gauss_constant-detail.png new file mode 100644 index 0000000..9314e84 Binary files /dev/null and b/docs/examples/spectrum-fitting/gauss_constant-detail.png differ diff --git a/docs/examples/spectrum-fitting/gauss_linear-detail.png b/docs/examples/spectrum-fitting/gauss_linear-detail.png new file mode 100644 index 0000000..fd9b63a Binary files /dev/null and b/docs/examples/spectrum-fitting/gauss_linear-detail.png differ diff --git a/docs/examples/spectrum-fitting/linear-overview.png b/docs/examples/spectrum-fitting/linear-overview.png new file mode 100644 index 0000000..3a801b4 Binary files /dev/null and b/docs/examples/spectrum-fitting/linear-overview.png differ diff --git a/docs/examples/spectrum-fitting/lorentz-seven.png b/docs/examples/spectrum-fitting/lorentz-seven.png new file mode 100644 index 0000000..1cc587c Binary files /dev/null and b/docs/examples/spectrum-fitting/lorentz-seven.png differ diff --git a/docs/examples/spectrum-fitting/lorentz_constant-detail.png b/docs/examples/spectrum-fitting/lorentz_constant-detail.png new file mode 100644 index 0000000..74a5e70 Binary files /dev/null and b/docs/examples/spectrum-fitting/lorentz_constant-detail.png differ diff --git a/docs/examples/spectrum-fitting/lorentz_linear-detail.png b/docs/examples/spectrum-fitting/lorentz_linear-detail.png new file mode 100644 index 0000000..49ebc0e Binary files /dev/null and b/docs/examples/spectrum-fitting/lorentz_linear-detail.png differ diff --git a/docs/examples/spectrum-fitting/multi-peak-data.npz b/docs/examples/spectrum-fitting/multi-peak-data.npz new file mode 100644 index 0000000..21558f0 Binary files /dev/null and b/docs/examples/spectrum-fitting/multi-peak-data.npz differ diff --git a/docs/examples/spectrum-fitting/multi-peak-manifest.json b/docs/examples/spectrum-fitting/multi-peak-manifest.json new file mode 100644 index 0000000..4893bd7 --- /dev/null +++ b/docs/examples/spectrum-fitting/multi-peak-manifest.json @@ -0,0 +1,474 @@ +{ + "purpose": "Human visual examples, not acceptance tests or a new scientific contract", + "source_candidate": "576602a6497ce1b893c2adce61c8a0a56136a592", + "python": "3.12.14", + "numpy": "2.5.3", + "scipy": "1.18.1", + "matplotlib": "3.11.2", + "generator_sha256": "b05b9bcf908e46446132b65f4537016f50110b6391b5d0bb87149d23540bffef", + "runtime_sha256": { + "dphtools/utils/_spectrum_fit.py": "1080b1f806f9e7faf27ef64df56e1c525f3dd26a8993ad7ef8f3136c84c09587", + "dphtools/utils/fitfuncs.py": "c56a772ea0c2cd81d8225b3e6fec29b3106a10ba481b5d566f0561c5ee674421" + }, + "coordinate_units": "nm", + "peak_height_units": "V", + "width_units": "nm", + "background_slope_units": "V/nm", + "covariance_parameter_order": "Flattened sorted peak rows, then b0 and b1; physical units", + "noise_model": "Independent additive Gaussian read noise, standard deviation 0.01 V", + "cases": [ + { + "id": "gauss-six", + "title": "Six Gaussian peaks \u00b7 automatic detection", + "family": "gauss", + "mode": "automatic", + "seed": 260601, + "background": [ + 0.25, + 0.009 + ], + "peaks": [ + [ + 1.2, + 502.0, + 0.25 + ], + [ + 0.55, + 506.0, + 0.5 + ], + [ + 0.9, + 510.0, + 0.35 + ], + [ + 0.65, + 514.0, + 0.7 + ], + [ + 1.45, + 520.0, + 0.4 + ], + [ + 0.45, + 526.0, + 0.55 + ] + ], + "controls": { + "prominence": 0.12, + "distance": 40 + }, + "input_x_unit": "nm", + "signal_unit": "V", + "samples": 1201, + "noise_standard_deviation_V": 0.01, + "sample_spacing_nm": 0.025, + "optimizer": "default Levenberg-Marquardt", + "options": { + "peak_type": "gauss", + "background": "linear", + "max_nfev": 10000, + "prominence": 0.12, + "distance": 40 + }, + "supplied_guesses": null, + "returned_components": 6, + "returned_peaks": [ + [ + 1.2022628775779227, + 501.999880395365, + 0.250807683580589 + ], + [ + 0.5481254720304749, + 505.99989152504946, + 0.5004570656946697 + ], + [ + 0.9034708532399321, + 509.99866245384334, + 0.34925050477010433 + ], + [ + 0.6528467500267656, + 513.9996755745086, + 0.7016933746538574 + ], + [ + 1.4497051441375794, + 519.9999918459664, + 0.399442001702854 + ], + [ + 0.44875396967205244, + 525.9970388275431, + 0.5484169192837036 + ] + ], + "returned_background": [ + 0.2485049322328966, + 0.009093081855434445 + ], + "residual_rms_V": 0.010416617923019409, + "truth_minus_fit_rms_V": 0.001289187062303909, + "warnings": [], + "caller_inputs_preserved": true + }, + { + "id": "lorentz-seven", + "title": "Seven Lorentzian peaks \u00b7 center guesses", + "family": "lorentz", + "mode": "centers", + "seed": 260602, + "background": [ + 0.3, + 0.006 + ], + "peaks": [ + [ + 0.7, + 501.8, + 0.32 + ], + [ + 1.2, + 505.2, + 0.5 + ], + [ + 0.8, + 508.4, + 0.62 + ], + [ + 0.5, + 510.0, + 0.55 + ], + [ + 1.05, + 515.5, + 0.35 + ], + [ + 0.7, + 520.5, + 0.6 + ], + [ + 0.4, + 526.2, + 0.4 + ] + ], + "center_offsets": [ + 0.12, + -0.1, + 0.08, + -0.09, + 0.1, + -0.12, + 0.08 + ], + "input_x_unit": "nm", + "signal_unit": "V", + "samples": 1201, + "noise_standard_deviation_V": 0.01, + "sample_spacing_nm": 0.025, + "optimizer": "default Levenberg-Marquardt", + "options": { + "peak_type": "lorentz", + "background": "linear", + "max_nfev": 10000 + }, + "supplied_guesses": [ + 501.92, + 505.09999999999997, + 508.47999999999996, + 509.91, + 515.6, + 520.38, + 526.2800000000001 + ], + "returned_components": 7, + "returned_peaks": [ + [ + 0.7070883532098078, + 501.801842241305, + 0.316856716123924 + ], + [ + 1.1979357459431048, + 505.19967291329993, + 0.5022315051035935 + ], + [ + 0.8008934691286242, + 508.40050675296504, + 0.6230489131768643 + ], + [ + 0.5019230042716843, + 510.0055108403805, + 0.5505429048615464 + ], + [ + 1.049287705101191, + 515.5022318136972, + 0.3521450025558068 + ], + [ + 0.7010556706300601, + 520.5005235726389, + 0.6006175236200281 + ], + [ + 0.4052540138032911, + 526.202472851775, + 0.3996151093348739 + ] + ], + "returned_background": [ + 0.29872652420310697, + 0.005993015211373636 + ], + "residual_rms_V": 0.00972257715999709, + "truth_minus_fit_rms_V": 0.001521491001216011, + "warnings": [], + "caller_inputs_preserved": true + }, + { + "id": "voigt-eight", + "title": "Eight Voigt peaks \u00b7 full guesses, overlapping pairs", + "family": "voigt", + "mode": "full", + "seed": 260603, + "background": [ + 0.22, + 0.008 + ], + "peaks": [ + [ + 1.05, + 502.0, + 0.25, + 0.15 + ], + [ + 0.65, + 506.0, + 0.4, + 0.22 + ], + [ + 0.85, + 509.7, + 0.3, + 0.24 + ], + [ + 0.55, + 510.45, + 0.35, + 0.2 + ], + [ + 0.95, + 516.0, + 0.6, + 0.22 + ], + [ + 0.7, + 520.2, + 0.35, + 0.18 + ], + [ + 0.45, + 521.0, + 0.3, + 0.2 + ], + [ + 1.2, + 526.0, + 0.45, + 0.25 + ] + ], + "center_offsets": [ + 0.08, + -0.1, + -0.08, + 0.1, + -0.1, + -0.08, + 0.09, + 0.1 + ], + "height_factors": [ + 0.85, + 1.1, + 0.9, + 1.15, + 1.1, + 0.85, + 1.15, + 0.9 + ], + "sigma_factors": [ + 1.15, + 0.85, + 1.1, + 0.9, + 0.9, + 1.1, + 0.9, + 1.15 + ], + "gamma_factors": [ + 0.9, + 1.1, + 0.85, + 1.15, + 1.15, + 0.9, + 1.1, + 0.85 + ], + "input_x_unit": "nm", + "signal_unit": "V", + "samples": 1201, + "noise_standard_deviation_V": 0.01, + "sample_spacing_nm": 0.025, + "optimizer": "default Levenberg-Marquardt", + "options": { + "peak_type": "voigt", + "background": "linear", + "max_nfev": 10000 + }, + "supplied_guesses": [ + [ + 0.8925, + 502.08, + 0.2875, + 0.135 + ], + [ + 0.7150000000000001, + 505.9, + 0.34, + 0.24200000000000002 + ], + [ + 0.765, + 509.62, + 0.33, + 0.204 + ], + [ + 0.6325, + 510.55, + 0.315, + 0.22999999999999998 + ], + [ + 1.045, + 515.9, + 0.54, + 0.253 + ], + [ + 0.595, + 520.12, + 0.385, + 0.162 + ], + [ + 0.5175, + 521.09, + 0.27, + 0.22000000000000003 + ], + [ + 1.08, + 526.1, + 0.5175, + 0.2125 + ] + ], + "returned_components": 8, + "returned_peaks": [ + [ + 1.0492265117918926, + 501.99801533874677, + 0.2530009722334274, + 0.14277601977990353 + ], + [ + 0.6460528184388374, + 506.00105261637975, + 0.4082319992653057, + 0.20731358782312012 + ], + [ + 0.7709951631087569, + 509.66560282153404, + 0.27725692842776767, + 0.2537522811887133 + ], + [ + 0.6009652402625933, + 510.37951165171484, + 0.39745781243248696, + 0.18083951900073553 + ], + [ + 0.9471946735067823, + 515.9988248783799, + 0.608995355168404, + 0.20654089199649794 + ], + [ + 0.7015911486907269, + 520.1955740868681, + 0.3332203667405117, + 0.19371160842665872 + ], + [ + 0.4573706889872254, + 520.9914194782311, + 0.29214400061626855, + 0.21389073066931463 + ], + [ + 1.19684007042165, + 525.998108045487, + 0.4547518070513012, + 0.24558944686237946 + ] + ], + "returned_background": [ + 0.222477298534763, + 0.007899813150747156 + ], + "residual_rms_V": 0.00965401158746813, + "truth_minus_fit_rms_V": 0.0017443179940453675, + "warnings": [], + "caller_inputs_preserved": true + } + ], + "assets_sha256": { + "gauss-six.png": "67657f4518658c397734961215a581195cb517b2964c4cc823997e7dccd9f384", + "lorentz-seven.png": "bd2e8d33218e0e1a3ce60dec1d9a3f15e5c3e5129b36a02fc4a3d17ea41e79e4", + "multi-peak-data.npz": "e877ff23fc4df372203a86b348c56f4adb32affc4c6f380fbf0f2537f1c5a959", + "multi-peak-overview.png": "af41138fb6ed488d8f492978575ecef2e73fd50f675ea0448e8bfaf296fd0e38", + "voigt-eight.png": "33c8d902a9e6e34940dfc100c050bc822e0f17c4cde8df95d9e3d773a96647d9" + } +} diff --git a/docs/examples/spectrum-fitting/multi-peak-overview.png b/docs/examples/spectrum-fitting/multi-peak-overview.png new file mode 100644 index 0000000..0799005 Binary files /dev/null and b/docs/examples/spectrum-fitting/multi-peak-overview.png differ diff --git a/docs/examples/spectrum-fitting/spectrum-review-data.npz b/docs/examples/spectrum-fitting/spectrum-review-data.npz new file mode 100644 index 0000000..2769b75 Binary files /dev/null and b/docs/examples/spectrum-fitting/spectrum-review-data.npz differ diff --git a/docs/examples/spectrum-fitting/spectrum-review-manifest.json b/docs/examples/spectrum-fitting/spectrum-review-manifest.json new file mode 100644 index 0000000..0c97fa7 --- /dev/null +++ b/docs/examples/spectrum-fitting/spectrum-review-manifest.json @@ -0,0 +1,196 @@ +{ + "format_version": 1, + "scope": "Preserved visual examples, not acceptance tests; no execution against later candidates is claimed.", + "source_runtime_product_commit": "f3a655709920f6c7f13a392c5c6265ae8d2f045e", + "plot_preparation_commit": "1c72cadf8f31a7a5925b06eb7183aa5319e5fb52", + "plot_preparation_tree": "cde1d245966abe6f9c27d7f2be3354ff5159b270", + "preparation_test_checkpoint_sha256": "b3fe64dc5da2bdb40e4af19832f39684507b77957ac7361dc789431af09069d0", + "public_entry_point": "dphtools.utils.fitfuncs.spectrum_fit", + "runtime": { + "python": "CPython 3.12.14", + "numpy": "2.5.3", + "scipy": "1.18.1", + "matplotlib": "3.11.2" + }, + "fixture": { + "case_count": 6, + "profiles": [ + "Gaussian", + "Lorentzian", + "Voigt" + ], + "backgrounds": [ + "constant", + "linear" + ], + "x_display_unit": "nm", + "x_input_units": [ + "nm", + "m" + ], + "signal_unit": "V", + "slope_display_unit": "V/nm", + "x_range_nm": [ + 500, + 510 + ], + "sample_spacing_nm": 0.05, + "component_height_V": 1.2, + "background_at_500_nm_V": 0.4, + "linear_slope_V_per_nm": 0.015, + "additive_read_noise_standard_deviation_V": 0.01 + }, + "recorded_calls": { + "count": 18, + "returned_fits": 18, + "numerical_warning_count": 0, + "colors": { + "blue": "default LM, nm input", + "red": "default LM, m input", + "green": "configured custom Trust Region Reflective solver, m input" + } + }, + "numerical_archive": { + "format": "NumPy compressed NPZ", + "array_count": 108, + "physical_covariance_arrays": 18, + "unit_notes": "Base fixtures and parameters_nm use V/nm display conventions. Covariance arrays retain physical parameter units of their original call: nm for lm_nm and m for lm_m/custom_m; slopes use V per input wavelength unit." + }, + "cases": [ + { + "key": "gauss_constant", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.7, + 0.4 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.009216093329704933, + "lm_m": 0.009216093329702935, + "custom_m": 0.009216093329702935 + } + }, + { + "key": "lorentz_constant", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.7, + 0.4 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.009680697963142887, + "lm_m": 0.009680697963143574, + "custom_m": 0.009680697963143583 + } + }, + { + "key": "voigt_constant", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.55, + 0.3, + 0.4 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.00946671278854328, + "lm_m": 0.00946671278854165, + "custom_m": 0.009466712788541644 + } + }, + { + "key": "gauss_linear", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.7, + 0.4, + 0.015 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.0091572258755378, + "lm_m": 0.009157225875535744, + "custom_m": 0.00915722587553518 + } + }, + { + "key": "lorentz_linear", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.7, + 0.4, + 0.015 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.009660429572419792, + "lm_m": 0.009660429572420469, + "custom_m": 0.009660429572420465 + } + }, + { + "key": "voigt_linear", + "truth_parameters_nm": [ + 1.2, + 505.1, + 0.55, + 0.3, + 0.4, + 0.015 + ], + "recorded_residual_rms_V": { + "lm_nm": 0.009462800888864384, + "lm_m": 0.009462800888862774, + "custom_m": 0.009462800888862786 + } + } + ], + "assets": { + "README.md": { + "bytes": 3542, + "sha256": "1f6ebe96c83a9c53e22d7c0e8be225d201c98b6935c8da34be85754d54a9593a" + }, + "constant-overview.png": { + "bytes": 484014, + "sha256": "1b6f631a6a0dc0b0d9c5a0a2184be2927512eebd7da9d38556824dbed8bdeecb" + }, + "gauss_constant-detail.png": { + "bytes": 286873, + "sha256": "eb91db13a8839500c647a07e91aebcb4e0d73bc81bdad7283b72909995c2c218" + }, + "gauss_linear-detail.png": { + "bytes": 295291, + "sha256": "1d60c33790f426edd39d20585dcc0a928488a8357745b6f30b8368ccd8e18210" + }, + "linear-overview.png": { + "bytes": 485171, + "sha256": "be854b77f941b4075672c2d9f793910d228c5d2d0eb48ad7a81b4c10f5f3525a" + }, + "lorentz_constant-detail.png": { + "bytes": 295453, + "sha256": "9ee5734b0c718834f1afaa10af4a4ac42d49711c9fdfd4d0d0674b520df50cf6" + }, + "lorentz_linear-detail.png": { + "bytes": 295732, + "sha256": "38344c36d7b2770ae65de1a0bbe615c67123a358c7f426046a26944fcfad2377" + }, + "spectrum-review-data.npz": { + "bytes": 114130, + "sha256": "8cd7acf84674a77386199e130826389b4c3bf9f4cdfa1fd5971ea049a5fe3b66" + }, + "spectrum-review.html": { + "bytes": 15037, + "sha256": "79c7c41cc07e22455065ad06071c2b094e8d38a868001438bea9b759718a2d2f" + }, + "voigt_constant-detail.png": { + "bytes": 297341, + "sha256": "4bffbf4f0004639f9f6fe7b4dcb3d4d9816b59e1d18356db1c52a00743fb5b7f" + }, + "voigt_linear-detail.png": { + "bytes": 300483, + "sha256": "580fcceb53cd192c950bed1f0170322581985b36d74ba2ee140616c96117f678" + } + } +} diff --git a/docs/examples/spectrum-fitting/spectrum-review.html b/docs/examples/spectrum-fitting/spectrum-review.html new file mode 100644 index 0000000..72b8740 --- /dev/null +++ b/docs/examples/spectrum-fitting/spectrum-review.html @@ -0,0 +1,36 @@ + + +Spectrum fitting: recorded visual examples +
+
SPECTRUM FITTING / SIMULATED MEASUREMENT / HUMAN REVIEW
+

Does the fit look right?

+

Six resolved, positive spectra: Gaussian, Lorentzian and Voigt, each with a constant or gently sloping background. +The component height is 1.2 V, the background starts at 0.4 V, and independent additive read noise has a 0.01 V standard deviation. +Samples are 0.05 nm apart, from 500 to 510 nm.

+

The same voltage samples and physically equivalent peak guesses are supplied in nanometres (nm) and metres (m). +Every displayed wavelength and returned width is converted to nm for comparison. The plotted signal includes the background; the component amplitude is its own height.

+
Recorded after-repair result: all eighteen calls returned fitted spectra.
+The recorded default Levenberg-Marquardt (LM) fits in nm and m, and the real custom solver in m, nearly coincide. +Red denotes default LM with metre input. All columns use the same measured voltage samples and equivalent starts. +These preserved examples are visual gut checks, not acceptance tests. They were generated during preparation 1c72cad, before later tests/docs-only corrections; they were not rerun for those later candidates.
+
+
1. Check the shape
Grey dots are the simulated measurement. The black dashed curve is the known noiseless truth. A good fit tracks that curve, rather than chasing individual noisy points.
+
2. Check the residuals
Compare both unit choices for noise around zero and any systematic shape. Detail plots also show the smooth noiseless-truth minus fit discrepancy.
+
3. Check units and values
Blue uses default LM with nm input. Red uses default LM with m input. Green uses a real custom solver with m input. The parameter tables all use V and nm.
+
+ +

Constant background

+
Three profiles on identical measurement scales. Blue, red and green nearly coincide; the red default-metre curve is still present beneath the overlapping curves.
Three profiles on identical measurement scales. Blue, red and green nearly coincide; the red default-metre curve is still present beneath the overlapping curves.

Linear background

The slope is 0.015 V/nm: only a 0.15 V increase across the full 10 nm range.

All three actual calls return fits for every profile, including Gaussian with a linear background. All residuals are plotted over their full range.
All three actual calls return fits for every profile, including Gaussian with a linear background. All residuals are plotted over their full range.

Individual fits and returned parameters

Expand a case to compare three actual calls side by side. Within each detailed case, all three residual panels use the same vertical scale. Red remains default LM with metre input; near-coincidence with blue and green is the observed result. The shaded +/-0.01 V band is a read-noise reference, not a confidence interval.

Gaussian / constant background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.1983061.1983061.198306
Centre (nm)505.1505.100374505.100374505.100374
Sigma (nm)0.70.7016560.7016560.701656
Background at 500 nm (V)0.40.399260.399260.39926
Residual root mean square (V)Reference only0.0092160.0092160.009216
Call outcomeGenerating modelReturned fitReturned fitReturned fit
Lorentzian / constant background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.200511.200511.20051
Centre (nm)505.1505.098714505.098714505.098714
Gamma, half width (nm)0.70.7022920.7022920.702292
Background at 500 nm (V)0.40.4001470.4001470.400147
Residual root mean square (V)Reference only0.0096810.0096810.009681
Call outcomeGenerating modelReturned fitReturned fitReturned fit
Voigt / constant background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.2028661.2028661.202866
Centre (nm)505.1505.099091505.099091505.099091
Sigma (nm)0.550.5345510.5345510.534551
Gamma, half width (nm)0.30.3244280.3244280.324428
Background at 500 nm (V)0.40.3978150.3978150.397815
Residual root mean square (V)Reference only0.0094670.0094670.009467
Call outcomeGenerating modelReturned fitReturned fitReturned fit
Gaussian / linear background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.1982661.1982661.198266
Centre (nm)505.1505.099951505.099951505.099951
Sigma (nm)0.70.7016070.7016070.701607
Background at 500 nm (V)0.40.3974620.3974620.397462
Background slope (V/nm)0.0150.0153640.0153640.015364
Residual root mean square (V)Reference only0.0091570.0091570.009157
Call outcomeGenerating modelReturned fitReturned fitReturned fit
Lorentzian / linear background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.2005341.2005341.200534
Centre (nm)505.1505.099013505.099013505.099013
Gamma, half width (nm)0.70.7023650.7023650.702365
Background at 500 nm (V)0.40.4012190.4012190.401219
Background slope (V/nm)0.0150.014780.014780.01478
Residual root mean square (V)Reference only0.009660.009660.00966
Call outcomeGenerating modelReturned fitReturned fitReturned fit
Voigt / linear background
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
Same samples and equivalent starts in every column. The black curve in the residual panels is noiseless truth minus fitted signal.
ParameterKnown truthDefault LM; input in nmDefault LM; input in mCustom solver; input in m
Height (V)1.21.2029111.2029111.202911
Centre (nm)505.1505.099221505.099221505.099221
Sigma (nm)0.550.5344450.5344450.534445
Gamma, half width (nm)0.30.3246280.3246280.324628
Background at 500 nm (V)0.40.3982660.3982660.398266
Background slope (V/nm)0.0150.0149040.0149040.014904
Residual root mean square (V)Reference only0.0094630.0094630.009463
Call outcomeGenerating modelReturned fitReturned fitReturned fit
\ No newline at end of file diff --git a/docs/examples/spectrum-fitting/voigt-eight.png b/docs/examples/spectrum-fitting/voigt-eight.png new file mode 100644 index 0000000..4bf2fcf Binary files /dev/null and b/docs/examples/spectrum-fitting/voigt-eight.png differ diff --git a/docs/examples/spectrum-fitting/voigt_constant-detail.png b/docs/examples/spectrum-fitting/voigt_constant-detail.png new file mode 100644 index 0000000..4153c58 Binary files /dev/null and b/docs/examples/spectrum-fitting/voigt_constant-detail.png differ diff --git a/docs/examples/spectrum-fitting/voigt_linear-detail.png b/docs/examples/spectrum-fitting/voigt_linear-detail.png new file mode 100644 index 0000000..3f9acbe Binary files /dev/null and b/docs/examples/spectrum-fitting/voigt_linear-detail.png differ diff --git a/docs/tasks/SPECTRUM-FITTING-001-A.md b/docs/tasks/SPECTRUM-FITTING-001-A.md new file mode 100644 index 0000000..294567a --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-A.md @@ -0,0 +1,56 @@ +# SPECTRUM-FITTING-001 — A packet + +Role: fresh root A, using .agents/skills/design-tests/SKILL.md. + +Approved slice: SPECTRUM-FITTING-001, public contract R3, scenarios S01-S33. +Owner explicitly approved R3 and the 33-scenario exception on 2026-10-05. +One checkpoint lineage. Approved runtime baseline: a835a490373835fa01e4460ce2159365150d1946. +Behavior and expectation sources: docs/tasks/SPECTRUM-FITTING-001-contract.md only. +No fixed owner execution time/token cap was supplied; report usage if available. +Default A/B window: at most two review rounds. Initial C plus one C repair; A/B +cannot replenish that allowance. No scope expansion, PR, push, merge, or release. + +Permitted inputs: this packet; the approved public contract; root AGENTS.md; +selected role skill; setup.cfg sections giving test/check conventions; approved +primary references linked by the contract. Use the existing NumPy/SciPy stack, +pytest-compatible tests, synthetic data, and the real public function. +Forbidden reads: dphtools product source, product history/diffs, existing product +tests/oracles, docs/PROJECT.md, the task execution record, other role transcripts, +and existing LESSONS entries. Reading LESSONS/README.md only is allowed if needed. +Do not use Superpowers, optional memory, delegation, or implementation helpers. +A/B restrictions are procedural, not engineered filesystem isolation. + +Before any public-API probe/test, configure source-free warnings and tracebacks. +Use a Python launcher that sets warnings.formatwarning to category/message and +filename/line only, then invokes pytest with -p no:warnings --tb=no. Do not hide +warnings, diagnostic messages, failure counts, or the exit status. A useful +launcher is: + +```python +import warnings +warnings.formatwarning = lambda message, category, filename, lineno, line=None: ( + f"{category.__name__}: {message} ({filename}:{lineno})\n" +) +import pytest +raise SystemExit(pytest.main([ + "tests/test_spectrum_fitting.py", "-p", "no:warnings", "--tb=no", "-q", "-ra" +])) +``` + +Interpreter: /Users/davidhoffman/Documents/GitHub/dphtools/.venv/bin/python. +Set MPLBACKEND=Agg and MPLCONFIGDIR to a writable temporary directory. Importing +the existing public module is permitted without reading its source. Do not add +product stubs or weaken checks. Disclose any accidental source exposure; a later +clean run does not restore that session's blindness. Classify observed failures: +environment/tooling, test defect, product defect, or unresolved requirement. + +Requested output: author tests/test_spectrum_fitting.py and a public mapping/ +tolerance report at docs/tasks/SPECTRUM-FITTING-001-tests.md. Do not edit any other +tracked file. Tests must cover S01-S33 through the public entry point and trace +numerical expectations to the contract, primary definitions, or invariants. +Choose justified tolerances independently, including discriminating wrong +amplitude/width/model/background/covariance values. Keep oracle helpers simple. +Return changed paths, scenario mapping, tolerance rationale, exact baseline +command/result and failure classification, input-exposure disclosure, and any +contract blocker. No test tuning from candidate implementation/output is allowed. +Completion: meaningful tests and handoff, or a specific contract blocker. diff --git a/docs/tasks/SPECTRUM-FITTING-001-B.md b/docs/tasks/SPECTRUM-FITTING-001-B.md new file mode 100644 index 0000000..445b8af --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-B.md @@ -0,0 +1,58 @@ +# SPECTRUM-FITTING-001 — B packet + +Role: fresh root B, using .agents/skills/review-work/SKILL.md in tests mode only. + +Approved slice: SPECTRUM-FITTING-001, public contract R3, scenarios S01-S33. +Owner explicitly approved R3 and the 33-scenario exception on 2026-10-05. +One checkpoint lineage. Approved runtime baseline: a835a490373835fa01e4460ce2159365150d1946. +Behavior and expectation sources: docs/tasks/SPECTRUM-FITTING-001-contract.md only. +No fixed owner execution time/token cap was supplied; report usage if available. +Default A/B window: at most two review rounds. Initial C plus one C repair; A/B +cannot replenish that allowance. No scope expansion, PR, push, merge, or release. + +Permitted inputs: this packet; the approved public contract; root AGENTS.md; +selected role skill; setup.cfg sections giving test/check conventions; approved +primary references linked by the contract. Use the existing NumPy/SciPy stack, +pytest-compatible tests, synthetic data, and the real public function. +Forbidden reads: dphtools product source, product history/diffs, existing product +tests/oracles, docs/PROJECT.md, the task execution record, other role transcripts, +and existing LESSONS entries. Reading LESSONS/README.md only is allowed if needed. +Do not use Superpowers, optional memory, delegation, or implementation helpers. +A/B restrictions are procedural, not engineered filesystem isolation. + +Before any public-API probe/test, configure source-free warnings and tracebacks. +Use a Python launcher that sets warnings.formatwarning to category/message and +filename/line only, then invokes pytest with -p no:warnings --tb=no. Do not hide +warnings, diagnostic messages, failure counts, or the exit status. A useful +launcher is: + +```python +import warnings +warnings.formatwarning = lambda message, category, filename, lineno, line=None: ( + f"{category.__name__}: {message} ({filename}:{lineno})\n" +) +import pytest +raise SystemExit(pytest.main([ + "tests/test_spectrum_fitting.py", "-p", "no:warnings", "--tb=no", "-q", "-ra" +])) +``` + +Interpreter: /Users/davidhoffman/Documents/GitHub/dphtools/.venv/bin/python. +Set MPLBACKEND=Agg and MPLCONFIGDIR to a writable temporary directory. Importing +the existing public module is permitted without reading its source. Do not add +product stubs or weaken checks. Disclose any accidental source exposure; a later +clean run does not restore that session's blindness. Classify observed failures: +environment/tooling, test defect, product defect, or unresolved requirement. + +Additional permitted inputs: A's proposed tests/test_spectrum_fitting.py and +public mapping/tolerance report docs/tasks/SPECTRUM-FITTING-001-tests.md, identified +by the coordinator's checkpoint hashes. Do not read A's conversation/logs. +Requested output: a read-only independent review of all S01-S33 mappings, +expectation sources, conditioning, tolerances in both directions, custom oracle +logic, input validation discrimination, and real public-call coverage. Do not fix +or edit tests or product files. Reject accidental extra requirements, unrelated +error-path successes, source-derived oracles, and nondiscriminating assertions. +Return ACCEPT with identified test hashes and valid baseline evidence, or concrete +REJECT findings/contract gaps. This is review round 2 of 2 after correction of B1's S10/S13/S23 defects. After the second +nonacceptance, diagnose before another rewrite. Completion: acceptance or specific +evidenced defects/blocker with classification and source-exposure disclosure. diff --git a/docs/tasks/SPECTRUM-FITTING-001-C.md b/docs/tasks/SPECTRUM-FITTING-001-C.md new file mode 100644 index 0000000..c93f51d --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-C.md @@ -0,0 +1,58 @@ +# SPECTRUM-FITTING-001 — C packet + +Role: fresh root specialist C, .agents/skills/implement-task/SKILL.md. +Owner approved public contract R3 and S01-S33 as one slice on 2026-10-05. +Public behavior: [contract](SPECTRUM-FITTING-001-contract.md). +Architecture/check commands: docs/PROJECT.md and root AGENTS.md. + +Reviewed test checkpoint: 1f9050ccd1d7e6b0ae86fd9eabc3600fcf29a268. +Tests SHA-256: 4f762faf63839e4301038a91d02abee9b2f6c8748dc671d19a48aec1ffb674d2. +Mapping/report SHA-256: 0f9e2b90b7f6d96a016073bc2dd0807c3888a9d257f781c568d097f221d23674. +B2 accepted all behavior/oracles/tolerances; B3 accepted Black-only correction +with exact AST equality. Original window2/2 and format window1/2 are closed; +prior attempts/spending remain retained. The public tests/report are permitted; +do not load A/B private conversations or logs. + +Approved runtime baseline: a835a490373835fa01e4460ce2159365150d1946. +Accepted tests on unchanged runtime collect263 cases: 1 failure/262 errors, +exit1, because the new public spectrum_fit entry point is absent. This is feature +absence, not an existing bug reproduction. No numerical assertion was reached. +Baseline evidence: /private/tmp/dphtools-spectrum-delivery/accepted-baseline.log +and historical public test report; B3 reproduced the formatted baseline. + +Allowed edits: additive spectrum_fit at dphtools.utils.fitfuncs, new fitting +helpers/modules under dphtools/utils if needed, and their docstrings. Preserve +all existing public signatures/behavior. No tests/fixtures/snapshots, workflows, +coverage/discovery/type settings, dependencies, skills, delivery rules, task +records, or unrelated product interfaces may change. Do not add test-only seams. +No Superpowers, optional memory, or delegation. No push, PR, merge, or release. + +Use the isolated locked interpreter: +/private/tmp/dphtools-spectrum-delivery/venv/bin/python +Python3.13.12; existing NumPy2.5.3/SciPy1.18.1. It passed canonical preflight, +including real nested installation. Never install into the shared primary .venv. +Use MPLBACKEND=Agg and MPLCONFIGDIR=/private/tmp/dphtools-spectrum-delivery/mplconfig. + +Implement the smallest approved change, then meaningful focused tests and fast. +Commit only authorized product paths with normal hooks before full so the exact +candidate is immutable. Set DPHTOOLS_PYTHON to the isolated interpreter for Git. +Run canonical full on that commit: + +```sh +MPLCONFIGDIR=/private/tmp/dphtools-spectrum-delivery/mplconfig /private/tmp/dphtools-spectrum-delivery/venv/bin/python tools/verification.py full +``` + +Retain full log at /private/tmp/dphtools-spectrum-delivery/C-full.log and verifier +receipts. No exclusions/skips/check weakening. Require exact global/per-file100% +owned statements/branches, including child processes and never-imported files. +If a check fails, classify environment/tooling, test defect, product defect, or +unresolved requirement before repair. Test defects/coverage gaps return to the +coordinator; C may not modify tests or evade a valid check. An unclassified cause +requires bounded diagnosis. Do not repeatedly run expensive full checks when a +focused failure already blocks them. + +This is initial C. One subsequent fresh C repair is available; no new time/token +cap was supplied, report measurable usage and unavailable billing. Preserve +allowances. Finish with candidate hash/changed paths, exact focused/fast/full +commands/results/coverage, unchanged test-hash confirmation, and any classified +blocker. You cannot approve your own candidate. D/platform checks remain pending. diff --git a/docs/tasks/SPECTRUM-FITTING-001-C1.md b/docs/tasks/SPECTRUM-FITTING-001-C1.md new file mode 100644 index 0000000..95870c0 --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-C1.md @@ -0,0 +1,59 @@ +# SPECTRUM-FITTING-001 — C1 repair packet + +Role: fresh root specialist C, .agents/skills/implement-task/SKILL.md. +Owner approved contract R3 and S01-S33 on 2026-10-05, including 33-scenario +exception, height amplitudes, positive amplitudes/widths and freely moving centers. +Read the contract, root AGENTS.md, docs/PROJECT.md, approved public tests/report. +No previous role transcripts or private reasoning. No Superpowers, optional +memory or delegation. Do not use the historical initial-C packet as current. + +Accepted checkpoint: f70661a9ed1813312e5bc409fae8a288c8c3d025. +Tests SHA-256: c5e51bab70912a674d7242541591d1844939dca981c5b78cbce9de70617accca. +Report SHA-256: 2bd7819d6c6112d2a6660fd03cd5f71685613565f11081fa8b340f3647a8ef3c. +B5 independently accepted the latest three numerical regressions in round1/2; +all 273 previously accepted cases and historical report bytes are unchanged. +Original, format, coverage and numerical B windows are accepted/closed. +Initial product: dd31c2c237ac119d1ba1a3008f6b16e7e48b41f8. Baseline runtime: +a835a490373835fa01e4460ce2159365150d1946. Current task pointer adds no product. + +Valid product-red: current spectrum file has 273 passed, 3 failed, no errors/skips. +Two S29 cases leak LinAlgError for the smallest finite positive Gauss/Lorentz +widths; numerical failure must be RuntimeError or legitimate validated success. +One S09/S31 case proves an identifiable stationary minimizer and independently +checks local physical covariance and unit transformation C'=D C D. Its covariance +fails after converting x/centers/widths by 1e-6. Repair numerical covariance/failure +handling generally. Do not change the contract or impose arbitrary lower-width +cutoffs. A separate nine-sample maximum-finite-height exact-minimizer custom +probe stalled after a covariance overflow warning and was terminated. This is +incomplete diagnostic evidence for the same numerical boundary, not an extra +approved timeout oracle. Keep diagnostics; avoid feeding nonfinite numerical +work into routines that cannot handle it. No fixture-specific implementation. + +Allowed edits: additive spectrum fitter/helper/export and docstrings only, +under dphtools/utils. Preserve all older public signatures/behavior. Never edit +reviewed tests/fixtures/snapshots, workflows, measurement/discovery/type settings, +dependencies, skills, task records or delivery rules. No test-only seams. No +push/PR/merge/release. This is the ONE remaining C repair: usage becomes1/1. +No new budget cap was supplied; preserve all attempts/spending and report usage. + +Clean selected Python: /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 +CPython3.12.14, all85 applicable hashed pins. Real nested system-site inheritance, +wheel import, subprocess coverage, and all59 verification-reuse tests passed. +Set MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig and +PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache. +Never install into the shared primary .venv. Preserve the repaired environment. + +Run meaningful focused spectrum plus existing fitter regressions, then canonical +fast. Commit only authorized product files with normal hooks using +DPHTOOLS_PYTHON=. Return promptly after the immutable +commit and focused/fast evidence. The COORDINATOR will run canonical full on +that exact commit, retain full receipts, and arrange fresh D/platform CI. This +allocation does not waive full verification or the exact100% global/per-file +owned statement/branch gate before submission. Do not repeatedly run expensive +full while a focused known failure blocks it. Do not claim full/D/CI success. + +Classify any failed checks before routing. Test defects/coverage gaps go to the +coordinator; do not edit them. No further C product repair is authorized after +this one without owner extension. Final report: candidate commit, changed paths, +exact commands/results, unchanged test/report hashes, classified blockers and +remaining full/D/CI work. You cannot approve your own repair. diff --git a/docs/tasks/SPECTRUM-FITTING-001-C2.md b/docs/tasks/SPECTRUM-FITTING-001-C2.md new file mode 100644 index 0000000..d5d419e --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-C2.md @@ -0,0 +1,56 @@ +# SPECTRUM-FITTING-001 — C2 approved extension packet + +Fresh root C uses .agents/skills/implement-task/SKILL.md. Read this packet, +R3 public contract, root AGENTS.md, PROJECT.md and reviewed public tests/report. +No other role transcripts/events. Do not use historical C/C1 packets as current. +No Superpowers, optional memory or delegation. + +Owner approved R3 and33 scenarios; then explicitly replied on2026-10-05: +"Approve one additional C repair (recommended)" for the independently confirmed +custom numerical-exception leak. Initial C and original repair remain counted. +This is exactly one additional repair, usage2/2 after completion, not a reset. +No further product repair is authorized without another explicit owner extension. +No new fixed time/token cap was supplied; preserve all attempts/spending. + +Accepted checkpoint b62450a206b6f3592c6b8aab712d34fb42f7f08f,285cases. +TestsSHA256 50bb168312355b5c3035f534195260f4a0081a6f171cdd6a6de52235774837dd. +ReportSHA256 73bf5276ddf3a315544f37627701615ab9651b9f967f9aebe96c8bb0b528c55c. +B7 accepted exact tests/report/R3 mapping in final round2/2 after46 independent +checks. All276 prior accepted cases and historical bytes/AST are retained. +Original2/2, format1/2, coverage1/2, numerical1/2, post-repair2/2 closed. +Runtime baseline a835a490373835fa01e4460ce2159365150d1946; first repaired product +8ae5438c09fa80559882ba0e3834f13756015851. Task-pointer updates add no product. + +Valid product-red: exact spectrum284passed/1failed; sole [optimizer_lstsq] +case injects a documented NumPy least-squares numerical decomposition failure +inside a legitimate selected custom optimizer at a proved stationary strict +local minimum. The public fitter leaks LinAlgError rather than R3 RuntimeError; +input-preservation checks pass. B6 and B7 independently confirmed high confidence. +The returned physical optimizer protocol, positivity/free centers and units stay +unchanged. Repair only numerical-exception normalization generally. Default LM +and custom numerical failures should use a consistent boundary without duplicate +handling. Preserve applicable diagnostics, do not swallow interrupts or unrelated +programming errors, add no fixture-specific branch/seam/domain cutoff. + +Allowed product paths: additive spectrum fitter/helper/export and docstrings +under dphtools/utils. Preserve the correct covariance and all older interfaces. +Never edit reviewed tests/fixtures/snapshots, workflows, coverage/discovery/types, +dependencies, skills, task records or delivery instructions. No push/PR/merge/ +release. Failed checks grant no new editing authority. Classify before routing. + +Clean CPython3.12.14 /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3. +All85 applicable hashed pins, nested inheritance/wheel import, actual child +coverage and59 verification-reuse cases passed. MPLBACKEND=Agg; +MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig; +PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache. +No environment changes or shared .venv installations. + +Run meaningful focused spectrum plus old fitter regressions, then canonical fast. +Commit ONLY authorized product files with normal hooks and DPHTOOLS_PYTHON set to +the selected interpreter. Return promptly after the immutable candidate with exact +checks/test hashes. COORDINATOR runs mandatory canonical full on that exact commit, +retains all receipts, and arranges fresh D and3-platform CI. Full and exact100% +global/per-file owned statements/branches are not waived. Do not claim full/D/CI +success or approve your own repair. Route defects/coverage gaps without test edits. +Final report: candidate/changed paths, actual commands/results, unchanged hashes, +remaining checks, classified blockers, repair2/2 used. Raw logs outside Git. diff --git a/docs/tasks/SPECTRUM-FITTING-001-contract.md b/docs/tasks/SPECTRUM-FITTING-001-contract.md new file mode 100644 index 0000000..ea17374 --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-contract.md @@ -0,0 +1,278 @@ +# SPECTRUM-FITTING-001 public contract + +**Version 1.0. Contract revision R4 — R3 approved by owner on 2026-10-05, +with the owner's physical-plausibility clarification on 2026-10-05.** +This is the public behavior packet. It contains no implementation or task status. + +## Purpose and scope + +Add one public function for a finite real one-dimensional spectrum. Fit positive +Gaussian, Lorentzian, or Voigt components jointly, using either detected local +maxima, supplied centers, or supplied full parameters. All components in one call +use the selected profile family. Support no background, a constant, or a line. + +Reuse the existing NumPy/SciPy dependencies and Python >=3.8 package metadata. +Reuse existing fitting functions where they satisfy this contract. If they are +deficient for this task, add new fitting functions or helpers. Do not change +existing public signatures or behavior. New additive interfaces are allowed. +Weighted/Poisson objectives, absorption dips, mixed profile families, plotting, +batch spectra, automatic information-criterion model selection, and release +publication are outside this slice. A new additive function is the release-note +impact. + +## Public interface + +```python +from dphtools.utils.fitfuncs import spectrum_fit + +result = spectrum_fit( + data, xdata=None, *, peak_type="gauss", guesses=None, + background="constant", prominence=None, distance=None, max_nfev=10000, + optimizer="lm", +) +``` + +- `data`: a nonempty real finite 1-D array-like of sample values. It is not mutated. +- `xdata`: optional real finite 1-D coordinates matching `data`, strictly + increasing. Nonuniform spacing is allowed. Omission means sample indices + `0, ..., len(data)-1`. Sample values are point observations, not bin integrals. +- `peak_type`: `"gauss"`, `"lorentz"`, or `"voigt"`, case insensitive. Gaussian and + Lorentzian are aliases `"gaussian"` and `"lorentzian"`. The correct name is Voigt. +- `guesses=None`: detect local maxima, estimate starting parameters, and jointly + fit the resulting components and background. +- A nonempty 1-D `guesses` sequence supplies centers in x units. Estimate the + other starting parameters without detecting or adding components. +- A nonempty 2-D `guesses` sequence supplies one full parameter row per component: + Gaussian `(amplitude, center, sigma)`, Lorentzian `(amplitude, center, gamma)`, + or Voigt `(amplitude, center, sigma, gamma)`. Fit exactly this many components. +- Initial amplitudes and widths must be strictly positive and finite; centers + must be finite but may lie outside the observed x interval. Guesses are not + mutated. +- `background`: `"none"`, `"constant"`, or `"linear"`. Default is constant. + A line is `b0 + b1*(x-xdata[0])`; `b0` is the value at the first coordinate. +- `prominence`: optional finite nonnegative minimum prominence in data units. + `distance`: optional finite value >=1, interpreted as minimum separation in + samples. These controls apply only when guesses are absent; explicit guesses + combined with either control raise `ValueError`, rather than ignoring it. +- `max_nfev`: a positive integer optimizer evaluation limit. Exhaustion is a fit + failure. Counting follows the selected optimizer; this is not a uniform limit + on every model evaluation, including covariance calculations. +- `optimizer`: `"lm"` by default, or a custom callable with the protocol below. + Other optimizer choices can be supplied as callables or configured adapters. + +`result` exposes these public attributes: + +| Attribute | Meaning | +|---|---| +| `peak_parameters` | 2-D array of rows in the above profile order, sorted by fitted center | +| `background_parameters` | Empty array, `(b0,)`, or `(b0, b1)` for none/constant/linear | +| `covariance` | Approximate full parameter covariance, ordered as flattened sorted peak rows followed by background coefficients | +| `fitted` | 1-D fitted spectrum at the input coordinates, including background | +| `residuals` | 1-D array `data - fitted` | + +Sorting must reorder covariance consistently, including cross-component and +background correlations. Returned array storage must not modify caller inputs. + +## Numerical convention and objective + +For `d=x-center`, use unit-height profiles: + +- Gaussian: `G(d) = exp(-d**2/(2*sigma**2))`. +- Lorentzian: `L(d) = gamma**2/(d**2+gamma**2)`. +- Voigt: `P(d) = V(d; sigma, gamma)/V(0; sigma, gamma)`, where `V` is the + normalized Gaussian/Cauchy convolution defined by SciPy's `voigt_profile`. + +The model is `background(x) + sum(amplitude_i * profile_i(x-center_i))`. +Each profile equals one at its center, so `amplitude` is the individual +component's peak height in data units, not integrated area. For overlapping +components, the total spectrum at a component's center also includes the other +components and background. Centers and widths have x units; Gaussian `sigma` is +standard deviation and Lorentzian `gamma` is half width at half maximum. Gaussian +full width at half maximum is `2*sqrt(2*log(2))*sigma`; Lorentzian full width is `2*gamma`. +Voigt has two independent width parameters. Both are strictly positive in this +first fitting interface; select Gauss or Lorentz for the pure limiting family. + +Fit unweighted nonlinear least squares: minimize the sum of squared residuals +at the supplied samples. Nonuniform x spacing does not introduce integration +weights. Amplitudes and widths remain strictly positive. Centers and background +coefficients are unconstrained, including centers outside the observed x interval. + +Covariance follows the local linear approximation with residual-variance scaling +by residual sum of squares divided by `number_of_samples-number_of_parameters`. +Require more samples than fitted parameters. Covariance is an approximate +uncertainty estimate, not a guarantee of identifiability or a global optimum. +Preserve applicable numerical warnings; do not fabricate finite uncertainties. +Numerical test tolerances are chosen independently by A and reviewed by B. + +### Physical scope of numerical acceptance + +The owner clarified: "No, let’s keep this grounded in terms of physical +plausibility." Required successful-fit and covariance-accuracy examples use +resolved spectra, plausible measurement noise, and reasonable background-to-peak +ratios in stated physical units. An artificial arithmetic baseline of 1e12 data +units with a height-three peak and noise around 0.01 data units is outside this +acceptance scope. This slice does not guarantee numerical accuracy throughout +the entire range of finite floating-point inputs. + +Keep accurate local covariance, fit quality, positivity, input preservation, and +the approved default/custom optimizer behavior on representative spectra. +Invalid-input and numerical-failure boundary checks remain useful robustness +checks; they are not evidence of physically representative measurements. Do not +interpret additive read-noise fixtures as photon-counting data or claim that a +local covariance recipe establishes repeated-measurement uncertainty. Weighted +or Poisson fitting remains outside this slice. + +## Optimizer interface + +The default is SciPy's Levenberg-Marquardt solver through +`scipy.optimize.least_squares(..., method="lm")`. It minimizes the stated +unweighted residual objective. The public fitting result and peak-height units +remain the same when a different optimizer is supplied. + +A custom callable has this signature: + +```python +optimizer(residual, initial, *, bounds, max_nfev) -> optimizer_result +``` + +- `residual(parameters)` returns `data - model(parameters)` as a 1-D array. +- `initial` and the returned `optimizer_result.x` use physical parameters, not + private transformed coordinates: flattened peak rows followed by background + coefficients. Parameter ordering stays fixed throughout the optimizer call; + the fitting result is subsequently sorted by center. +- `bounds` is a pair of lower/upper bound arrays in the same order and units: + amplitude/width lower bounds are zero and their upper bounds are infinity; + centers/background coefficients have infinite bounds in both directions. + The physical domain requires strictly positive amplitudes/widths, so a final + zero value is invalid even though zero marks their bound. The custom optimizer + must respect this domain and minimize the supplied residual sum of squares. + Configure additional solver options with a wrapper or `functools.partial`. +- `max_nfev` is forwarded to the callable, which must honor its evaluation limit. +- The returned object must expose a finite real 1-D `x` of the correct length + and a Boolean `success`. A `message` is optional. A failed convergence status, + malformed result, or final parameters outside the physical domain produces + `RuntimeError`; caller input arrays remain unchanged. +- The fitter calculates covariance from the residual Jacobian in physical + coordinates at the final solution. A custom optimizer need not return its own + Jacobian or covariance. Sorting still reorders the full covariance. + +SciPy's LM method does not accept explicit bounds. The default path therefore +uses internal parameter transforms for strictly positive amplitudes and widths. +Centers remain free, including outside the observed interval. Returned parameters +and covariance remain in physical units. Transformation details are internal +implementation choices; they are not part of the custom-optimizer protocol. + +## Discovery and failure behavior + +Automatic discovery supplies local-maximum candidates. It does not promise +identification of endpoints, unresolved overlapping components, or every physical +peak in noisy data. Prominence and distance let callers control candidate count; +full or center guesses supply components without local maxima. Starting estimates +and internal optimization choices remain delegated. + +Raise `ValueError` for invalid inputs/options, insufficient observations, or no +eligible detected peaks. Raise `RuntimeError` when fitting fails or reaches the +evaluation limit. Do not return a failed fit as a successful result. Do not +silently drop nonfinite samples or return a baseline-only fit for no peaks. + +## Expectation sources + +- Owner request on 2026-10-05: 1-D spectrum, automatic discovery or supplied + guesses, and selectable Gauss/Lorentz/Voigt. +- Owner clarification on 2026-10-05: positive peaks; selectable constant/linear + background; centers and full parameters; optional x, joint least squares, + parameters/covariance/fitted values/residuals. +- Owner correction on 2026-10-05: amplitude means component peak height, not + integrated area. This convention is settled. +- Owner clarification on 2026-10-05: preserve existing public interfaces; add new + fitting functions/helpers when existing functions are deficient. Use a default + Levenberg-Marquardt optimizer and allow a custom optimizer. +- Owner constraint choice on 2026-10-05: positive amplitudes/widths, with freely + moving centers; retain positivity through internal transforms for default LM. +- Owner approval on 2026-10-05: "Approve R3 and the 33-scenario exception." + This approves the complete contract, including width conventions, schema, + custom-optimizer protocol, failures, and one coherent 33-scenario slice. +- Owner scope clarification on 2026-10-05: "No, let’s keep this grounded in + terms of physical plausibility." R4 records that scope without changing the + public interface, optimizer protocol, or the 33 approved scenario rows. +- [SciPy 1.15.3 Voigt definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.special.voigt_profile.html) + gives the normalized Gaussian/Cauchy convolution and sigma/gamma convention. +- [SciPy peak discovery](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.find_peaks.html) + defines local maxima, prominence, sample-distance controls, and limitations. +- [SciPy measured peak widths](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.peak_widths.html) + distinguishes half prominence from model full width at half maximum. +- [SciPy least-squares/covariance convention](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) + describes the objective, scaling, covariance approximation, and failures. +- [SciPy optimizer methods and result interface](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) + defines LM's bounds limitation, residual objective, result status, and + method-dependent evaluation counts. +- Invariants: unit-height profile normalization, positive components, covariance + ordering, input preservation, and `residuals == data-fitted`. + +## Contract scenarios + +This is one coherent fitting slice with 33 explicit scenarios, exceeding the +five-scenario default. The owner explicitly approved the 33-scenario exception +on 2026-10-05. Profile and data variants within a row may exercise that same +approved behavior. A must +supply public-entry-point mappings and justified tolerances; B reviews both. + +| ID | Input/context and observable outcome | Expectation source | +|---|---|---| +| S01 | Full Gaussian guesses recover a well-conditioned positive component model and correctly shaped outputs | Gaussian model/objective | +| S02 | Full Lorentzian guesses recover a well-conditioned positive component model | Lorentzian model/objective | +| S03 | Full Voigt guesses recover a well-conditioned model with both widths positive | Voigt model/objective | +| S04 | Supplied centers estimate other starts and jointly fit exactly the requested components, including identifiable overlap | Owner guess mode; objective | +| S05 | Automatic mode fits separated eligible local maxima with discovery controls and sample-index x default | Owner discovery mode; SciPy discovery | +| S06 | Constant background is fitted jointly and its coefficient/covariance are represented correctly | Approved background model | +| S07 | Linear background is fitted jointly in the stated coordinate convention | Approved background model | +| S08 | No-background mode adds no coefficient and returns the correct fitted sum | Optional background | +| S09 | Physical/nonuniform coordinates preserve stated center, amplitude, width, and covariance units; unsorted guesses return consistently sorted rows | Units and covariance ordering | +| S10 | Non-1-D data is rejected with ValueError | Data shape | +| S11 | Nonfinite spectrum samples are rejected with ValueError | Finite sample domain | +| S12 | Complex spectrum samples are rejected with ValueError | Real sample domain | +| S13 | Coordinate dimension or length mismatch is rejected with ValueError | Coordinate shape | +| S14 | Nonfinite or complex coordinates are rejected with ValueError | Real finite coordinate domain | +| S15 | Duplicate or decreasing coordinates are rejected with ValueError | Strict coordinate ordering | +| S16 | Empty/malformed/wrong-width guess arrays are rejected with ValueError | Guess schema | +| S17 | Nonfinite or complex guess values are rejected with ValueError | Real finite parameter domain | +| S18 | Nonpositive initial amplitudes are rejected with ValueError | Positive initial amplitude | +| S19 | Nonpositive initial widths are rejected with ValueError | Positive width convention | +| S20 | Finite initial and fitted centers may lie outside the observed interval; an identifiable edge-tail model can be fitted without center bounds | Owner free-center choice | +| S21 | Unsupported profile selection is rejected with ValueError | Profile choices | +| S22 | Unsupported background selection is rejected with ValueError | Background choices | +| S23 | Invalid prominence is rejected with ValueError | Discovery control domain | +| S24 | Invalid distance is rejected with ValueError | Discovery control domain | +| S25 | Explicit guesses combined with discovery-only controls are rejected with ValueError | No ignored options | +| S26 | No eligible automatic peaks produces ValueError | No-peak outcome | +| S27 | Empty data or too few samples for positive residual degrees of freedom produces ValueError | Sample count and covariance scaling | +| S28 | Invalid evaluation limit produces ValueError | Positive integer limit | +| S29 | Exhausted/nonconvergent optimization produces RuntimeError and preserves caller inputs | Fit failure outcome | +| S30 | Omitted optimizer selects Levenberg-Marquardt, retains positive amplitudes/widths, and leaves centers free | Owner optimizer default and constraints | +| S31 | A supplied custom callable receives physical residuals, initial values, bounds, and the evaluation limit; a successful fit returns the same result/covariance convention | Owner optimizer override; callable protocol | +| S32 | A noncallable optimizer other than the supported default is rejected with ValueError | Optimizer selection domain | +| S33 | Malformed or physically invalid custom optimizer output produces RuntimeError without mutating caller inputs | Optimizer result contract | + +## Delivery constraints supplied to roles + +Use the specialist A/B/C/D route because this introduces a new scientific fitting +contract and independently checked numerical expectations. Blind A/B may read +this contract, their narrow packet, the applicable skill, permitted public +instructions/test conventions, and these references. They must not inspect +product implementation/history, task execution state, other role transcripts, +or implementation-bearing project records/lesson entries. + +A authors meaningful public-function tests. B reviews their expectation sources, +tolerances, and failure discrimination. Record the accepted checkpoint and valid +baseline evidence before C. C edits only approved product code/docstrings and +preserves reviewed tests, fixtures, configuration, workflows, and delivery rules. +D independently reviews the exact passing candidate and real check evidence. + +Default allowances are two A/B review rounds and initial C plus one corrective C +cycle. No fixed owner time/token budget has been supplied; record measurable +usage and missing billing rather than claiming a new unlimited allowance. A scope +or allowance extension requires owner approval. Preserve exact global/per-file +100% statement and branch coverage, including subprocesses and never-imported +owned runtime. Full local reference and protected Linux/macOS/Windows CI are +required before submission/merge under the governing instructions. Merge needs +separate owner authorization; no release publication is authorized. diff --git a/docs/tasks/SPECTRUM-FITTING-001-tests.md b/docs/tasks/SPECTRUM-FITTING-001-tests.md new file mode 100644 index 0000000..2567bc6 --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001-tests.md @@ -0,0 +1,3234 @@ +# SPECTRUM-FITTING-001 — historical independent A test evidence + +Approved expectation source: [public contract R3](SPECTRUM-FITTING-001-contract.md), +approved on 2026-10-05, including the 33-scenario exception. Approved runtime +baseline supplied by the A packet: `a835a490373835fa01e4460ce2159365150d1946`. +This report preserves historical A authorship, checks, and checkpoint evidence. +The task record alone owns the authoritative Current state section; it was not +read by A. This report grants no additional scope, review rounds, implementation +repairs, or publication authority, and records no independent acceptance. + +## Historical authorship and review evidence + +Initial A authored the two permitted output files for all 33 approved scenarios +(263 parametrized cases). Its source-free fixture/derivative sanity check passed. +Its last baseline returned 1 failure and 262 errors, exit status 1, because the +public entry point was absent; no numerical product assertion was reached. +Initial A used one root author session, no delegated agents, and launched no B or +C sessions. Its measured authoring/baseline interval was 425 s, from 17:31:10 to +17:38:15 UTC on 2026-10-05, excluding input reading and final report preparation. +The retained attempts and checks below describe that original work. + +The owner supplied B round1 **REJECT** with three concrete test defects in S10, +S13, and S23, and confirmed B otherwise found the S01-S33 mapping, numerical +expectations, and independently checked tolerances sound. A fresh root correction +session changed only those defects and this report's historical-evidence format. +The revised-check evidence below belongs to that second A session. B round1 +remains spent; only B round2 remains. The same checkpoint lineage, 33-scenario +scope, and initial C plus one C repair allowance are preserved without reset. + +Known authorship/review spending at this correction handoff: two root A sessions, +no delegation, one completed B round (rejection), and no C work performed by +either A session. No fixed owner time/token cap was supplied. Billing and exact +model-token usage are unavailable; the original 425 s interval is retained rather +than replaced by the correction interval. + +## Scenario mapping + +Every product assertion calls the real public +`dphtools.utils.fitfuncs.spectrum_fit` interface. The solver-identity test calls +that same interface in a fresh Python process. Tests use synthetic point +observations and existing NumPy/SciPy, with no product model helpers, existing +test fixtures, or implementation oracles. + +| Scenario | Public test(s), prefix `test_` | Expected behavior/source | +|---|---|---| +| S01 | `s01_full_gaussian_guesses_and_schema` | R3 Gaussian unit-height equation, full rows, sorted result schema; case-insensitive aliases and list/array inputs | +| S02 | `s02_full_lorentzian_guesses` | R3 Lorentzian unit-height equation and gamma as half width at half maximum; aliases | +| S03 | `s03_full_voigt_guesses_independent_widths` | R3 normalized Voigt equation; both independently identifiable positive widths | +| S04 | `s04_center_guesses_joint_components`; `s04_center_guesses_identifiable_overlap_with_one_maximum` | R3 center-only starts, exact component count, joint fitting; two-component Gaussian mixture has only one local maximum | +| S05 | `s05_automatic_discovery_controls_and_index_default` | R3/SciPy local maxima, prominence in data units, distance in samples; omitted x means sample indices; fractional distance is valid | +| S06 | `s06_constant_background_and_covariance` | R3 default constant background, jointly estimated coefficient, full physical covariance | +| S07 | `s07_linear_background_first_coordinate_origin` | R3 `b0+b1*(x-x[0])`, with nonzero x origin and nonzero slope | +| S08 | `s08_no_background_multicomponent_sum` | R3 empty background parameters and no added coefficient | +| S09 | `s09_nonuniform_units_full_sorted_covariance_and_storage` | R3 physical units, unweighted objective on nonuniform samples, sorting of all covariance rows/columns, caller storage preservation | +| S10 | `s10_non_1d_data` | R3 1-D data domain: matrix/3-D reshapes of a valid 41-sample Gaussian with matching valid x and full guesses; scalar rejection also overlaps insufficient observations | +| S11 | `s11_nonfinite_data` | R3 finite observations: NaN and both infinities rejected | +| S12 | `s12_complex_data` | R3 real data domain, including complex dtype with zero imaginary part | +| S13 | `s13_coordinate_shape_length` | R3 1-D matching coordinates: scalar, strictly increasing x reshaped as a matrix, shorter and longer arrays; valid Gaussian/full guesses | +| S14 | `s14_coordinate_real_finite_domain` | R3 real finite coordinates, including zero-imaginary complex dtype | +| S15 | `s15_strict_coordinate_order` | R3 duplicate, descending and one reversed adjacent pair rejected | +| S16 | `s16_empty_or_malformed_guesses`; `s16_wrong_full_guess_row_width` | R3 nonempty 1-D centers or 2-D rows of family-specific width; reject ragged, scalar, 3-D, empty and wrong-column arrays | +| S17 | `s17_guess_real_finite_domain`; `s17_nonfinite_amplitude_or_width` | R3 finite real centers, amplitudes and every width, including both Voigt widths | +| S18 | `s18_nonpositive_initial_amplitude` | R3 strictly positive initial heights, every family | +| S19 | `s19_nonpositive_initial_width` | R3 strictly positive initial widths, including both Voigt widths | +| S20 | `s20_free_initial_and_fitted_edge_tail_centers` | R3 free centers on either side of observed interval; start inside or outside; identifiable Gaussian/Lorentzian edge tails | +| S21 | `s21_invalid_profile` | R3 allowed profile names, unsupported names/types rejected | +| S22 | `s22_invalid_background` | R3 allowed background choices, unsupported names/types rejected | +| S23 | `s23_invalid_prominence` | R3 finite nonnegative scalar prominence; sequence `[0, 10]` retains the fixture peak if incorrectly forwarded as a SciPy interval | +| S24 | `s24_invalid_distance` | R3 finite sample distance at least one | +| S25 | `s25_explicit_guesses_reject_discovery_controls` | R3 no ignored controls with either center-only or full guesses; includes boundary-valid controls | +| S26 | `s26_no_local_maxima`; `s26_no_peaks_meet_prominence` | R3 no-eligible-peaks ValueError, without baseline-only success | +| S27 | `s27_empty_data`; `s27_positive_residual_degrees_of_freedom` | R3 `n>p`; test `n=p` and `n=p-1`, one/two components, every family/background | +| S28 | `s28_invalid_evaluation_limit` | R3 positive integer evaluation limit; reject nonpositive, fractional, nonfinite and nonnumeric values | +| S29 | `s29_exhausted_default_optimizer_preserves_inputs`; `s29_failed_custom_status_preserves_inputs` | R3 exhausted real default solve or unsuccessful custom status raises RuntimeError; arrays unchanged | +| S30 | `s30_default_is_real_lm_with_positive_domain_and_free_centers` | R3 actual SciPy least_squares method is LM for omitted/explicit optimizer; same positive physical result, center leaves interval | +| S31 | `s31_custom_optimizer_physical_protocol_and_covariance` | R3 custom physical initial/residual/order/bounds/limit; successful real TRF adapter has no returned Jacobian; fitter computes full sorted physical covariance | +| S32 | `s32_invalid_optimizer_selection` | R3 only default `"lm"` string or callable; other noncallable choices rejected | +| S33 | `s33_malformed_custom_optimizer_output`; `s33_invalid_final_positive_domain_and_input_storage` | R3 required result attributes, real finite 1-D correct-length x, Boolean success, strictly positive final heights/widths; failure preserves input arrays even if optimizer uses initial as working storage | + +Variants exercise the approved rows; they do not add contract scenarios. + +## Independent oracle and tolerance rationale + +The simple `_model` helper evaluates exactly the three R3 point-sample equations. +Gaussian and Lorentzian expectations use elementary expressions. Voigt uses the +permitted SciPy definition, divided by its value at zero. No peak-width measurement +routine supplies the expected fitted width: half prominence is a starting-estimate +quantity and need not equal the model's full width at half maximum. + +The [SciPy Voigt definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.special.voigt_profile.html) +specifies sigma as Gaussian standard deviation and gamma as Cauchy half width at +half maximum. The [peak discovery definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.find_peaks.html) +supports the synthetic local-maximum counts and sample-distance interpretation. +The [width definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.peak_widths.html) +explains why half-prominence estimates are not numerical truth for fitted widths. +These contract-linked pages were opened during A authoring. + +All core numeric fixtures have order-one peak heights and widths in the supplied +x units, multiple samples per width, wide observation intervals, and perturbed +starts near the identifiable solution. Noiseless comparisons use `rtol=3e-5` and +`atol=2e-5` for heights, centers, widths and background coefficients, and absolute +`2e-7` data units for fitted samples. These allow optimizer stopping and floating +point differences well above float64 roundoff, while requiring the exact sampled +model to be reconstructed accurately. Relative tolerances apply to each physical +parameter; absolute tolerances carry that parameter's units. + +The unit-height sanity check is direct: each isolated profile equals one at its +center. A Gaussian with sigma `0.83` would have area/height factor +`sqrt(2*pi)*0.83 ≈ 2.08`; a Lorentzian with gamma `1.27` has factor +`pi*1.27 ≈ 3.99`. These order-one differences greatly exceed the parameter +tolerances. Gaussian full width divided by sigma is approximately `2.355`, and +Lorentzian full width divided by gamma is `2`. The chosen Voigt fixture has sigma +`0.72` and gamma `0.46`, so swapping the widths also exceeds tolerance. Incorrect +families are constrained by both the recovered parameters and the complete +sampled profile, including tails. + +Internal consistency comparisons use `rtol=2e-10, atol=2e-10` for reconstruction +from returned parameters and `rtol=2e-12, atol=2e-12` for `data-fitted`. These are +arithmetic checks at identical physical parameters, not optimizer convergence +checks. Covariance symmetry permits `rtol=2e-10, atol=2e-12` and eigenvalues down +to `-1e-12` for numerical roundoff on these well-conditioned fixtures. This finite +covariance requirement applies only to the identifiable numeric fixtures; the +contract does not promise finite uncertainties for singular fits. + +Noisy fixtures add the fixed deterministic sequence +`0.018*(sin(1.63*i)+0.6*cos(0.71*i))` in data units. Assertions against the generating +parameters allow absolute `0.04` or `0.05` in each peak parameter's own units, +and `0.01` for background coefficients. The generating parameters are not +claimed to be the noisy optimum. Stronger checks use the returned optimum's +independent model, unweighted stationarity, and independently scaled covariance. + +For a sorted physical parameter vector p, the independent five-point derivative +is + +`J[:,j] = (m(p-2h)-8m(p-h)+8m(p+h)-m(p+2h))/(12h)`. + +Each h is `eps**(1/5)*max(1,abs(p[j]))`. Float64 epsilon gives a relative step +approximately `7.4e-4`, balancing fourth-order truncation and roundoff. All +fixture widths remain well above these perturbations. The covariance expectation +is + +`s² = residual.T @ residual / (n-p)`; `C = s² * inv(J.T @ J)`. + +The [SciPy covariance convention](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) +supports this residual-variance scaling. `_check_covariance_and_objective` checks +the dimensionless condition number of the column-normalized Jacobian is below +`1e4` before trusting the inverse. It compares every covariance entry after +division by `sqrt(C_expected[ii]*C_expected[jj])`, with relative `0.01` and +absolute `2e-4`. This tests cross-component and background correlations as well +as diagonal variances, and tolerates approximate numerical differentiation. +For the 81-sample constant-background fixture, omitting residual degrees of +freedom changes the variance scale by at least about 5%; it cannot fit this 1% +tolerance. Covariance is evaluated in the actual sorted physical units, so a +private log-parameter covariance or an inconsistent permutation cannot pass. + +The unweighted optimum requires `J.T @ residual = 0`. The maximum columnwise +dimensionless projection `abs(J.T @ residual)/(norm(J_column)*norm(residual))` +must be below `3e-4`. Nonuniform coordinates deliberately create unequal sample +spacing; integration-weighted objectives must still satisfy this unweighted +stationarity check. No fit is required to find a global optimum for arbitrary +input data. + +Automatic discovery uses three separated exact Gaussian peaks. The small middle +peak is removed by higher prominence or larger sample distance. When it is +removed, the reduced model is misspecified, so its fitted amplitudes/widths are +not compared to the original three-component truth. The test checks candidate +count, centers (absolute `0.003` x units), reconstruction, covariance and local +unweighted stationarity. Physical coordinates scaled by `0.01` distinguish sample +distance from distance in x units. No endpoint/noisy/unresolved automatic peak +identification guarantee is imposed. + +The custom optimizer adapter checks residual sign, profile normalization, +background origin, row order and physical bounds at two explicitly constructed +physical vectors before calling SciPy's real trust-region reflective solver. +No background starting estimator is specified or assumed. The adapter returns +only x, Boolean success and a message. The fitter must supply its own covariance. +The default-method test wraps the actual SciPy solver in a fresh interpreter, +before the first product import, and executes the real numerical solve. The +[SciPy optimizer documentation](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) +supports the method distinction and LM's inability to accept explicit bounds. +It is not used to prescribe a private positivity transform. + +All tolerances above were fixed before the first public baseline execution. +No product output, source, history, or prior tests informed the oracle or tolerance. + +## Initial A baseline execution and classification + +Initial A's last exact baseline command, run from the packet's worktree: + +```sh +set -o pipefail +MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/spectrum-fitting-001-a-mpl /Users/davidhoffman/Documents/GitHub/dphtools/.venv/bin/python - <<'PY' 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-baseline-final.log +import warnings +warnings.formatwarning = lambda message, category, filename, lineno, line=None: ( + f'{category.__name__}: {message} ({filename}:{lineno})\n' +) +import sys +import traceback +sys.excepthook = lambda kind, value, tb: traceback.print_exception(kind, value, None, limit=0) +import pytest +status = pytest.main([ + 'tests/test_spectrum_fitting.py', '-p', 'no:warnings', '--tb=no', + '-q', '-ra', '--capture=tee-sys', +]) +print('BASELINE_PYTEST_EXIT_STATUS=', status, sep='') +raise SystemExit(status) +PY +``` + +Result: **1 failed, 262 errors in 1.64 s; pytest and shell exit status 1**. +All 263 parametrized cases were collected. The 262 fixture setup errors arise +from importing the missing public entry point. The fresh-process LM probe also +exited 1 with this source-free diagnostic: + +```text +ImportError: cannot import name 'spectrum_fit' from 'dphtools.utils.fitfuncs' (/Users/davidhoffman/.codex/worktrees/spectrum-fitting/dphtools/dphtools/utils/fitfuncs.py) +``` + +**Classification: product defect — missing R3 public API**, high confidence. +The module path identifies the approved worktree, not an unrelated installed +package. This supplies product-red evidence for the absent entry point only. +The baseline cannot establish failures of the numerical, validation or optimizer +behaviors, because they were not reached. No environment/tooling defect or +unresolved requirement was observed; independent B still must assess test validity. + +Retained attempts, without deleting earlier evidence: + +| Attempt | Exact command difference | Result | Full log outside Git | +|---|---|---|---| +| Initial | Same launcher; tee path ends in `A-baseline.log` | 1 failed, 262 errors, 12.33 s, exit 1; same missing API | `/private/tmp/SPECTRUM-FITTING-001-A-baseline.log` | +| Final | Exact launcher above | 1 failed, 262 errors, 1.64 s, exit 1; same missing API | `/private/tmp/SPECTRUM-FITTING-001-A-baseline-final.log` | + +Between attempts, a static test review removed an unsupported assumption that +the custom optimizer's initial array must be writable. S33 now mutates that +working array only when it is writable; either storage choice is valid under +R3. This clarification did not alter any oracle, tolerance, product expectation, +or scenario count. The final baseline was rerun on the revised exact test file. + +No warnings, diagnostic messages, counts, exit status, exclusions, or skips were +hidden by the launcher. `--capture=tee-sys` retained live diagnostic output; +source-free rendering suppressed source excerpts. No warning was emitted in +either baseline. Full repository statement/branch coverage and platform-matrix +verification were not run in this narrow A execution; no coverage or merge +readiness claim is made. + +The source-free helper check used the specified Python, `MPLBACKEND=Agg`, and +`MPLCONFIGDIR=/private/tmp/spectrum-fitting-001-a-mpl`. It parsed the authored test, +checked the one-maximum overlap fixture, and compared the Gaussian/Lorentzian +five-point derivatives with their elementary analytic derivatives +(`rtol=2e-6, atol=2e-8`). Exit status was zero. Dimensionless Jacobian condition +numbers at the physical nonuniform truth were Gaussian `5.3078`, Lorentzian +`5.28439`, and Voigt `21.8762`, all comfortably below the fixture limit. + +Environment observed through that source-free launcher: Darwin arm64, Python +`3.13.12`, NumPy `2.5.3`, SciPy `1.18.1`, pytest `9.1.1`. These versions are execution +evidence; the approved primary definitions remain the contract-linked references. + +## Initial A blind-input and allowance disclosure + +Read only the A packet, approved public contract R3, root AGENTS.md, selected +`design-tests` skill, permitted test/check sections of setup.cfg, and the five +approved primary-reference pages. The two output paths were checked for existence +without reading prior content; both were absent. No product source/history, +existing tests/oracles, PROJECT.md, task state, existing lessons, or other role +conversation was read. No delegation, Superpowers, optional memory, configuration +editing, commit, push, PR, merge, or publication occurred. + +The helper and both baseline launchers configured source-free warnings and +exception rendering before imports. Both baselines used `-p no:warnings --tb=no`; +the default-method child also set source-free rendering before its imports. +**Source exposure: none observed.** The diagnostic showed a product filename, +which is permitted; no product source excerpt or source-bearing traceback was +rendered. Public imports execute product code but did not expose its source. +A/B retain the packet's single checkpoint lineage and at most two +review rounds; initial C plus one C repair is unchanged. A has not accepted its +own tests and has not spent a B round or C repair. + +## A correction after B round1 rejection + +The owner-supplied B1 findings are **test defects**, not unresolved requirements. +This correction preserves every other numerical fixture, model/covariance oracle, +and tolerance, and leaves the S01-S33 scenario count and 263 case count unchanged. + +| B1 defect | Correction | Why an unrelated rejection no longer satisfies the case | +|---|---|---| +| S10 matrix/3-D data also lacked detectable peaks | Reshape the existing valid Gaussian spectrum to `(41, 1)` and `(1, 41, 1)`; supply matching valid x, explicit full Gaussian guesses, and no background | Flattening yields the exact valid 41-sample Gaussian with 3 physical parameters; automatic discovery is bypassed | +| S13 matrix x also violated strict ordering when flattened | Reshape the fixture's strictly increasing x to `(41, 1)`; use the matching valid Gaussian and explicit full guesses with no background | Flattening restores the original valid ordered coordinates with matching sample count | +| S23 `[1, 2]` could legitimately remove the peak | Replace only that invalid sequence with `[0, 10]` | SciPy forwarding retains the fixture peak, so no-eligible-peaks rejection cannot satisfy this case | + +The scalar S10 case remains a rejection check, with a matching one-coordinate x +array and valid full guesses. It also has too few observations (`1 < 3`) and does +**not** independently establish dimension validation. Only the matrix/3-D cases +isolate the dimension defect. S13 scalar and length-mismatch cases retain their +approved rejection expectations. + +The approved [SciPy find_peaks documentation](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.signal.find_peaks.html) +was reopened during correction. It defines a two-element prominence sequence as +minimum/maximum bounds. The source-free correction sanity check measured one +retained peak at index 21 with prominence `2.999596352246759` data units for +`[0, 10]`, while `[1, 2]` retained none. This checks fixture discrimination, not +product behavior. + +Correction syntax/fixture checks passed, exit status 0. They parsed and compiled +the revised test without writing bytecode, checked finite real 41-sample data, +increasing matching coordinates and lossless reshapes, and solved the flattened +fixture with SciPy's real LM solver using the unchanged independent `_model`. +The recovered physical parameters were +`[3.0, 0.19999999999999993, 0.9]`, within the unchanged parameter/model tolerances. +These checks did not import the product. Full check output is retained outside +Git at `/private/tmp/SPECTRUM-FITTING-001-A-correction-checks.log`. + +The exact revised test file used in the baseline has SHA-256 +`f568ad84499aa7dea8a061bc5935fad09fbf96caee733bd78664cdb907d411c0`. +The report's final hash is returned outside the tracked tree to avoid a recursive +self-hash. The packet-supplied approved runtime revision remains +`a835a490373835fa01e4460ce2159365150d1946`; no product history or diff was inspected. + +Exact correction baseline command, run from the packet's worktree: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/spectrum-fitting-001-a-correction-mpl /Users/davidhoffman/Documents/GitHub/dphtools/.venv/bin/python - <<'PY' 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-baseline-correction.log +import warnings +warnings.formatwarning = lambda message, category, filename, lineno, line=None: ( + f'{category.__name__}: {message} ({filename}:{lineno})\n' +) +import sys +import traceback +sys.excepthook = lambda kind, value, tb: traceback.print_exception(kind, value, None, limit=0) +import pytest +status = pytest.main([ + 'tests/test_spectrum_fitting.py', '-p', 'no:warnings', '--tb=no', + '-q', '-ra', '--capture=tee-sys', +]) +print('BASELINE_PYTEST_EXIT_STATUS=', status, sep='') +raise SystemExit(status) +PY +``` + +Correction result: **1 failed, 262 errors in 12.08 s; pytest and shell exit status +1**. All 263 cases were collected. The 262 fixture errors occurred when importing +the absent `spectrum_fit`; the unchanged fresh-process LM test printed child exit +status 1 and the same source-free missing-entry-point ImportError recorded above. +No numerical or validation product assertion was reached. No warnings or skips +were reported; diagnostics, failure/error counts, and exit status were retained. +The new correction log does not replace either retained initial-A attempt. + +**Classification: product defect — missing R3 public API**, high confidence. +This remains product-red evidence for the missing entry point only. The corrected +tests still require fresh independent B round2 review; A grants no acceptance. +No unresolved requirement or environment/tooling defect was observed. The earlier +B1 test defects are recorded and corrected, not relabeled as product failures. + +Correction input disclosure: read the A packet, contract R3, session-supplied +AGENTS instructions, selected design-tests skill, setup.cfg test/check conventions, +the specifically permitted prior test/report, and the approved find_peaks page. +No product source/history/diff, other tests/oracles, PROJECT.md, task record, +lesson entries, or A/B conversations were read. Only the two authorized output +files were edited; logs and Matplotlib configuration used temporary paths. +No delegation, Superpowers, optional memory, commit, push, PR, or full verification +was performed. + +Source-free warning and exception rendering was configured before the correction +sanity check and baseline; pytest used `-p no:warnings --tb=no`, and the unchanged +LM child installed source-free rendering before imports. **Source exposure: none +observed.** The ImportError disclosed only a filename, not product source or a +source-bearing traceback. Correction environment: Darwin arm64; Python 3.13.12, +NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1. The measured correction authoring/check +interval was 189 s, from 17:45:45 to 17:48:54 UTC on 2026-10-05, excluding earlier +input reading and final report preparation. The prior 425 s interval and all +attempts remain recorded; these partial measurements total 614 s. Exact token +usage/billing are unavailable. Only B round2 remains in the existing window; +the C allowance and scope are unchanged. + +## A formatting correction after the accepted B2 checkpoint + +The owner supplied B2 acceptance of all approved scenarios at test SHA-256 +`f568ad84499aa7dea8a061bc5935fad09fbf96caee733bd78664cdb907d411c0`. +The later canonical fast format failure was classified as a **test formatting +defect**. This fresh root A session was authorized only to apply Black to the +test file, prove unchanged Python AST, run a permitted revised source-free +baseline, and append this historical evidence. All earlier report text, attempts, +oracle values, tolerances, and spending remain preserved. + +Preflight confirmed that the worktree test and the specifically permitted exact +B2-accepted source at `/private/tmp/dphtools-spectrum-delivery/B2-accepted-tests.py` +were byte-identical and both had the accepted hash above. The only test mutation +was this command, run from the supplied worktree: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/private/tmp/SPECTRUM-FITTING-001-A-format-sourcefree /private/tmp/dphtools-spectrum-delivery/venv/bin/python -m black --line-length 99 tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-format-black.log +``` + +The temporary `sitecustomize.py` in that PYTHONPATH installs the same source-free +warning formatter and exception hook used by the approved launcher, before Black +imports. It changes no repository configuration. Black 26.5.1 reported one file +reformatted; shell exit status was 0. The formatted test SHA-256 is +`4f762faf63839e4301038a91d02abee9b2f6c8748dc671d19a48aec1ffb674d2`. + +Both sources were parsed with `ast.parse(..., type_comments=True)` and compared +using `ast.dump(..., annotate_fields=True, include_attributes=False)`. The dumps +are **exactly equal**, including all constants, oracle values, tolerances, +decorators, and the embedded child-process script; location attributes are omitted. +Both AST dumps have SHA-256 +`5a7a1039ce49c886f75739f5b7dc4436acc38f55e44a5e4ab3939f9857f90855` +and 6,583 AST nodes under Python 3.13.12. AST proof exit status was 0. This session +did not redesign cases or repeat numeric oracle calibration. + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/private/tmp/SPECTRUM-FITTING-001-A-format-sourcefree /private/tmp/dphtools-spectrum-delivery/venv/bin/python -m black --check --line-length 99 tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-format-black-check.log +``` + +Black check reported one file would be left unchanged; shell exit status was 0. +**Formatting-defect correction: verified**, high confidence, with exact AST +equality to the supplied accepted source. + +The revised baseline reused the approved source-free launcher and exact pytest +arguments recorded above: `tests/test_spectrum_fitting.py -p no:warnings --tb=no +-q -ra --capture=tee-sys`. It used +`/private/tmp/dphtools-spectrum-delivery/venv/bin/python`, +`PYTHONDONTWRITEBYTECODE=1`, `MPLBACKEND=Agg`, and +`MPLCONFIGDIR=/private/tmp/spectrum-fitting-001-a-format-mpl`, with `set -o pipefail` +and output retained at +`/private/tmp/SPECTRUM-FITTING-001-A-baseline-format-correction.log`. +Source-free hooks preceded all probe imports. Added identity diagnostics recorded +the test hash, HEAD, environment, and imported public module filename; the test +hash and HEAD were unchanged after pytest. + +Observed baseline HEAD was `8361239543449f1afe2d7464a27790c5467c780b`, obtained +only through `git rev-parse HEAD`; no history or implementation was read. Execution +was in `/Users/davidhoffman/.codex/worktrees/spectrum-fitting/dphtools`, and the +imported public module filename was that worktree's `dphtools/utils/fitfuncs.py`. +HEAD identifies the observed checkout; this blind session makes no claim about +uninspected working-tree product changes. Environment: Darwin arm64, Python +3.13.12, NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. + +Result: **1 failed, 262 errors in 12.34 s; pytest and shell exit status 1**. +All 263 cases were collected. The fixture errors arose from the absent public +`spectrum_fit` entry point. The unchanged LM child exited 1 and retained the +same source-free missing-entry-point ImportError shown in the prior baseline. +No numerical or validation product assertion was reached. No warning or skip +was reported; the full log retains all diagnostics, counts, and failure status. +**Baseline classification: product defect — missing R3 public API**, high +confidence. This is evidence for the missing entry point only and does not +authorize product repair by A. + +Additional retained evidence outside Git: + +| Evidence | Path | Result | +|---|---|---| +| Preflight source hashes and identity | `/private/tmp/SPECTRUM-FITTING-001-A-format-preflight.log` | Accepted/current bytes equal; expected hash confirmed | +| AST proof | `/private/tmp/SPECTRUM-FITTING-001-A-format-ast.log`; `/private/tmp/SPECTRUM-FITTING-001-A-format-ast-proof.json` | Exact equality excluding locations; exit 0 | +| Historical report preimage | `/private/tmp/SPECTRUM-FITTING-001-A-format-report-before.md`; `/private/tmp/SPECTRUM-FITTING-001-A-format-report-preimage.log` | Prior report retained for prefix-preservation verification | + +Correction disclosure: read only the session packet/instructions, selected +design-tests skill, approved contract R3, permitted test/report, and exact +B2-accepted public test source. This session also read its own generated logs and +current HEAD identity. No implementation/history, other tests, PROJECT.md, task +state, lessons, or other conversations were read. Only the test and this report +were edited in the tracked tree. No delegation, Superpowers, optional memory, +commit, push, PR, full verification, or budget expansion occurred. **Source +exposure: none observed.** Diagnostics disclosed a public module filename, with +no implementation excerpt or source-bearing traceback. This appendix adds no +competing Current state and grants no test acceptance. + +Allowance evidence supplied by the owner: the prior review window spent 2/2 B +rounds and was accepted/closed. This later evidenced, authorized correction opens +a new section9 test-correction window with at most 2 B reviews, 0 used at this +handoff. All prior attempts/spending remain retained; no C allowance is reset. +This A session launched no reviewers or C sessions and performed no C repair. + +Metrics: 33 unchanged scenarios / 263 unchanged cases; 1 fresh root A launch +(3 known A launches including the two historical authors); 0 reviewer launches +in this session; prior B window 2/2 accepted/closed, correction window B 0/2; +no C allowance reset. The observed formatting/check interval was 88 s, from +17:57:09 to 17:58:37 UTC on 2026-10-05, excluding final report preparation. +Together with the retained 425 s and 189 s intervals, measured partial spending +totals 702 s. Exact token usage/billing remain unavailable. + +## Post-implementation public coverage correction — new review window + +This fresh independent root A session was authorized for three evidenced public +coverage gaps after initial C. These additions are **post-implementation +correction evidence**, not original test-first red evidence. The owner approved +R3 and S01-S33 as one slice; no scenario or behavior approval was added or reset. +The packet supplied accepted test SHA-256 +`4f762faf63839e4301038a91d02abee9b2f6c8748dc671d19a48aec1ffb674d2`, historical report +SHA-256 `0f9e2b90b7f6d96a016073bc2dd0807c3888a9d257f781c568d097f221d23674`, and +checkpoint `1f9050ccd1d7e6b0ae86fd9eabc3600fcf29a268`. Both input hashes were +confirmed before editing. The earlier original review window was 2/2 +accepted/closed; the Black-only correction window was 1/2 accepted/closed. +Their attempts and spending remain recorded above. The coordinator's supplied +narrow public gap descriptions were used; no raw implementation or coverage +logs were read. + +### Added-case mapping and independent expectations + +Three new public test functions add ten cases, bringing the focused file from +263 to 273 cases while retaining all 33 approved scenarios. + +| Existing scenario(s) | Added public test, prefix `test_` | Cases and expected behavior | +|---|---|---| +| S04/S08; S01-S03 numerical definitions | `s04_s08_center_guess_broad_component_truncated_half_height` | 6: each family, left/right truncation; center-only guesses fit one broad component with no background, recovering physical height, center, width(s), model and residuals despite an absent half-height crossing | +| S31; S01-S03/S08 model and R3 covariance boundary | `s31_coincident_components_warn_without_finite_individual_height_uncertainties` | 3: each family; public physical custom optimizer returns an exact minimizer for two coincident equal-width components; covariance warning remains observable and both individual height variances are nonfinite | +| S29; S03 normalized Voigt and finite-domain definitions | `s29_finite_subnormal_voigt_numerical_boundary_preserves_inputs` | 1: finite strictly increasing coordinates, finite data and strictly positive subnormal initial widths; numerical inability raises RuntimeError, or a robust solver returns an independently verified finite exact-model fit; either outcome preserves caller inputs | + +**Truncated broad component.** The physical row is `(3.4, 7.3, 2.5)`, or +`(3.4, 7.3, 2.5, 1.25)` for Voigt. Height has data units; center and widths have +x units. The 241 coordinates are `center + sigma_or_gamma * offsets`, with +offsets `[-0.4, 3]` for left truncation and `[-3, 0.4]` for right truncation. +The boundary on the truncated side lies above half height, while the opposite +tail lies below half height. Continuity and monotonicity of these isolated +profiles place one half-height crossing outside the observation interval. +For example, the Gaussian half-height offset is +`sqrt(2*log(2))*2.5 = 2.9435` x units; the truncated boundary is only `1.0` +x unit from the center. Lorentzian half height is at offset `2.5` x units. +The endpoint ordinate assertions check this property for every family. + +An independent source-free fixture check, using the existing five-point oracle, +found column-normalized physical Jacobian condition numbers `4.60255` for +Gaussian, `3.21364` for Lorentzian, and `19.60241` for Voigt, on either side. +Thus the samples retain width and height information; no initialization +algorithm or internal estimate is specified. Each result uses the unchanged +parameter tolerances `rtol=3e-5, atol=2e-5` in the parameter's own units, +model/residual absolute tolerance `2e-7` data units, and existing reconstruction +and residual consistency checks. Wrong height units, a stuck fallback width, +a shifted center, dropped components or an added background coefficient cannot +satisfy the combined physical-parameter, shape and complete-model checks. + +**Coincident component uncertainty.** The two identical rows are +`(2, 0.25, 0.9)`, or `(2, 0.25, 0.9, 0.4)` for Voigt, on 161 coordinates +from `-4` to `4` x units. With a common unit-height profile P, + +`m(x) = a1*P(x-c) + a2*P(x-c) = (a1+a2)*P(x-c)`. + +For any `t` retaining positive heights, `(a1+t, a2-t)` yields the same model. +The individual-height Jacobian columns are identical; the null direction is +`(1, 0, ..., -1, 0, ...)`. No finite individual-height uncertainty follows from +this rank-deficient local linear approximation, even when residual sum of +squares is zero. The independent fixture check verified the unchanged model +at height split `(2.5, 1.5)` and exactly equal numerical height columns. +The public custom callable returns the known physical exact minimizer, checks +its supplied residual against zero with absolute `2e-12` data units, and returns +no Jacobian/covariance. This removes optimizer convergence as a confounder. + +The result must preserve the correct peak rows, empty background, finite model +and residuals, real full covariance shape, and an observable warning. Captured +warnings are explicitly rendered, including category, message, filename and +line, even if the call fails. Both individual-height diagonal entries must be +nonfinite; either infinity or NaN is allowed. No particular warning category, +wording, covariance algorithm, or encoding is required, and no assertion +requires nonfinite uncertainty for an identifiable combined quantity. The +unchanged identifiable-fit covariance helper is deliberately not used here. +The [contract-linked SciPy covariance reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) +was opened; R3's prohibition on fabricated finite uncertainties and the exact +height-exchange invariant determine this expectation, rather than a particular +SciPy covariance implementation. Finite fabricated height variances, suppressed +warnings, a wrong model, or dropping a component fail independently. + +**Finite numerical boundary.** Let `s = finfo(float).tiny/1024`, observed as +`2.1729236899484e-311` x units, and take `x = s*linspace(-4,4,81)`. +The [NumPy floating-point limits definition](https://numpy.org/doc/stable/reference/generated/numpy.finfo.html) +was opened to establish the positive subnormal domain. The approved +[SciPy Voigt definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.special.voigt_profile.html) +was also opened. Its normalized convolution has the scale property + +`V(s*d; s*sigma, s*gamma) = V(d; sigma, gamma)/s`. + +Consequently the unit-height ratio is unchanged by s. Finite exact data are +formed in order-one coordinates as `3*V(x/s;1,1)/V(0;1,1)`; the physical full +initial row is the exact generating minimizer `(3,0,s,s)`. The test checks +finite inputs, positive widths and strictly increasing coordinates explicitly. +The independent scaled fixture has normalized Jacobian condition number +`11.07356`. Its center sample is exactly `3` data units. Direct public SciPy +normalization at physical s returned infinite center density and 81 nonfinite +ratios, with an unsuppressed RuntimeWarning. This is evidence of a real floating +point limitation, not an invalid input or a patched production optimizer. + +The outcome is intentionally conditional. A numerical fit failure must be +RuntimeError; ValueError, other leaked exceptions or mutated caller inputs fail. +A robust finite solution is allowed, so the test does not force solver failure. +Success must return one real finite positive Voigt row, empty background, +correctly shaped real covariance, finite fitted samples/residuals, and the +verified generating model. Rescale returned center/widths by s before comparing +to `(3,0,1,1)` with the unchanged `rtol=3e-5, atol=2e-5`; this prevents the +ordinary absolute width tolerance from accepting zero or arbitrary subnormal +widths. Independent scale-normalized reconstruction uses `rtol=2e-10, +atol=2e-10`; fitted data and zero residuals use `atol=2e-7` data units, and +`data-fitted` uses the existing `rtol=2e-12, atol=2e-12`. Physical covariance +may itself encounter numerical limits, so no finite-covariance assertion is +added for this outcome. The starting row is already an exact minimizer; the +success alternative imposes no recovery requirement from a distant local basin. +All tolerance choices and both allowed outcomes preceded product execution. +The [contract-linked optimizer reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) +was opened for the public objective/status definitions; R3 determines the +public failure exception type. + +### Focused execution, preservation and classification + +All commands ran from the supplied worktree, using +`/private/tmp/dphtools-spectrum-delivery/venv/bin/python`, with +`PYTHONDONTWRITEBYTECODE=1`, `MPLBACKEND=Agg` and writable +`MPLCONFIGDIR=/private/tmp/dphtools-spectrum-delivery/mplconfig`. No environment +or repository configuration was changed. The known Anaconda-base tooling +repair remained outside this session. + +Retained evidence directory, outside Git: +`/private/tmp/SPECTRUM-FITTING-001-A-coverage-20261005/`. +The authored `launch.py` sets `warnings.formatwarning` to category/message/file/ +line only and sets the uncaught-exception hook to exception-only rendering +before importing pytest. Every fixture probe and product test uses +`-p no:warnings --tb=no -q -ra --capture=tee-sys`. The launcher's explicit exit +status and `set -o pipefail` retain failures. The existing LM child independently +sets the same source-free hooks before its imports. + +Exact focused full-spectrum command: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-delivery/mplconfig /private/tmp/dphtools-spectrum-delivery/venv/bin/python /private/tmp/SPECTRUM-FITTING-001-A-coverage-20261005/launch.py tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-coverage-20261005/spectrum-tests.log +``` + +The authored `format.py` installs the same source-free hooks before importing +Black and invokes Black's public command entry point. Formatting was applied +once to the new additions. Exact final format-check command: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-delivery/mplconfig /private/tmp/dphtools-spectrum-delivery/venv/bin/python /private/tmp/SPECTRUM-FITTING-001-A-coverage-20261005/format.py --check --line-length 99 tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-coverage-20261005/black-check.log +``` + +| Check/evidence | Result | Retained file in evidence directory | +|---|---|---| +| Independent fixture/domain checks | 10 passed in 0.87 s; exit 0; no product import | `fixture-checks.log`; `test_fixture_checks.py` | +| Focused full spectrum | 273 passed in 2.21 s; pytest/shell exit 0; no failures, errors or skips | `spectrum-tests.log`; `launch.py` | +| Numerical boundary outcome | RuntimeError; RuntimeWarning remained visible; input preservation passed | `spectrum-tests.log` | +| Coincident-component outcomes | All three returned warnings and nonfinite individual-height variances | `spectrum-tests.log` | +| Black application/check | One file formatted; final check leaves one file unchanged; both exit 0 | `black-format.log`; `black-check.log`; `format.py` | +| Original-test preservation | All original module AST nodes remain exactly unchanged excluding locations; only a warnings import and three functions were added; exit 0 | `preservation-environment.log`; `tests-before.py` | +| Historical report preservation | Original report bytes retained as an exact prefix | `report-before.md`; `final-identity.log` | + +The spectrum run retained one RuntimeWarning from the finite numerical boundary +and three OptimizeWarnings from covariance estimation. The standalone fixture +check retained its own RuntimeWarning for direct public SciPy normalization. +Diagnostics were not suppressed, and no exact message assertion was used. +Execution environment: Darwin arm64; Python 3.13.12, NumPy 2.5.3, SciPy 1.18.1, +pytest 9.1.1, Black 26.5.1. The final test SHA-256 is +`1ca6c122fa10d628af4e4c00f6c19b3aef948bbff38bcabec147397061bac764`. +The report's own final hash is returned outside this tracked tree. + +**Classification: authorized post-implementation test coverage correction; +focused public checks pass**, high confidence. The observed numerical failure +is contract-permitted RuntimeError, not an input-validation or product defect. +The rank-deficient warnings and nonfinite individual-height variances satisfy +the approved uncertainty boundary. No unresolved requirement was found. A does +not accept its own tests. Fresh independent B round1 review is the next handoff +in this new window. No canonical full coverage measurement or platform matrix +was run, and this report makes no coverage-completion or merge-readiness claim. + +Read only root AGENTS.md, the narrow owner packet, R3, selected design-tests +skill, setup.cfg conventions, the permitted current test/historical report, +the cited primary-reference pages, and this session's own source-free evidence. +No product implementation, private-helper source, history/diff, other tests/ +oracles, PROJECT.md, task state, lesson entries or role conversations were +read. Only the two authorized tracked paths were edited; temporary launchers, +preimages and logs reside outside Git. No Superpowers, optional memory, +delegation, repository rule/config edit, commit, push, PR, broad full +verification, merge or release occurred. **Source exposure: none observed.** +Warnings disclosed product filenames/line numbers as required, but no source +excerpt or source-bearing traceback was rendered. + +Metrics: 33 unchanged scenarios / 273 cases / 10 added cases; 1 fresh root A +launch (4 known A launches including preserved historical sessions), 0 reviewer +or C launches in this session; original B window 2/2 accepted/closed, Black-only +window 1/2 accepted/closed, new correction B window 0/2 used with round1 next; +initial C used, C-repair counter 0/1 unchanged. Measured fixture-authoring/check +interval: 136 s, 18:31:42–18:33:58 UTC on 2026-10-05, excluding earlier permitted +input/reference reading and final report preparation. Added to the retained +702 s of earlier partial measurements, partial measured spending totals 838 s. +No fixed owner time/token cap was supplied; exact token usage and billing are +unavailable. Earlier attempts, spending and allowances were not reset. This +appendix is historical evidence and creates no competing Current state. + +## Post-implementation smallest-positive-width regression — new review window + +Fresh independent root A was authorized to add only the evidenced Gaussian and +Lorentzian numerical-boundary regression. This is **post-implementation +product-red evidence**, not original test-first evidence. The supplied accepted +checkpoint is `9e3962dcfaa2fc60beb67803446e642b5b87ff17`, with test SHA-256 +`1ca6c122fa10d628af4e4c00f6c19b3aef948bbff38bcabec147397061bac764` and historical +report SHA-256 `a6dd1d690176ddc159dbd2479f8a024fcd4c86eda78602dfaf2bfc53f341aec3`. +Both input hashes were confirmed before editing. R3 and all 33 scenarios remain +approved; earlier windows and spending remain retained. This report provides +no independent acceptance or competing Current state. + +### Mapping, fixture legitimacy and allowed outcomes + +One parametrized public test adds two cases, bringing the spectrum file to 275 +cases. The only test additions are `import math`, the independent stable helper, +and the new test; every preexisting test, comment, oracle and tolerance remains +byte-for-byte unchanged after removing those additions. + +| Existing scenarios | Added public test, prefix `test_` | Expectations | +|---|---|---| +| S29; S01/S02 unit-height definitions, S08 no background, S17/S19 finite positive domain, S30 default LM | `s29_smallest_positive_width_point_samples_preserve_inputs` | Two cases: `gauss` and `lorentz`; numerical failure must raise RuntimeError and preserve inputs, or success must independently reproduce the sampled model with valid public outputs and applicable uncertainty warnings | + +Both calls use precisely `x=linspace(-1,1,41)`, zero data except `y[20]=2`, +full list guesses `[[2.0,0.0,nextafter(0.0,1.0)]]`, `background="none"`, and an +omitted optimizer. Height is in data units; center, width and spacing are in x +units. Coordinates, samples and guesses are finite; amplitude and width are +positive; `N=41>P=3`, giving 38 residual degrees of freedom. The float64 width +is `2**-1074`, approximately `4.9406564584124654e-324` x units. NumPy distinguishes +this positive subnormal from the smallest normal value. Its square rounds to +zero; that arithmetic limitation does not invalidate the width. The +[NumPy floating-point limits reference](https://numpy.org/doc/stable/reference/generated/numpy.finfo.html) +was opened independently. + +R3's point-observation convention and elementary unit-height equations establish +the fixture. At the center, both profiles equal one, so the modeled sample is +2. At any other sample, the distance is at least approximately 0.05 x units. +For Lorentzian, `L(d) <= (width/abs(d))**2`, less than approximately `1e-644`; +for Gaussian, `G(d)=exp(-0.5*(d/width)**2)` is still smaller. Both off-center +models round to zero in float64. This is a component narrower than the sampling +distance, not a bin-integrated component or an identifiable-width fixture. +The independent checks reproduced the complete supplied data exactly for both +families and found sampled sensitivity rank one at the starting row. + +The test calls only the real public `spectrum_fit` through the unchanged +input-preservation helper. RuntimeError is allowed without requiring specific +wording or a warning. Any other exception fails, including LinAlgError/ValueError. +Input preservation is checked in `finally`, including failed calls. A successful +return must have one finite real positive physical row, an empty real background, +a real 3-by-3 covariance, and finite real fitted/residual arrays of length 41. +Missing attributes, None, incorrect shapes, nonpositive parameters, inconsistent +or fabricated fitted arrays, and inconsistent residuals fail. No mock, custom +optimizer, production seam, optimizer-failure mandate, or comparison of returned +width/center to the initial row is introduced. + +### Stable reconstruction, tolerances and uncertainty + +The new helper evaluates the approved equations independently without squaring +the physical width. Gaussian uses `exp(log(amplitude)-z**2/2)` with `z=d/width`; +division is bounded before evaluating z. Beyond `abs(z)=64`, even the largest +finite float64 amplitude times the profile rounds to zero, since +`log(max_float)-64**2/2 < -1338 < log(smallest_subnormal)`, approximately -744. +This bound applies only to oracle evaluation, not to accepted physical widths. +Lorentzian uses `scale=max(abs(d),width)`, `t=d/scale`, `u=width/scale`, and +`exp(log(amplitude)+2*(log(width)-log(scale))-log(t*t+u*u))`. +The scaled denominator is at least one. Log evaluation also retains a finite +height-times-profile product when the separate profile would underflow. + +All tolerances were fixed before public product execution. Existing reconstruction +`rtol=2e-10, atol=2e-10` and residual-consistency `rtol=2e-12, atol=2e-12` are +reused. Complete fitted data and zero residuals use existing absolute `2e-7` +data units, allowing solver stopping far above float64 roundoff while rejecting +loss of the height-2 sample or spurious tails. There is no physical-parameter +recovery tolerance for this unidentifiable starting fixture. Covariance symmetry +reuses `rtol=2e-10, atol=2e-12`, allowing symmetric NaN/infinity encodings; +finite diagonal variances must be nonnegative. + +Applicable uncertainty checks use the **returned** row. The helper also evaluates +analytic physical Jacobian columns multiplied by `(amplitude,width,width)`, +then normalizes each nonzero column by its maximum magnitude. These positive +column scalings preserve rank and individual-parameter estimability. For Gaussian +the scaled columns are `(m,m*z,m*z*z)`. For Lorentzian they are +`(m,2*m*t*u/(t*t+u*u),2*m*t*t/(t*t+u*u))`. Rank uses NumPy's documented singular +value decomposition (SVD) threshold `largest_singular_value*max(shape)*eps`, +approximately `9.10e-15*largest_singular_value` here. The +[NumPy rank reference](https://numpy.org/doc/stable/reference/generated/numpy.linalg.matrix_rank.html) +was opened. A parameter is locally unidentifiable when removing its column +leaves rank unchanged: the remaining columns can compensate its local change. +At a deficient returned solution, those diagonal variances must be nonfinite +and an observable warning must exist. Any nonfinite covariance also requires +a warning. A wider full-rank returned solution is allowed. No width is declared +identifiable merely because it is positive, and no singular result is required. + +These conditions follow R3's local-linear uncertainty convention and prohibition +on fabricated finite uncertainties. The opened +[contract covariance reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) +documents covariance's local-linear limits and warnings; it does not specify +the product's internal covariance algorithm. Seven independent source-free +checks, with no product import, verified the environment, exact boundary samples, +ordinary profile values, analytic scaled derivatives against independent +five-point differences, full rank at an ordinary row, and finite products when +a profile alone underflows. Derivative sanity tolerances reuse `rtol=2e-6, +atol=2e-8`; ordinary-model sanity uses `2e-14` relative/absolute tolerance. + +### Execution, preservation and failure classification + +Evidence resides outside Git at +`/private/tmp/SPECTRUM-FITTING-001-A-boundary-lwd67_xk/`. +Every Python launch installs source-free warning and exception hooks before +probe/test imports. `launch.py` supplies `-p no:warnings --tb=no -q -ra +--capture=tee-sys`, prints the pytest exit status, and propagates it. The temporary +`sitecustomize.py` also supplies the hooks to child interpreters; the unchanged +LM child retains its own hooks. Warnings retain category, message, filename and +line. Captured regression warnings are rendered even when the call fails. + +Exact final full-spectrum command from the supplied worktree: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/private/tmp/SPECTRUM-FITTING-001-A-boundary-lwd67_xk MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 /private/tmp/SPECTRUM-FITTING-001-A-boundary-lwd67_xk/launch.py tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-boundary-lwd67_xk/spectrum-tests-final.log +``` + +The focused command adds `-k smallest_positive_width` and uses +`regressions-final.log`. Fixture checks replace the test argument with the +evidence directory's `test_fixture_checks.py` and use `fixture-checks.log`. +Black uses the same environment with `format.py --check --line-length 99 +tests/test_spectrum_fitting.py` and `black-check-final.log`. + +| Check | Result | Evidence file | +|---|---|---| +| Independent fixture/oracle/environment | 7 passed in 0.12 s; exit 0; no product import | `fixture-checks.log`; `test_fixture_checks.py` | +| Final focused regressions | 2 failed, 273 deselected in 0.80 s; pytest/shell exit 1 | `regressions-final.log` | +| Final existing spectrum suite plus additions | 2 failed, 273 passed in 1.33 s; no errors/skips; pytest/shell exit 1 | `spectrum-tests-final.log` | +| Black 99 | One file would be left unchanged; exit 0 | `black-check-final.log` | +| Exact old-test bytes and abstract syntax tree (AST) preservation | Removing only the three additions reconstructs the exact preimage; every old AST node is unchanged | `preservation-final.py`; `final-identity.json` | +| Historical report preservation | All 47,184 preimage bytes remain an exact prefix | `report-before.md`; `final-identity.json` | + +Both new public calls emitted numerical RuntimeWarnings and leaked +`LinAlgError: SVD did not converge`; the unchanged input checks completed +successfully before the exception was re-raised. Independent runtime inspection +of the public NumPy exception type confirmed it subclasses ValueError. Each +regression retained ten overflow warnings and one invalid-value warning. The +full suite also retained the prior Voigt numerical warning and three covariance +OptimizeWarnings; the real LM child exited 0. No failure, warning, count or +exit status was hidden. The seven fixture checks and Black emitted no warning. + +**Classification: product defect — leaked numerical exception for valid public +inputs**, high confidence, subject to fresh independent B validation of the +tests. R3 permits verified success or RuntimeError for numerical failure; it +does not permit an input-validation exception here. The tests reached that +exact boundary, so these are valid post-implementation product-red observations. +No unresolved requirement or environment defect was observed. The opened +[R3 optimizer reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) +supports the residual objective; R3 determines the public exception type. +Fresh B round1 review is required before C repair. A grants no acceptance and +performs no repair. + +The initial focused/full runs returned the same two failures with 273 +deselected/passing cases in 0.85/1.34 s, respectively; their logs remain +`regressions.log` and `spectrum-tests.log`. Initial Black also passed. +After clarifying only the new helper's docstring to describe physical column +scaling precisely, focused/full/Black checks were rerun on the final test bytes. +No numerical oracle or tolerance was calibrated from product results. +The final test SHA-256 is +`8bcfb0a747c6572403d57122dd16e60e08668472b68879014d95aa1c94889b09`. +The old 7,806-node AST has SHA-256 +`df2f48ffe10ada93cb0b3a2fd9d81d2e6341c0795b9916bd4a2bc8efdc5de6f5`. +The report's final hash is returned outside its tracked tree. + +Environment: macOS 27.0 arm64, confirmed non-Conda CPython 3.12.14 at the supplied +path; NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. Interpreter prefix +and base prefix both point to the clean supplied Python. These tests exercised +the working-tree public entry point; warning filenames identify the supplied +worktree. The supplied checkpoint is prior acceptance identity, not a claim +about uninspected product working-tree bytes. + +Read only root AGENTS.md, the owner packet, R3, design-tests skill, setup.cfg, +the permitted spectrum test/report, cited primary-reference pages, and this +session's own source-free evidence. No implementation/private-helper source, +history/diff, other tests/oracles, PROJECT.md, task state, lessons, role +transcripts or raw implementation logs were read. Only the two permitted tracked +paths were edited. No Superpowers, memory, delegation, commit/push/PR, broad full +verification, merge or release occurred. **Source exposure: none observed.** +Required diagnostics revealed filenames/line numbers but no source excerpt or +source-bearing traceback. Full coverage and platform-matrix verification were +not run or claimed. + +Metrics: 33 unchanged scenarios / 275 cases / 2 added cases; 1 fresh root A +launch (5 known A launches), 0 reviewer/C launches in this session; original B +2/2, format B 1/2, coverage B 1/2 all accepted/closed; new regression window +B 0/2 used, round1 next. Initial C used; C repairs remain 0/1 used. Partial +measured authoring/check interval: 192 s, 18:51:37–18:54:49 UTC on 2026-10-05, +excluding earlier permitted reading and final report preparation. Retained prior +838 s plus this interval totals 1,030 s of partial measured spending. No fixed +owner time/token cap was supplied; exact token usage/billing are unavailable. +No scenario approval, review allowance or C repair allowance was reset. + + +## Supplementary physical-covariance unit regression — before the same B round1 + +This fresh independent root A was authorized to supplement the still-unreviewed +A5 numerical-regression correction before one combined B round1. This is +**post-implementation regression evidence**, not original test-first evidence. +R3 and all 33 scenarios remain approved. The supplied accepted checkpoint remains +`9e3962dcfaa2fc60beb67803446e642b5b87ff17` (273 cases). The prior unreviewed A5 +preimages were confirmed exactly before editing: test SHA-256 +`8bcfb0a747c6572403d57122dd16e60e08668472b68879014d95aa1c94889b09`; report SHA-256 +`dd4459a66b1cd9cf6c8f53556da98033c4a0d6c1753eaeada283367ef632efa2`. +All 275 authored cases and all historical report bytes are preserved. This +supplement neither opens another window nor resets scenarios, reviews, spending, +or the C-repair allowance. It grants no independent acceptance or Current state. + +### Mapping and analytic expectation + +One new public test adds one collected case, calling the real fitter under two +x-unit systems; the file now contains 276 cases. + +| Existing scenarios | Added public test, prefix `test_` | Expected behavior | +|---|---|---| +| S09/S31 | `s09_s31_gaussian_physical_covariance_transforms_with_x_units` | One identifiable Gaussian and no background; a legitimate physical custom optimizer returns the independently established local minimizer without a Jacobian/covariance; fitted samples and residuals agree under both units, and the fitter's physical covariance matches the analytic expectation and transforms consistently | + +For `m=A*exp(-z**2/2)`, `z=(x-c)/sigma`, the physical model-Jacobian columns are + +`J_A=exp(-z**2/2)`, `J_c=m*z/sigma`, `J_sigma=m*z**2/sigma`. + +The residual Jacobian is `-J`; this sign cancels in its Gram matrix. R3 gives + +`v=RSS/(N-P)`; `C=v*inv(J.T@J)`. + +The opened [contract-linked SciPy covariance reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) +supports the local-linear approximation and residual-degrees-of-freedom scale. +R3 additionally requires the custom path's Jacobian and covariance in physical +coordinates. These contract terms determine the expectation; no unresolved +units, estimator, or requirement was found. + +Changing x units by a positive factor `s` maps `(A,c,sigma)` to +`(A,s*c,s*sigma)`. With `D=diag(1,s,s)`, the model and residuals are unchanged, +`J'=J*inv(D)`, and therefore + +`C'=v*inv(inv(D)*(J.T@J)*inv(D))=D*C*D`. + +Height variance has squared data units; center/width variances have squared x +units. Their covariance with height has data-times-x units. Every physical +covariance entry is converted back by division by `outer((1,s,s),(1,s,s))`. +The test compares that matrix with the analytic base-unit C, and finally compares +the two converted returned matrices. It specifies neither a finite-difference +step nor a decomposition or internal positivity transform for production. + +### Fixture legitimacy and independent precision checks + +The fixed fixture is `x=linspace(-4,4,201)`, one Gaussian row `(3,0,1)`, and no +background. All inputs are finite, coordinates strictly increase, and the +positive height/width are interior to the public domain. `N=201`, `P=3`, and +`N-P=198`. There are 25 sampling intervals per sigma in either unit system. +This is an ordinary identifiable component, not a narrow unresolved peak. +The second scale is the ordinary finite `s=1e-6`; its sigma is `1e-6` x units +and its sampling interval is `4e-8` x units. + +Small deterministic high-frequency noise is +`u_i=0.01*(sin(1.63*i)+0.6*cos(0.71*i))` in data units. With Q an orthonormal +basis for the analytic Jacobian columns, set `r=(I-Q*Q.T)*u` and `y=m+r`. +Projection gives `J.T*r=0` algebraically. The test uses the represented `y-m`, +including addition roundoff, both to prove stationarity and to scale covariance. +Observed RSS is `0.013791440787719707` squared data units, and the maximum noise +magnitude is `0.015960383757952064` data units. Nonzero noise makes covariance +scale meaningful and discriminating. The normalized gradient projection is +`5.932776029801998e-16`, below the independently fixed `1e-12` bound. + +For half the residual sum of squares, the objective Hessian is +`H=J.T@J-K`, `K=sum_i(r_i*Hessian(m_i))`. At `(3,0,1)`, analytic second derivatives +in `(A,c,sigma)` order are + +`m_AA=0`, `m_Ac=profile*x`, `m_A_sigma=profile*x**2`, +`m_cc=m*(x**2-1)`, `m_c_sigma=m*(x**3-2*x)`, +`m_sigma_sigma=m*(x**4-3*x**2)`. + +Symmetry supplies the other entries. The test requires +`norm(K,2)<0.01*lambda_min(J.T@J)`. By the eigenvalue perturbation bound, H is +positive definite. The observed ratio is `6.750085189827774e-5`; Gram eigenvalues +are approximately `(28.014426,199.400969,315.396977)` and H eigenvalues are +`(28.014499,199.400969,315.398190)`. Thus the stationary physical row is a strict +local least-squares minimizer to floating-point precision. The scaled objective +Hessian is related by congruence with `inv(D)` and remains positive definite. +R3 does not require a global optimum. The column-normalized Jacobian condition +number is `1.931855195700513`, below the fixed bound 3 in both systems; the +unscaled base Jacobian condition number is `3.3553505359410005`. + +Five separate temporary fixture checks ran without importing product code. +They verified the clean interpreter, gradient/curvature proof, analytic first +and second derivatives, exact covariance transform, attainable numerical +precision, and tolerance discrimination. Complex-step derivatives of the +independent elementary Gaussian agree with the analytic columns at relative/ +absolute `2e-14`. Central second differences with base-unit step `1e-4` agree +with all six second derivatives at relative/absolute `2e-6`; their weighted +sum agrees with K within absolute `2e-15`. The exact unit-transform oracle agrees +within relative/absolute `2e-14`. A dimensionless three-point derivative sanity +check at both scales gives normalized covariance agreement within `2e-8`. +These are independent feasibility checks, not prescribed production algorithms. + +The custom callable checks its physical initial vector, bounds, limit, and +supplied residual at the known solution and a nearby physical trial. It makes +two residual calls under `max_nfev=10`, then returns only a copied physical x +and Boolean success. It supplies no Jacobian or covariance. This is a legitimate +minimizing callable under the opened [R3 optimizer reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) +and public protocol; it removes iterative convergence as a covariance confounder. +No production seam or supplied production result is introduced. + +### Tolerances, discrimination and allowed outcome + +All fixture/oracle/tolerance choices preceded the first product call. No tolerance +was calibrated to the owner-supplied bad-output description or observed output. +The existing generic result/schema and input-preservation helpers are reused. +Since the custom callable returns the known row, parameter checks after unit +conversion, exact fitted samples, and nonzero residuals use `rtol=2e-12, +atol=2e-12` in the respective base parameter/data units. These are arithmetic +checks at identical physical parameters, not iterative convergence tolerances. + +Every covariance comparison divides by `sqrt(C_expected[ii]*C_expected[jj])`. +It reuses the existing `rtol=0.01, atol=2e-4` in dimensionless uncertainty units. +For each variance this allows about 1.02% error; nearly zero cross terms have a +small absolute allowance in their natural uncertainty units. This exceeds the +independently demonstrated derivative/oracle roundoff by many orders of magnitude +while retaining physical variance and height-width correlation discrimination. +For this fixture, using RSS/N instead of RSS/(N-P) changes each variance by +`3/201=1.492537%`, beyond that allowance. Independent synthetic wrong expectations +also fail: zero covariance, missing residual scaling, ignored x units, a private +log-amplitude covariance, and deleted height-width correlation. These examples +come from analytic expectation errors, not production output. + +The valid outcome is a successful public result with the verified physical row, +model, residuals and finite identifiable covariance satisfying both analytic and +unit-transform comparisons. No numerical failure or nonfinite covariance is +required or accepted as a substitute here. The simple interior minimizer is +already supplied; unlike the retained A5 extreme cases, this is not a failure- +handling or nonidentifiability fixture. No warning category/message or particular +covariance method is imposed. + +### Source-free execution, preservation and classification + +Evidence is retained outside Git at +`/private/tmp/SPECTRUM-FITTING-001-A-covariance-vgcernwb/`. +Every Python probe/check installs source-free warnings and exception rendering +before probe imports. Temporary `sitecustomize.py` also supplies those hooks to +child interpreters. Warnings show only category/message/filename/line; uncaught +exceptions show only type/message. The pytest launcher uses `-p no:warnings +--tb=no -q -ra --capture=tee-sys`, prints and propagates the exit status, and its +diagnostic plugin emits only failure exception/assertion messages, without +traceback/source excerpts. The unchanged LM child retains its own hooks. + +Exact full-spectrum command from the permitted worktree: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/private/tmp/SPECTRUM-FITTING-001-A-covariance-vgcernwb MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 /private/tmp/SPECTRUM-FITTING-001-A-covariance-vgcernwb/launch.py tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-covariance-vgcernwb/spectrum-tests.log +``` + +The targeted command adds `-k physical_covariance_transforms_with_x_units` and +uses `covariance-regression.log`. Independent checks substitute the evidence +file `test_fixture_checks.py` for the spectrum file and use `fixture-checks.log`. +Black uses the same environment and interpreter with `format.py --check +--line-length 99 tests/test_spectrum_fitting.py`, retaining `black-check.log`. +Preservation uses the same environment with `preservation.py` and retains +`preservation-final.log` plus `final-identity.json`. + +| Check | Observed result | Evidence file(s) | +|---|---|---| +| Independent fixture/oracle/environment | 5 passed in 0.13 s; pytest/shell exit 0; no product import | `fixture-checks.log`; `test_fixture_checks.py`; `analytic-preflight.json` | +| New targeted covariance regression | 1 failed, 275 deselected in 0.83 s; pytest/shell exit 1; no warnings | `covariance-regression.log` | +| Full spectrum file | 3 failed, 273 passed in 1.79 s; no errors/skips; pytest/shell exit 1 | `spectrum-tests.log` | +| Black 99 application/check | File already formatted; final check leaves one file unchanged; both exit 0 | `black-format.log`; `black-check.log` | +| Exact original bytes and abstract syntax tree (AST) | All 41,547 preimage test bytes are an exact prefix; removing only the appended function restores all 275 cases and every prior AST node | `tests-before.py`; `preservation.py`; `final-identity.json` | +| Historical report | All 60,731 preimage bytes are an exact prefix | `report-before.md`; `preservation.py`; `final-identity.json` | + +Both unit systems passed real returned parameter/model/residual checks, custom +protocol checks and caller input preservation. Base-unit analytic covariance +passed. At `s=1e-6`, five of nine covariance entries failed the independently +fixed analytic comparison after conversion back to base units. Normalized +height variance was `1.2122417284` instead of 1; center variance was +`18380.1989992` instead of 1; width variance was `1.3814470827` instead of 1; +height-width correlation was approximately `-0.86815035` instead of +`-0.57735149`. The intended covariance assertion was reached. The final direct +unit-to-unit assertion was not reached because this analytic comparison failed. +These numbers are diagnostic observations only; they supplied no expected value. + +**Classification: product defect — physical covariance changes inconsistently +with x units**, high confidence, subject to fresh B validation. This supplies +valid supplementary S09/S31 product-red evidence within the same unreviewed +window. There is no observed unresolved requirement, environment defect or +fixture defect. A performs no product repair and grants no acceptance. + +The full spectrum file retains both A5 failures: valid smallest-positive-width +Gaussian and Lorentzian calls leak `LinAlgError: SVD did not converge` and retain +ten overflow warnings plus one invalid-value warning each. The 273 prior +accepted cases still pass; their earlier Voigt RuntimeWarning and three +covariance OptimizeWarnings remain visible, and the real LM child exits 0. +No diagnostic, warning, failure count or exit status was suppressed. Fixture and +Black checks emit no warning. No broad full coverage/platform check was run. + +Final test SHA-256: +`c5e51bab70912a674d7242541591d1844939dca981c5b78cbce9de70617accca`. +The preserved 8,881-node A5 AST has SHA-256 +`3091be61d7a8f8000178785c63c7129921d82360bce526cfd025ee3a13ecd4c5` +under CPython 3.12.14, excluding location attributes. Exactly one new top-level +function is appended; no import, original AST node, oracle, tolerance or case +was changed. The report's final hash is returned outside its tracked tree. + +Environment: macOS 27.0 arm64, non-Conda CPython 3.12.14 at the supplied clean +path; interpreter/base prefixes point to that clean Python. NumPy 2.5.3, +SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. The public imports execute the current +worktree product, without source inspection. The supplied checkpoint identifies +prior test acceptance; this session makes no identity claim about uninspected +product working-tree changes. + +Read only root AGENTS.md, the owner packet, contract R3, selected design-tests +skill, setup.cfg conventions, permitted current spectrum test/historical report, +the two cited primary references, and this session's own source-free evidence. +No product source/private helpers/history/diffs, other tests/oracles, PROJECT.md, +task state, lessons, role conversations or raw implementation logs were read. +Only the two authorized tracked files were edited; all supporting evidence lives +in the temporary directory. No Superpowers, memory, delegation, commit/push/PR, +broad full verification, merge or release occurred. **Source exposure: none +observed.** Filename/line diagnostics were retained; no implementation excerpt +or source-bearing traceback was rendered. + +Metrics: 33 unchanged scenarios / 276 cases / 1 supplementary case; 1 fresh root +A launch (6 known A launches), 0 reviewer/C launches in this session. Original +B 2/2, format B 1/2 and coverage B 1/2 remain accepted/closed. The current +numerical-regression window remains B 0/2, round1 next for combined A5 plus this +supplement; C repairs remain 0/1 used. Partial measured authoring/check interval: +217 s, 19:00:37–19:04:14 UTC on 2026-10-05, excluding earlier permitted +input/reference reading and final report/preservation preparation. Retained +1,030 s plus this interval totals 1247 s of partial measured spending. No fixed +owner time/token cap was supplied; exact token usage/billing are unavailable. +No approval, review allowance, or C allowance was reset. One fresh independent +B review is the next handoff before any C repair. + + +## Post-repair numerical-failure coverage correction — new B window + +This fresh independent root A was authorized for the three narrow public needs +in the owner packet. R3 and all 33 scenarios remain approved. This is +**post-repair test-correction evidence**, not original test-first evidence. +The owner supplied accepted checkpoint +`f70661a9ed1813312e5bc409fae8a288c8c3d025`, test SHA-256 +`c5e51bab70912a674d7242541591d1844939dca981c5b78cbce9de70617accca`, and report +SHA-256 `2bd7819d6c6112d2a6660fd03cd5f71685613565f11081fa8b340f3647a8ef3c`. +Both preimage hashes were confirmed before editing. The 276 old cases and all +76,378 historical report bytes remain preserved. No implementation, private +coverage evidence, or other role conversation supplied the new expectations. +This appendix adds no competing Current state and grants no acceptance. + +### Mapping and independently allowed outcomes + +Three appended public test functions add five cases, for 281 total. All map to +existing S29/S31, using the R3 Gaussian, no-background, finite-domain and +physical-covariance conventions. No new scenario or behavior was added. + +| Added test, prefix `test_` | Cases | Independent expectation | +|---|---:|---| +| `s29_s31_extreme_finite_height_exact_custom_minimum` | 2 | Heights `1e200` and largest finite float, nine integer coordinates from -4 to 4, exact Gaussian row; RuntimeError numerical failure or independently checked exact-model success, with valid covariance and input preservation | +| `s29_s31_large_noise_units_unrepresentable_height_variance` | 1 | Well-conditioned stationary Gaussian with finite data and a data-unit factor `1e160`; RuntimeError or verified success with a warning and nonfinite height variance, without fabricated finite uncertainties | +| `s29_s31_public_numerical_backend_failure` | 2 | A documented NumPy decomposition fails on valid finite operands inside the selected custom optimizer or during covariance calculation; RuntimeError with input preservation, or an independently verified mathematically valid successful result | + +All product calls use only `dphtools.utils.fitfuncs.spectrum_fit`, through the +unchanged `_preserving_call` helper. It checks data, coordinates and full guesses +in `finally`, on success and failure. No production helper, module patch, seam, +supplied covariance, or optimizer-supplied Jacobian is used. A None successful +return, an input-validation exception, a leaked backend exception, or mutation +cannot satisfy the numerical-failure helper. No product timeout is imposed. +The owner-supplied earlier stalled probe remains an incomplete observation; +this session does not claim that it completed or supplied a result. + +### Exact-height and noise-projection proofs + +For the extreme-height cases, `P=exp(-x*x/2)`, `y=A*P`, and the full initial +row is `(A,0,1)`. Every sample is finite because `0&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-postrepair-xm4t1pf2/spectrum-tests-exact-final.log +``` + +Black uses the same interpreter/environment with `format.py --check --line-length +99 tests/test_spectrum_fitting.py`, and `black-check-exact-final.log`. Independent +checks substitute the evidence file `test_fixture_checks.py` for the spectrum +file, retaining `fixture-checks.log`. The initial focused command adds +`-k 'extreme_finite_height or large_noise_units or public_numerical_backend_failure'` +and retains `new-cases.log`. Preservation uses `preservation.py`, retaining +`preservation-final.log`, `preflight.json` and `final-identity.json`. + +| Check | Result | Evidence file | +|---|---|---| +| Independent fixture/environment/oracle checks | 6 passed in 0.64 s; exit 0; no product import or warnings | `fixture-checks.log`; `analytic-preflight.json` | +| Initial new cases | 1 failed, 4 passed, 276 deselected in 0.67 s; exit 1 | `new-cases.log` | +| Final full spectrum on exact formatted test bytes | 1 failed, 280 passed in 1.38 s; pytest/shell exit 1; no errors/skips | `spectrum-tests-exact-final.log` | +| Black 99 on exact final test bytes | One file would be left unchanged; exit 0 | `black-check-exact-final.log` | +| Original tests | All 45,853 preimage bytes are an exact prefix; original AST nodes/constants/oracles/tolerances are unchanged | `tests-before.py`; `preservation-final.log`; `final-identity.json` | +| Historical report | All 76,378 preimage bytes remain an exact prefix | `report-before.md`; `preservation-final.log`; `final-identity.json` | + +The `1e200` case returned a verified exact-model fit with zero covariance. Largest +finite height retained an overflow RuntimeWarning and returned RuntimeError. +Large noise units returned RuntimeError for unrepresentable covariance. +The optimizer boundary reached the verified finite `(201,3)` J and residual, +then leaked `LinAlgError: Numerical decomposition did not converge (injected)`. +Caller-input checks completed before the leak. The covariance boundary reached +a finite `(201,3)` operand and returned RuntimeError. Its alternative successful +path is independently checked but was not the observed product outcome. + +**Classification: product defect — numerical failure inside a selected custom +optimizer leaks LinAlgError instead of RuntimeError**, high confidence, subject +to fresh independent B validation of the injection and tests. This is valid +post-repair product-red evidence at S29/S31. The other new outcomes satisfy R3. +All 276 previously accepted cases pass on this run. No test, fixture or environment +failure explains the optimizer leak; A performs no product repair. The full run +retains ten RuntimeWarnings and three OptimizeWarnings, including the new height +warning and previous numerical/uncertainty warnings. The real LM child exits 0. + +All attempts remain retained. An initial Black check passed. Adding an injection +diagnostic print subsequently caused a format-only test defect; the intermediate +full run still had 1 failed/280 passed in 1.45 s, and intermediate Black exit was +1 (`spectrum-tests-final.log`, `black-check-final.log`). Formatting only appended +bytes corrected it; exact complete AST equality was asserted, then full-spectrum +and Black checks were rerun on the final bytes as above. No oracle or tolerance +changed. Final test SHA-256 is +`b241e8d23eda41ecdc3f95ed6d9641007d58e5e000a78947243a8e43e9ed9417`. +The report's final hash is returned outside its tracked tree. + +Environment: supplied non-Conda CPython 3.12.14, NumPy 2.5.3, SciPy 1.18.1, +pytest 9.1.1, Black 26.5.1; interpreter/base prefixes both identify the supplied +clean Python. No package, environment or repository configuration edits occurred. +Public imports execute the worktree entry point; no source bytes were inspected. +The supplied checkpoint identifies prior acceptance, not the uninspected current +product candidate. Full coverage/platform-matrix verification was not run or +claimed. These passing additions alone do not prove complete coverage. + +Read only root AGENTS.md, the owner packet, contract R3, selected design-tests +skill, setup.cfg conventions, the permitted spectrum test/historical report, +opened primary references, and this session's own source-free evidence. No +implementation/private helper source, source/history/diff, private coverage data, +other tests/oracles, PROJECT.md, task state/role packet, existing lessons, or role +conversation was read. Only the two authorized tracked files were edited. No +Superpowers, optional memory, delegation, commit/push/PR, broad full verification, +merge or release occurred. **Source exposure: none observed.** Diagnostics retain +filenames/line numbers without implementation excerpts or source-bearing tracebacks. + +Metrics: 33 unchanged scenarios / 281 cases / 5 added cases; 1 fresh root A launch +(7 known A launches), 0 reviewer/C launches in this session. Owner-supplied original +B 2/2, format B 1/2, earlier coverage B 1/2 and numerical B 1/2 are accepted/closed. +This new post-repair correction window has B 0/2 used, round1 next. Initial C and +C repair 1/1 are used; this work grants no further repair. Partial measured +authoring/check interval: 192 s, 19:32:59–19:36:11 UTC on 2026-10-05, excluding +earlier input/reference reading and final report/preservation preparation. Retained +1,247 s plus this interval totals 1,439 s of partial measured spending. No new +budget cap was supplied; exact token usage/billing remain unavailable. No approval, +attempt, spending, scenario or allowance was reset. Fresh independent B round1 +review is the next handoff; any further C repair needs owner allowance extension. + +## Supplementary large-origin narrow-width public boundary — same post-repair window + +This fresh independent root A was authorized for one S29 boundary supplement, +concurrently with A7 authoring other numerical boundaries. This is supplementary +post-implementation public-boundary evidence. It is not original test-first +red evidence or independent acceptance. Owner approval of R3 and all 33 scenarios +is unchanged. The supplied accepted checkpoint is +`f70661a9ed1813312e5bc409fae8a288c8c3d025` (276 cases), with accepted public test +SHA-256 `c5e51bab70912a674d7242541591d1844939dca981c5b78cbce9de70617accca` and +accepted public report SHA-256 +`2bd7819d6c6112d2a6660fd03cd5f71685613565f11081fa8b340f3647a8ef3c`. +Both snapshots were hashed before any public probe. Original, format, coverage, +and prior numerical windows remain accepted/closed. This supplement shares A7's +existing post-repair test-correction window, before combined B round1/2. Initial +C and the sole C repair are used (repair1/1). No allowance/window/spending reset, +new budget cap, repair authorization, or competing Current state is introduced. + +### Mapping and independent mathematical proof + +One parametrized public test, `test_s29_large_origin_narrow_width_point_boundary`, +adds four variants of existing S29: Gaussian/Lorentzian, each with a legitimate +exact-minimizer custom callable or the omitted default LM optimizer. Supporting +expectations are R3's S01/S02 point-model definitions, S08 no-background schema, +S17/S19 finite positive domain, S30 default LM, S31 physical callable protocol, +and local-linear covariance/warning convention. No scenario is added. + +The fixture is precisely `x=1e12+arange(9)`, full row `(2,x[4],1e-6)`, and +`background="none"`. Height has data units; origin, center, width and coordinate +spacing have x units. The test independently checks all finite inputs, strictly +increasing coordinates, positive height/width, and `N=9>P=3` (six residual +degrees of freedom). Coordinate spacing is 1 x unit. Around this origin, float64 +representable spacing is `2**-13=0.0001220703125` x units, approximately 122 +widths; adding or subtracting `1e-6` x units from the center leaves its represented +value unchanged. This establishes a real numerical boundary without choosing any +production derivative step, method, or failure wording. + +Let `d=x-center`, `w=1e-6`, and `A=2`. R3 gives + +`m_G=A*exp(-0.5*(d/w)**2)`; `m_L=A*w**2/(d**2+w**2)`. + +At `d=0`, both samples are exactly 2 data units. Every off-center sample has +`abs(d)>=1`. The Gaussian exponent is at most `-5e11`, so its float64 point +samples are zero. Lorentzian adjacent samples are approximately `2e-12` data +units; endpoint samples are approximately `1.25e-13`. They remain positive and +finite. Data use the accepted independent `_stable_narrow_component` equation +helper. An independent 80-digit Decimal calculation of the Lorentzian model +and analytic scaled sensitivities agrees within relative `2e-14`; observed +maximum model relative error is `2.625329092575691e-15`. Gaussian samples and +sensitivities are checked exactly against the analytic zero-tail result. No +product helper or optimizer output supplies numerical truth. + +The full row generates these point observations. Thus mathematical residuals +are zero and RSS=0, the global lower bound of the unweighted sum of squares. +It is a minimizer even when parameters are weakly identifiable. The real custom +optimizer checks the physical initial vector, bounds `(0,-inf,0)` / all positive +infinities, evaluation limit 10, and finite correctly shaped supplied residuals. +It evaluates one physical residual vector and returns only a copied physical +minimizer and Boolean success. It supplies no derivative or covariance. The +other two cases call the real omitted default optimizer. No mocks, production +seams, private coordinates, private derivative recipe or synthetic failure are +used. + +### Tolerances, discrimination and allowed outcomes + +All proposal equations, data, tolerances, assertions and allowed outcomes were +fixed before public execution. The custom residual at the known minimizer must +satisfy `abs(r_i)<=2e-12*abs(y_i)+8*smallest_subnormal` in data units. This accepts +arithmetic roundoff while resolving the Lorentzian tails instead of comparing +them with an order-one absolute allowance. + +Success reconstruction uses the accepted independent stable helper at the +**returned** physical row, with `rtol=2e-10` and absolute floor +`8*smallest_subnormal`, approximately `3.95e-323` data units. These are model +arithmetic checks at identical parameters; an 80-digit sanity check establishes +precision well within this allowance. Existing absolute `MODEL_ATOL=2e-7` +data units applies to fit quality and zero residuals, allowing solver stopping. +Existing residual consistency `rtol=2e-12, atol=2e-12` checks `data-fitted`. +No exact returned center/width/height recovery is required. Independent Gaussian +rows with widths `1e-6`, `1e-4` and `1e-2` produce identical point samples and +rank-one sampled sensitivity, demonstrating why width recovery is inappropriate. + +Temporary independent discriminator checks reject zero Lorentzian tails in the +reconstruction check, a lost center sample, Lorentzian width `1e-3` with spurious +fitted tails, peak area substituted for peak height, and the wrong residual sign. +Ordinary equivalent direct Lorentzian arithmetic passes the fixed reconstruction +tolerance. These are mathematical incorrect-output examples, not production +mutations or tuning to observed output. + +A numerical fit inability may raise RuntimeError. There is no mandatory failure, +warning category/message, or exception wording. Other exceptions fail, including +ValueError and leaked linear-algebra errors. Input preservation is checked in +`finally` on both outcomes. A successful return must have one finite real positive +row, empty real background, finite real fitted samples/residuals, real 3-by-3 +covariance, independently consistent point model/residuals, symmetry with +`rtol=2e-10, atol=2e-12` including symmetric nonfinite encodings, and nonnegative +finite diagonal entries. + +The reused helper analytically evaluates physical model-Jacobian columns scaled +by `(A,w,w)`. Gaussian columns are `(m,m*z,m*z*z)` with `z=d/w`; Lorentzian +columns are `(m,2*m*d*w/(d*d+w*w),2*m*d*d/(d*d+w*w))`. The residual sign cancels +in the covariance Gram matrix. Positive column scaling and normalization preserve +rank/estimability. Rank is assessed at the **returned** row. If deficient, +parameters whose column removal leaves rank unchanged need nonfinite diagonal +uncertainties and an observable warning. Any nonfinite covariance also needs an +observable warning. These checks reuse the accepted public uncertainty boundary. + +At the initial row, Gaussian rank is 1: center and width sensitivities vanish, +so their individual local-linear uncertainties cannot be finite. Lorentzian +normalized rank is 3 with condition number `1.4688443954432964`: its tiny positive +tails carry formal sensitivity even though center perturbations can be below +coordinate representation. A robust analytical/scaled calculation may succeed; +finite uncertainty is not forbidden solely by small physical units or large +origin. Returned alternatives are assessed without requiring either initial +rank or a particular covariance computation. The identifiable covariance +helper demanding finite entries is deliberately not used here. + +### Sequential boundary and retained checks + +All initial proposal work and probes lived in +`/private/tmp/SPECTRUM-FITTING-001-A-origin-r8f18rb3/`. Only the accepted B5 public +snapshots were read before A7's completion marker. The frozen proposal SHA-256 +is `ae4a51de1fb0391a94dbd71f0d71f078d83299a4417f12b5b0bb40edca42df3d` (4,608 +bytes), recorded before the public probe at 2026-10-05 19:37:30 UTC. Mutation +waited for `/private/tmp/dphtools-spectrum-delivery/A7-editing-complete.json`; +exact marker/live hashes and preimages are retained in this evidence directory. + +The marker identified A7 test SHA-256 +`b241e8d23eda41ecdc3f95ed6d9641007d58e5e000a78947243a8e43e9ed9417` and report +SHA-256 `270b0d4b6eaa1daed5f02fa5ddef6cf1c61a7816a6d33eebe5dbdef8727b6d9e`. +Both exact live identities were verified before interpreting current public +content or mutating either file. Marker bytes were retained and rechecked. +The 57,441-byte A7 test preimage and 94,978-byte A7 report preimage remain exact +prefixes; each also retains its B5 accepted snapshot as an exact prefix. +Appending only the frozen proposal plus two separator newlines added 4,610 test +bytes and one top-level function. Removing only that function restores the +complete 12,276-node A7 abstract syntax tree (AST), excluding location attributes, +SHA-256 `16fe12017fed883d88f5e62e9082e89cae98d74a996b4ebe55186bae263eb4ad` under +CPython 3.12.14. Every prior decorator, constant, helper, oracle, tolerance and +case is unchanged. The final file contains 285 cases: 276 accepted, A7's five, +and this supplement's four. No unexplained changed input was observed. + +Every Python probe/check installed category/message/file/line-only warning +rendering and exception-only rendering before imports. All pytest runs used +`-p no:warnings --tb=no -q -ra --capture=tee-sys`. A diagnostic plugin printed +failure identifiers and exception/assertion type/message without traceback or +source. Captured boundary warnings were explicitly rendered in `finally`. +Pytest and pipeline failure statuses were retained. The existing real LM child +has its own source-free hooks. No temporary sitecustomize or environment repair +was needed in this session; only task-local launchers and command-local variables +were used. + +Exact final spectrum command from the authorized worktree: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 /private/tmp/SPECTRUM-FITTING-001-A-origin-r8f18rb3/launch.py tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-origin-r8f18rb3/spectrum-tests-final.log +``` + +Black used the same command-local environment/interpreter, with +`format.py --check --line-length 99 tests/test_spectrum_fitting.py`, retaining +`black-check-final.log`. Independent fixture checks and the proposal probe used +`launch.py` with respectively `test_fixture_checks.py` and `test_proposal.py` +under the evidence directory and `--confcutdir` set to that directory. +Preservation used `preservation.py`, retaining `preservation-initial.log`, +`preservation-final.log`, and `final-identity.json`. Black formatting was applied +only to new temporary proposal/check files; tracked preimages were never reformatted. + +| Check | Actual result | Evidence in the temporary directory | +|---|---|---| +| Independent fixture/geometry/oracle/discrimination checks | 5 passed in 0.59 s; pytest/shell exit 0; no product import or warnings | `fixture-checks.log`; `gauss-proof.json`; `lorentz-proof.json`; `environment.json` | +| Smallest frozen public proposal, before live access | 4 passed in 0.63 s; pytest/shell exit 0; all four RuntimeError outcomes; one retained RuntimeWarning each | `proposal-probe.log`; `proposal-frozen.json` | +| Combined temporary candidate format preflight | Black 99 exit 0; one file unchanged; no tracked mutation | `candidate-black-check.log` | +| Whole final spectrum file | 1 failed, 284 passed in 1.77 s; pytest/shell exit 1; no errors/skips | `spectrum-tests-final.log` | +| Exact final tracked Black 99 | Exit 0; one file unchanged | `black-check-final.log` | +| Sequential inputs / exact prefix and AST proof | Exact A7 marker/live identities; all accepted and A7 bytes/AST preserved; one function/four cases appended | `sequential-preflight.json`; preimages; `preservation-final.log`; `final-identity.json` | + +All four supplementary cases reach a contract-permitted RuntimeError, preserve +caller inputs, and retain an invalid-value RuntimeWarning. Both robust success +alternatives are specified independently but were not observed against the +product. No failure or warning is required merely because the current candidate +fails numerically. **Classification for this supplement: authorized public +numerical-boundary coverage, passing initially**, high confidence, subject to B's +independent expectation/tolerance review. No product-red claim is manufactured. + +The single combined-file failure remains A7's +`test_s29_s31_public_numerical_backend_failure[optimizer_lstsq]`: a valid finite +custom-optimizer decomposition boundary leaks +`LinAlgError: Numerical decomposition did not converge (injected)`. +**Classification: product defect — leaked selected-optimizer numerical failure**, +subject to independent B validation of A7's fixture/injection and expectations. +All 276 accepted cases pass, four other A7 cases pass, and all four supplementary +cases pass. The full run retains 14 RuntimeWarnings and three OptimizeWarnings; +the real LM child exits 0. No failure, diagnostic, warning, count, skip, or status +was hidden. A performs no product repair; the supplied C repair1/1 is exhausted. + +All local attempts remain retained. Initial temporary formatting of two files +failed, exit 123 (`proposal-black.log`), because this session's Black wrapper +lacked a multiprocessing main guard. This is a **temporary tooling-script defect**, +not a product or environment defect. The task-local wrapper was corrected; the +next formatting attempt passed (`proposal-black-corrected.log`). No product +probe, oracle/tolerance change, repository/environment edit, or implementation +source exposure occurred in that failed attempt. Independent/public checks then +ran once on the frozen proposal. No later test edits invalidated them. + +Final test SHA-256: +`c3151ae5fd74a315ad185b6fbc3d4b137909f87a275ce927e5ff4137dcd6f43c`. +The report's final SHA-256 is returned outside its tracked tree. Public imports +exercise the working-tree entry point; the accepted checkpoint identifies prior +test acceptance and makes no identity claim about uninspected product bytes. +Environment evidence: supplied non-Conda CPython 3.12.14, macOS 27.0 arm64, +NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. Interpreter and base +prefixes both identify the supplied clean Python. + +Read only root AGENTS.md, selected design-tests skill, owner packet, R3, accepted +B5 public snapshots, the coordinator completion marker, current permitted public +tests/report only after exact marker verification, and this session's own evidence. +No implementation/private-helper source, history/diff, coverage report, other +preexisting tests/oracles, PROJECT.md, task state/role packet, lessons or role +transcript was read. Only the two authorized tracked paths were edited. No +Superpowers, memory, delegation, config/rules/environment edit, commit/push/PR, +full verification, merge or release occurred. **Source exposure: none observed.** +Warning and failure diagnostics retained filenames/line numbers without product +source excerpts or source-bearing traceback. Mathematical sensitivity probes +constrain no production derivative algorithm. Full coverage and platform-matrix +measurement were neither run nor claimed; B must review the combined additions. + +Metrics: 33 unchanged scenarios / 285 cases / 4 supplementary cases; 1 fresh root +A launch (8 known including six in B5 history and A7), 0 reviewer/C launches in +this session. Original/format/coverage/prior numerical windows remain +accepted/closed. This is the same post-repair correction window as A7, B 0/2 +used, combined round1/2 next. Initial C and repair1/1 remain used. Measured partial +authoring/probing/sequential-check interval: 288 s, 19:35:22–19:40:10 UTC on +2026-10-05, including initial draft preparation and the marker boundary, excluding +earlier permitted input reading and final report completion. Prior recorded +1,439 s plus this interval totals 1,727 s of partial measured spending; overlapping +root sessions are counted as agent effort, not elapsed wall time. All prior +attempts/spending are retained. No fixed owner time/token cap or new cap is +introduced; exact model-token usage/billing is unavailable. Combined fresh B +round1 is the next handoff; A grants no acceptance or further C repair allowance. + +## Correction of the two B6 test defects — same post-repair window + +Fresh independent root A was authorized only to correct the two evidenced B6 +observers. This is post-repair test correction, not original test-first evidence. +The owner packet reports B6 round1/2 rejection of these restrictions, a spectrum +run of 284 passed/1 failed, 16 independent checks and passing Black 99. It also +confirms that the selected custom optimizer's LinAlgError leak is valid product-red +evidence. That report/transcript was not read. R3 and the 33 approved scenarios +remain unchanged. B round1 is spent; fresh independent B round2 is next. C +repair1/1 remains exhausted. The coordinator's extra-repair request supplies no +authorization in this packet. No window, allowance, spending or budget cap resets. +This appendix is historical evidence, without acceptance or competing Current state. + +Confirmed rejected preimages: test SHA-256 +`c3151ae5fd74a315ad185b6fbc3d4b137909f87a275ce927e5ff4137dcd6f43c`; report SHA-256 +`d001e99a794331c5e6031814c0cca2f05673a8e57b0a7cdfd450db94bccb3cf7`. +Both exact preimages and all local attempts are retained outside Git. The B5 +accepted snapshots retain hashes `c5e51bab70912a674d7242541591d1844939dca981c5b78cbce9de70617accca` +and `2bd7819d6c6112d2a6660fd03cd5f71685613565f11081fa8b340f3647a8ef3c`. + +### Explicit historical corrections and unchanged mapping + +The earlier A7 restriction to two-dimensional covariance SVD operands was a +**test defect**. Its statement that this described the documented input domain +was incorrect. [NumPy's SVD documentation](https://numpy.org/doc/stable/reference/generated/numpy.linalg.svd.html), +opened in this session, permits operands with at least two dimensions and applies +decomposition to stacked matrices. The injection now accepts `ndim>=2`, while +retaining real, finite, nonempty operand checks. The custom optimizer's independently +derived least-squares context still requires a two-dimensional matrix exactly +equal to the analytic Jacobian and the independently known residual. Its adapter +and those exact comparisons are unchanged. Input preservation, reached-boundary +failure checks, RuntimeError classification, recovery/alternate-algorithm success, +and the physical model/covariance oracle are unchanged. + +The A7 large-unit statement that height variance must always be nonfinite with +a warning was also a **test defect**. The physical variance exceeds float64 range; +R3 does not promise a float64 covariance dtype. That historical mapping, overflow +wording and mandatory-warning claim are superseded by this correction: nonfinite +height variance and an observable warning are required when its physical variance +exceeds the actual returned covariance dtype's maximum. A correct representable +finite variance may succeed without a warning. Every finite covariance entry still +has to satisfy the physical analytic expectation, symmetry and nonnegative +variances; all nonfinite encodings still require an observable warning. Exact +zero-residual identifiable covariance still has to be exactly zero. + +The observer now uses `np.log(np.finfo(covariance.dtype).max)`. It takes the logarithm +before any narrowing conversion, so a valid wider maximum is not first converted +to float64 infinity. [NumPy's finfo documentation](https://numpy.org/doc/stable/reference/generated/numpy.finfo.html), +opened here, defines limits for the supplied dtype and documents platform variation +in longdouble. The success diagnostic now says physical covariance rather than +claiming variance overflow for every possible dtype. This changes no assertion +outside the two rejected observers. Earlier A7/A8 attempts, restrictions and +claims remain in their historical bytes; this appendix explicitly corrects them. + +| Existing mapping | Corrected observer | Cases/tolerances | +|---|---|---| +| S29/S31, `test_s29_s31_public_numerical_backend_failure[covariance_svd]` | Documented stacked SVD domain; legitimate optimizer matrix context retained | Same two boundary cases and all physical failure/success tolerances | +| S29/S31, the extreme-height and large-noise-unit tests | Actual returned covariance dtype's representable range | Same three cases; unchanged finite-entry `rtol=0.01, atol=2e-4` in independent uncertainty units | + +No functions, parameterizations, fixtures or scenarios were added to the public +suite: 285 cases remain, including all 276 accepted cases and all nine A7/A8 +additions. The four A8 boundary variants and their oracles/tolerances are unchanged. + +### Host-independent range proof and both-direction observer checks + +The unchanged stationary Gaussian yields base height variance +`C_AA=2.357879943719217e-6` squared base data units. For data-unit factor `s=1e160`, +`D=diag(s,1,1)` and `C_physical=D*C*D`, so + +`V=C_AA*s*s=2.3578799437192171e314` squared physical data units. + +A 90-digit Decimal calculation uses exact conversions of the represented base +variance and unit factor. It establishes `log10(V)=314.372521688330045...`, whereas +`log10(float64_max)=308.254715559916744...`. Thus float64 cannot represent V. +For a mathematical binary format with 113 significant bits and maximum exponent +16384, the largest finite value is `(2-2**-112)*2**16383`, with decimal logarithm +`4932.075448958667902...`. Hence `float64_max < V < max_wide`. Rounding at relative +precision `2**-112` is far below the fixed 1.02% diagonal-variance allowance. +The old threshold would reject that mathematically valid finite covariance; +the corrected threshold permits it, subject to the same physical-entry comparisons. +This is a mathematical possible-format proof, not a claim about local availability. + +Local ARM longdouble has maximum exponent 1024, equal to float64. No local wider +result, product wide-dtype success, or additional runtime dependency is claimed. +The retained proof includes a real wider-longdouble observer check only when +such a dtype exists; that conditional branch did not execute here and no pytest +skip hides it. Separate narrow instrumentation verifies that the observer queries +the actual dtype and passes its maximum directly to NumPy logarithm without +float64 narrowing; it is explicitly instrumentation, not a synthetic host dtype. + +The 25 independent observer checks do not import product code. They demonstrate: +valid SVD reconstruction and analytic covariance for `(201,3)`, `(1,201,3)` and +`(2,1,201,3)`; successful recovery after injected failure; legitimate RuntimeError +translation at both boundaries; valid covariance through an alternate algorithm; +continued rejection of an optimizer LinAlgError leak and vector/empty/nonfinite/ +complex decomposition operands; preserved exact optimizer matrix/residual context; +actual float32/float64/longdouble limit lookup and valid finite physical covariance; +honest warned nonfinite encodings when range is exceeded; rejection of missing +warnings, saturated maximum/zero/unit fabricated height variance, doubled width +variance and nonzero covariance for exact zero residual; and the mathematical +range/nonnarrowing proofs. All equations and tolerances precede product execution. +These checks are evidence outside the public suite, not new contract scenarios. + +### Exact checks, retained attempts and classification + +Evidence directory: `/private/tmp/SPECTRUM-FITTING-001-A-B6-correction-20261005/`. +Every Python launch/probe/test installs category/message/filename/line-only +`warnings.formatwarning` and exception-only rendering before probe imports. Pytest +uses `-p no:warnings --tb=no -q -ra --capture=tee-sys -o addopts=`. The diagnostic +plugin prints only failed identifiers, exception type and message. Captured +warnings and the unchanged LM child retain source-free rendering. Exit statuses +are propagated directly; stdout/stderr are retained without a masking pipeline. +No sitecustomize, environment, repository config or rule file was edited. + +Exact whole-spectrum command, from the authorized worktree: + +```sh +PYTHONDONTWRITEBYTECODE=1 MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 /private/tmp/SPECTRUM-FITTING-001-A-B6-correction-20261005/launch.py /private/tmp/SPECTRUM-FITTING-001-A-B6-correction-20261005/spectrum-results.json tests/test_spectrum_fitting.py > /private/tmp/SPECTRUM-FITTING-001-A-B6-correction-20261005/spectrum-tests.log 2>&1 +``` + +Black uses the same interpreter and command-local variables with +`run.py black --check --line-length 99 tests/test_spectrum_fitting.py`. +Observer checks use `launch.py observer-results.json test_observer_proofs.py` +with absolute evidence paths and `--confcutdir` set to the evidence directory. +Preservation and count checks use `run.py preservation.py` and +`run.py outcome_summary.py`; all scripts/logs are retained in that directory. + +| Check | Actual result | Evidence | +|---|---|---| +| Independent observer/proof checks | 25 passed in 0.67 s; pytest/process exit 0 | `observer-checks.log`, `observer-results.json`, `mathematical-range-proof.json` | +| Exact whole spectrum | 284 passed, 1 failed in 1.43 s; pytest/process exit 1; no errors/skips | `spectrum-tests.log`, `spectrum-results.json` | +| Exact Black 99 | One file would be left unchanged; exit 0 | `black-check.log` | +| Accepted case membership/outcomes | All 276 accepted cases passed; total 285 | `outcome-summary.log`, `outcome-summary.json` | +| Preservation | Accepted 45,853 test bytes, every accepted AST node/oracle/tolerance unchanged; only authorized observer AST changes; all other A7/A8 AST unchanged | `preservation-corrected.log`, `final-identity.json` | +| Historical evidence | All 111,341 rejected report bytes retained as exact prefix, including all 76,378 accepted report bytes | `report-before.md`, `final-identity.json` | + +The sole public failure is still +`test_s29_s31_public_numerical_backend_failure[optimizer_lstsq]`: +`LinAlgError: Numerical decomposition did not converge (injected)` on the valid +finite `(201,3)` analytic Jacobian and independent residual. Input-preservation +checks complete before propagation. **Classification: product defect — leaked +selected-optimizer numerical failure instead of RuntimeError**, high confidence; +fresh B round2 must review the corrected candidate. Neither covariance observer +defect explains or weakens this separate red evidence. No production repair occurred. + +The other appended cases satisfy R3. Extreme height `1e200` returns a verified +exact fit; maximum finite height and large-noise units return RuntimeError. +Covariance SVD reaches a finite `(201,3)` operand and returns RuntimeError. All +four A8 variants return RuntimeError. The full log retains 14 RuntimeWarnings +and three OptimizeWarnings; the real LM child exits 0. No warning category/message, +failure count, exception or status was hidden. + +One initial preservation attempt failed with source-free `IndexError: list index +out of range`, exit 1 (`preservation-initial.log`). **Classification: temporary +tooling-script defect**: the local launcher dropped the script's argument while +forwarding it to runpy. Correcting only that launcher yielded preservation exit 0 +(`preservation-corrected.log`). The failed attempt remains retained. This changed +no product, public test, oracle, tolerance, environment or repository configuration. + +Final test SHA-256: +`4c40f4cf5bac99b3d47d4f3bb1fcf18ae366941b23428dc0bc9fbfa7bf81cafe`. +The final report hash and exact final preservation results are returned outside +its tracked tree. Accepted module AST hash under CPython 3.12.14 is +`ee2bc688f9c90d1b4044f21223d23f72237b36b31b5b64d1b28d9863f0053b80` +(9,790 nodes, locations excluded). No tracked formatting was applied. + +Observed host: supplied clean non-Conda CPython 3.12.14, macOS 27.0 arm64, +NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1 and Black 26.5.1. Interpreter/base prefixes +identify the supplied clean Python. Public execution exercises the uninspected +worktree product; accepted checkpoint identity is prior test acceptance, not a +claim about current product bytes. Wider-range runtime execution, full coverage +and Linux/macOS/Windows verification were not performed or claimed. + +Read only root AGENTS.md, design-tests skill, owner packet, R3 public contract, +current permitted spectrum tests/historical report, B5 accepted public snapshots, +the two opened NumPy references, and this session's own evidence. No implementation, +private helpers/source/history/diffs, private coverage, other preexisting tests/ +oracles, PROJECT.md, task state/role packets, lessons or role transcripts were read. +Only the two authorized tracked files changed. No Superpowers, memory, delegation, +environment/config/rules edits, commit/push/PR, full verification, merge or release +occurred. **Source exposure: none observed.** Diagnostics disclosed filenames/line +numbers without implementation excerpts or source-bearing tracebacks. + +Metrics: 33 unchanged scenarios / 285 unchanged public cases / 0 added cases; +1 fresh root A launch (9 known), 0 reviewer/C launches in this session. Same +post-repair B window: 1/2 used and rejected, one remaining. C repair1/1 exhausted; +extra repair requested but not authorized. All earlier attempts/spending and closed +windows remain retained. Partial measured authoring/proof/check interval: 243 s, +19:50:18–19:54:21 UTC on 2026-10-05, excluding earlier input/reference reading +and final report preparation. Prior recorded 1,727 s plus this interval totals +1,970 s of partial measured agent spending. No fixed owner time/token cap or new cap is supplied; +exact model-token usage/billing are unavailable. Fresh independent B round2 is next. +After another nonacceptance, no extra rewrite is authorized without diagnosed +resolution or explicit extension. + +## Supplementary complex SVD operand observer correction — same post-repair window + +This fresh independent root A (A10) corrects one further independently evidenced +test-observer defect before combined B round2/2. R3, all 33 scenarios, and the +285 public cases remain unchanged. This is a test correction and independent +counterexample, not original test-first evidence, product-red evidence, or +self-acceptance. B6 round1/2 rejected the dimensional restriction and float64 +output-range assumption; A9 corrected those before this sequential supplement. +The current owner packet explicitly authorizes one additional C repair for the +valid custom-optimizer numerical exception defect. That later authorization +supersedes the earlier report's statements that the extra repair was merely +requested. Original repair1/1 remains spent; no review window, scenario, spending +or budget cap resets. Only B round2/2 remains in this correction window. + +### Sequential identity and narrow correction + +All proposal work initially used only the supplied B5 accepted and B6 rejected +public snapshots, R3, root AGENTS.md, and design-tests skill. No live test/report +was read or written before `/private/tmp/dphtools-spectrum-delivery/A9-editing-complete.json` +existed. Both live hashes exactly matched that marker before live access: + +- A9 test: `4c40f4cf5bac99b3d47d4f3bb1fcf18ae366941b23428dc0bc9fbfa7bf81cafe`. +- A9 report: `6391435c246b4c3933c2f7437587b505aad4d669ead710b4efae315dcc50d375`. + +Exact copies are retained in +`/private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY/A9-tests-before.py` +and `A9-report-before.md`. A9 had removed the two-dimensional covariance +restriction but still asserted `np.isrealobj(matrix)` for covariance SVD operands. +The only additional test change moves this real-only check into the test-owned +`optimizer_lstsq` context, whose operand is specifically the independently +derived real Jacobian. Covariance operands retain `ndim>=2`, nonempty and finite +checks. The exact least-squares matrix/residual checks, injected LinAlgError, +required reached-boundary RuntimeError outcome, input preservation, permitted +verified success, public real outputs and all mathematical expectations/tolerances +remain unchanged. A9's dtype-range correction and other bytes remain intact. + +Earlier A7/A9 descriptions of real-only covariance SVD inputs, including A9's +claim that complex operands must be rejected, are superseded here. They describe +an unsupported **test defect**. [NumPy's SVD documentation](https://numpy.org/doc/stable/reference/generated/numpy.linalg.svd.html), +opened independently in this session, allows real or complex input with at least +two dimensions and stacked decomposition. R3 requires real public observations, +parameters, model and physical covariance. It specifies no dtype for an internal +numerical operand. No complex public output is required or permitted by this +correction. No production algorithm or binding is mandated. + +### Independent counterexample and outcome discrimination + +The unchanged stationary fixture has `N=201`, physical `(A,c,sigma)=(3,0,1)` and +projected finite noise. Its analytic Jacobian is +`J=[g, m*(x-c)/sigma**2, m*(x-c)**2/sigma**3]`, with `g=exp(-z**2/2)` and +`m=A*g`. Independently observed normalized stationarity is `1.73304e-15`, the +column-normalized condition number is `1.93186`, and the fixture's unchanged +curvature inequality proves a strict local minimum. All inputs use ordinary +finite base units; no extreme-unit or covariance-output-range issue is involved. + +A temporary conforming observer alternative calls the supplied optimizer via +its physical residual/initial/bounds/limit protocol, computes J at the returned +solution and casts J to complex128. With NumPy's documented decomposition +`J=U*S*V^H`, it forms `F=V/S` and real covariance +`C=real(F*F^H)*RSS/(N-P)`. Here `RSS=0.013791440787719714` squared data units and +`N-P=198`. Because this J is physically real, `J^H*J=J.T*J`; the Hermitian factor +product yields the same real physical covariance. Unpatched covariance agrees +with the unchanged analytic expectation at `rtol=1e-12, atol=1e-14`; maximum error +in independent uncertainty units is `1.21242e-15`. Public parameters, fitted +values, residuals and covariance satisfy all existing real-output checks. + +The B6 observer rejects this `(201,3)` complex128 path with AssertionError solely +at its real-only check. A9's completed observer also rejects complex128 2-D and +4-D paths. The corrected live observer accepts real and complex inputs with +shapes `(201,3)`, `(1,201,3)` and `(1,2,201,3)`, including legitimate RuntimeError +translation, successful recovery and a correct earlier binding/alternate path +that does not reach the injection. Independent negative alternatives still fail +for a leaked LinAlgError, unrelated early RuntimeError, mutated data or guesses, +doubled covariance, incorrect fitted model, complex public covariance, scalar/ +vector/empty/nonfinite operands. The unchanged real optimizer-lstsq context still +accepts a valid translated numerical failure. No probe imports production code. + +The initial frozen-snapshot proof passed 38 checks in 0.68 s, exit 0. Expanded +proof against the exact corrected live observer passed 44 checks in 0.70 s, +exit 0, with no skips or warnings. Both attempts are retained as +`observer-proof.log` and `live-observer-proof.log`; the public suite gains no case. +The alternatives are independently derived discriminating witnesses for this +fixture, not a general fitter, production repair or universal backend-coverage +claim. A covariance implementation may use any conforming numerical algorithm. + +### Exact verification evidence + +Evidence and all temporary scripts reside outside Git at +`/private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY/`. Every Python probe/test +installs category/message/filename/line-only warning rendering and exception-only +rendering before imports. Pytest uses `-p no:warnings --tb=no -p no:cacheprovider +-q -ra --capture=tee-sys -o addopts=`. The diagnostic plugin retains exception +type/message, case outcomes and process status without source or traceback. +Recorded warnings and the existing LM subprocess retain their own source-free +rendering. Pipeline exit status is preserved with `set -o pipefail`. + +Exact corrected test SHA-256: +`50bb168312355b5c3035f534195260f4a0081a6f171cdd6a6de52235774837dd`. +The report's own final hash is returned outside the tracked tree. + +The whole-spectrum command, from the authorized worktree, is: + +```sh +set -o pipefail +PYTHONDONTWRITEBYTECODE=1 SPECTRUM_A10_RESULTS=/private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY/spectrum-results.json MPLBACKEND=Agg MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache /private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 /private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY/launch.py tests/test_spectrum_fitting.py 2>&1 | tee /private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY/spectrum-tests-final.log +``` + +Black uses the same interpreter and command-local variables, with +`format.py --check --line-length 99 tests/test_spectrum_fitting.py` and +`black-check-final.log`. Exact preservation/case outcomes use `launch.py +test_preservation.py --confcutdir=/private/tmp/SPECTRUM-FITTING-001-A-complex-2LPa2aaY`, +with absolute evidence paths, retaining `preservation-final.log`, +`final-identity.json` and `outcome-summary.json`. Final results follow below. + +| Check | Actual result | Retained evidence | +|---|---|---| +| Initial independent snapshot proof | 38 passed in 0.68 s; exit 0 | `observer-proof.log`, six `unpatched-*.json` mathematical witnesses | +| Corrected live observer and discriminating negatives | 44 passed in 0.70 s; exit 0; no skips/warnings; no product import | `live-observer-proof.log`, `test_observer_proof.py` | +| Exact whole spectrum | 284 passed, 1 failed in 1.62 s; pytest/shell exit 1 | `spectrum-tests-final.log`, `spectrum-results.json` | +| Exact Black 99 | One file would be left unchanged; exit 0 | `black-check-final.log` | + +The sole spectrum failure remains +`test_s29_s31_public_numerical_backend_failure[optimizer_lstsq]`, with +`LinAlgError: Numerical decomposition did not converge (injected)` at the valid +finite `(201,3)` analytic Jacobian and independently known residual. This is +**product defect — leaked custom-optimizer numerical exception**, high confidence, +subject to final independent B review. Neither covariance observer correction +explains or weakens that red evidence. A did not repair production code. + +The covariance case reaches the finite `(201,3)` boundary and returns RuntimeError; +the success alternative remains independently checked. The suite retains 14 +RuntimeWarnings and three OptimizeWarnings; the real LM subprocess exits 0. +There are no errors or skips. Accepted-case membership/outcomes and preservation +are recorded by the final source-free preservation check outside the tracked +candidate. Its assertions require all 276 accepted cases to pass, unchanged +accepted test prefix/AST, exact equality to the A9 preimage except the two moved +assertions, and the A9/rejected/accepted report preimages as exact prefixes. +All 45,853 accepted test bytes, all 125,173 A9 report bytes, all 111,341 rejected +report bytes and all 76,378 accepted report bytes are retained. The final report +and test hashes are in `final-identity.json` and the handoff conversation. + +Environment: supplied clean non-Conda CPython 3.12.14, NumPy 2.5.3, SciPy 1.18.1, +pytest 9.1.1, Black 26.5.1; interpreter and base prefixes identify the supplied +clean Python. Only command-local supplied environment values and task-local +scripts were used. No environment/config/rules change occurred. Full coverage +and platform-matrix verification were neither run nor claimed. No local +counterexample prescribes an internal dtype, decomposition or covariance algorithm. + +Read only the authorized public inputs/snapshots, marker and permitted live +test/report after exact hash verification, the opened NumPy reference, and this +session's own evidence. Directory filename inventory was used for permitted +input discovery; no forbidden file content was read. No product implementation, +private helpers/source/history/diffs, coverage, other preexisting tests/oracles, +PROJECT.md, task state/role packets, lessons or role transcripts were inspected. +Only the authorized two tracked files changed. No Superpowers, memory, +delegation, commit/push/PR, full verification, merge or release occurred. +**Source exposure: none observed.** Diagnostics contain category/message/file/line +and exception types/messages, without product source excerpts or traceback. + +Metrics: 33 unchanged scenarios / 285 unchanged public cases / 0 added cases; +1 fresh independent root A launch (10 known), 0 reviewer/C launches in this +session. Same post-repair B window: round1/2 rejected, combined final round2/2 +next. Initial C and original repair1/1 remain spent; the owner explicitly approved +one additional repair for the valid custom numerical exception defect. Partial +measured authoring/proof/sequential-check interval: 225 s, 19:54:17–19:58:02 UTC +on 2026-10-05, excluding earlier input/reference reading and final report/hash +completion. Prior recorded 1,970 s plus this interval totals 2,195 s of partial +measured agent spending; concurrent effort is not elapsed wall time. All local +attempts remain retained. No fixed owner time/token cap or new cap was supplied; +exact model-token usage/billing remain unavailable. No allowance or spending reset +is claimed. Fresh independent combined B round2 is the next handoff; A grants no +acceptance and launches no reviewer or product repair. + + +## Evidenced covariance correction — independent A, 2026-10-05 + +This append-only regression closes an evidenced public covariance gap within +approved R3 S06/S09/S31. The coherent 33-scenario slice remains unchanged. This +is post-implementation regression evidence, not original test-first evidence. +Only two Gaussian public cases and two small mathematical helpers were added. +The approved 285-case snapshot's AST, oracles, tolerances, and all 62,168 prefix +bytes remain identical. The preceding report was treated as raw bytes only: +136,921 prefix bytes with SHA-256 +`73bf5276ddf3a315544f37627701615ab9651b9f967f9aebe96c8bb0b528c55c` +remain intact. Final candidate hashes and preservation proof are outside the +tracked candidate, in the handoff and `/private/tmp/dphtools-spectrum-covariance-a12/`. + +### Mapping and public observations + +| New public test variant | Approved scenarios | Observations | +|---|---|---| +| `test_s06_s09_s31_large_constant_background_full_physical_covariance[default_lm]` | S06/S09 | Omitted optimizer; jointly fitted constant background; physical 4-by-4 covariance, including background correlations; fitted/residual consistency and caller input/result-storage preservation | +| `test_s06_s09_s31_large_constant_background_full_physical_covariance[analytic_custom_trf]` | S06/S09/S31 | Same checks through a real bounded SciPy least-squares adapter; physical residuals/initial parameters/bounds, forwarded evaluation limit, and Boolean convergence status | + +The expectation source is R3's Gaussian point-sample model, physical optimizer +protocol, and local residual-variance covariance estimator. No implementation, +private helper, coverage, historical execution report, or other oracle was read. +No numerical success guarantee at this background was introduced. An actual +numerical fit failure may raise RuntimeError, with diagnostics and input checks +retained. Other exceptions fail the test. Adapter protocol checks are recorded +outside the callback's exception path, so a wrapped RuntimeError cannot hide a +failed protocol assertion. A warning does not excuse inaccurate finite covariance. + +### Independent fixture, solver, and covariance proof + +Use N=201 coordinates uniformly spaced from -4 to 4 x units, one component +with height 3 data units, center 0 x units, sigma 1 x unit, and constant 1e12 +data units. Starting peak guesses are (2.8, 0.08, 1.06), rather than a purported +production fitting result. Noise starts as +`0.01*(sin(1.73*i) + 0.4*cos(0.61*i))` data units and is projected off the +four analytic Jacobian columns by a reduced QR factorization. Projection is +only fixture construction, not a production algorithm requirement. + +At physical parameters (A,c,s,b0), set z=(x-c)/s and G=exp(-z*z/2). The analytic +model Jacobian is `[G, A*G*z/s, A*G*z*z/s, 1]`. The residual Jacobian has the +opposite sign, which leaves J.T J unchanged. At every successful returned fit, +compute `C = (residuals @ residuals)/197 * inverse(J.T @ J)` using its actual +returned parameters and residuals. Do not substitute generating truth or a +solver-specific convergence result. This is the full covariance in flattened +peak-row then constant-background order. Height/background variances have data +units squared; center/sigma variances have x units squared; cross covariances +have their corresponding products of units. + +Independent binary64 representability evidence, computed before any product +call: spacing at 1e12 is 0.0001220703125 data units; represented noise has +RSS 0.0116786211729 data units squared and peak magnitude 0.013916015625 data +units (114 spacings). Quantization is below 1% of the unrounded noise's peak. +All data, model/Jacobian entries, and the covariance oracle are finite. The +column-normalized Jacobian has condition number 3.256924914 and minimum singular +value 0.469793750. At generating parameters, the covariance's standard deviations +are approximately (0.001562921 data units, 0.0005452542 x units, +0.0006996599 x units, 0.0009319442 data units). These numbers are proof/sanity +checks, not expected recovered parameters or hard-coded test uncertainties. +Height/background and sigma/background correlations are approximately -0.42245 +and -0.62663. Thus dropping correlations is a substantial error. + +The custom adapter calls real SciPy `least_squares(method="trf")` on the supplied +residual callable, with its independent negative analytic model Jacobian. It +forwards bounds and max_nfev=2000, checks positive returned height/sigma, and +returns physical x and the real Boolean solver success, without a Jacobian or +covariance. Translating only the background coordinate within the adapter avoids +premature step termination from a 1e12 parameter norm. This is an adapter choice, +not a prescribed product transform. Independent solver controls on the contract +model succeeded for zero background (5 evaluations) and 1e12 (15 evaluations), +reducing RSS from about 2.423 to about 0.01167. The zero-background control is +only an external fixture/oracle proof, not a redundant additional public case. + +### Tolerances and discrimination + +Normalize every covariance entry by `sqrt(C_ii*C_jj)` from the analytic oracle. +The oracle diagonal becomes one, and off-diagonals become correlations. Check +all 16 entries with relative tolerance zero and absolute tolerance 0.03. This +checks variances and physically meaningful background correlations without a +near-zero-entry relative-error problem. Symmetry and positive semidefiniteness +are additionally checked in these dimensionless units with 1e-10 roundoff slack. + +The 0.03 limit was chosen independently before product calls. A central +full-model derivative using a 0.05 physical-unit step is one allowed numerical +variation, not a required differentiation algorithm. Its normalized Frobenius +roundoff bound is 0.003279155 from the background spacing, and its independent +smooth-model central-difference truncation bound is 0.002433733. Dividing their +sum by the minimum normalized singular value gives eta=0.012160417. The inverse +Gram perturbation bound `(1-eta)**(-2)-1` is 0.024771764. A 0.03 normalized-entry +tolerance allows this bound with modest slack. The directly calculated full-model +central-difference covariance error is 0.003584724; a 1% covariance scale change +has error 0.01. Both pass. Covariance halved has error 0.5; background variance +100 times too small has error 0.99; zeroed cross correlations have error +0.626631248. Each fails by a wide margin. No bound or tolerance was fitted to +production output. No differentiation method, decomposition, internal dtype, or +optimizer internals are prescribed. + +Fitted-model agreement uses absolute tolerance two binary64 background spacings, +0.000244140625 data units, with relative tolerance zero. A relative tolerance +on the 1e12 background would hide a wrong peak. Residual agreement with +`data-fitted` uses absolute tolerance 8*binary64 epsilon and relative tolerance +zero; this allows subtraction roundoff without hiding an inconsistent residual. +Input preservation uses exact equality, including on RuntimeError. Writable +returned arrays are modified after successful verification to check that caller +inputs remain intact. The returned column-normalized condition number must be +below 1e4 to guard this inverse oracle's applicability; failure of that guard +would require diagnosis before claiming this covariance defect. No recovered +truth or particular convergence path is asserted. + +### Baseline results, classification, and retained attempts + +The final changed test file was formatted with Black, line length 99, and its +Black check passed with exit status 0. Source-free full spectrum testing on +clean CPython 3.12.14 collected 287 cases: 285 passed, two new cases failed, +exit status 1, reported duration 1.66 s. Both failures occur at the fixed full +covariance comparison; schema, finite output, positivity, protocol where +applicable, model/residual consistency, conditioning, symmetry, and positive +semidefiniteness checks preceding it passed. + +| Variant | Actual RSS (data units squared) | Returned normalized Jacobian condition | Maximum normalized covariance error | Classification | +|---|---:|---:|---:|---| +| Default LM | 2.4032688 | 3.49278348 | 0.929593142 | Product defect against the physical covariance convention, pending fresh B review | +| Real analytic custom TRF | 0.0116734058 | 3.25694088 | 0.907581986 | Same product defect, pending fresh B review | + +For default LM, returned variance/oracle variance ratios are approximately +(0.185806, 0.151028, 0.070407, 0.342974). For custom TRF they are approximately +(0.131869, 0.123265, 0.092418, 0.391890). Important background cross covariances +also fail. These are inaccurate finite uncertainties on successful returns, +not the contract's allowed RuntimeError numerical-failure outcome. The oracle +uses each fit's own residual variance, so the default fit's higher RSS does not +explain or invalidate the covariance failure. No optimizer-convergence behavior +is rewritten or separately claimed by this gap correction. + +Retained author attempts: one independent mathematical proof run; two full +spectrum runs; two Black format invocations and two Black check invocations. +The first test draft had the same two covariance failures and 285 passes; its +log is retained as `full-spectrum-initial.log`. Before reading that baseline +output, one control-flow revision hardened callback protocol assertion retention +and removed an unsupported exact callback-count requirement. It changed no +fixture, covariance oracle, or numerical tolerance. The final run is +`full-spectrum-final.log`; `final-spectrum-session.json` retains counts, statuses, +node IDs and durations. Independent proof is `independent-proof.json`, preservation +proof is `preservation.json`, and final hashes/commands are in `handoff-manifest.json`, +all under `/private/tmp/dphtools-spectrum-covariance-a12/`. + +Every probe/test launcher installed source-free warning rendering (category, +message, file, line only) and uncaught-exception rendering (type, message, real +failure status) before third-party imports. Pytest used `-p no:warnings --tb=no`; +failure diagnostics exposed only type/message and public numeric arrays, without +source or traceback frames. All warnings and real exit statuses were retained. +No accidental source exposure occurred. Imports and public calls remained opaque. +Only the authorized tests/report were changed; no product/configuration/workflow, +environment installation, coverage, broad verification, delegation, Git mutation, +PR, merge, release, or lesson activity occurred. + +This invocation used one fresh root A, no additional author/reviewer/C launches, +and no delegation. The new covariance B window has used 0/2 rounds; fresh B +round 1 is next. Original B window 2/2, formatting 1/2, coverage 1/2, numerical +1/2, and final post-repair 2/2 remain accepted/closed and counted. Both authorized +C repairs remain used 2/2; the owner's additional extension is pending. This +handoff is not self-acceptance and authorizes no C work. No fixed budget was +supplied; token/billing usage is unavailable here, not reset or claimed unlimited. + + +## A13 — B8 bookkeeping-only correction and independent discrimination + +This append records only the authorized test defect from the public +`/private/tmp/dphtools-spectrum-delivery/B8-public-finding.md`. R3 and its 33 +approved scenarios remain the expectation source. This is the same covariance +correction window: B8 round 1/2 rejected; final B round 2/2 remains. Earlier +windows, attempts, and spending remain counted. C repairs 2/2 are used, the owner +extension is pending, and this A session has no C authority or self-acceptance. + +### Correction and failure discrimination + +The old custom marker started `False` and became `True` only after both residual +checks completed. A permitted numerical `RuntimeError` from `residual(trial)` +therefore left a false marker despite no failed test assertion. This is a **test +bookkeeping defect**, reproduced twice against the exact B8 snapshot: the first +residual callback and the second callback each raised a numerical `RuntimeError` +with correct physical initial values, bounds, ordering, and evaluation limit. + +Only that bookkeeping changed. The callback now catches `AssertionError`, records +the actual failure, and re-raises it. The outer assertion requires an empty +failure list. Numerical `RuntimeError` interrupts checks without recording a test +assertion failure. A fitter that catches the callback assertion and wraps it as +`RuntimeError`, even suppressing its cause, still leaves the recorded failure and +is rejected. There is no exception-chain inspection, completion requirement on a +failed fit, numerical-success requirement at a 1e12 background, or new algorithm, +decomposition, or dtype requirement. + +The unchanged successful-fit path still requires real custom optimization, +successful callback status, accurate finite model/residual/covariance outputs, +physical covariance with correlations, and preservation of caller inputs and +returned storage. The custom adapter's physical protocol assertions and all +tolerances are unchanged. + +Independent stand-ins follow R3's public protocol and use the Gaussian equation +directly. They do not import product code or use product helpers. They drive the +actual permitted covariance test, with a real SciPy solver for successful custom +controls. Full diagnostic outcomes and stage counters are retained in +`/private/tmp/dphtools-spectrum-covariance-a13/bookkeeping-proof.json`. + +| Control group | Outcomes | +|---|---| +| Default numerical RuntimeError before, between, and after independent valid model evaluations | 3 accepted; default has no custom callback checks | +| Custom numerical RuntimeError before the adapter, first residual before its checks, second residual between checks, solver residual after protocol checks, and after adapter return | 5 accepted | +| Exact B8 numerical-interruption reproductions at first and second residual | 2 rejected by the defective original marker | +| Invalid initial shape/nonfinite/value/order; every lower/upper-bound entry; evaluation limit; first/second residual shape, values/sign, and trial dependence | 18 variants, each rejected bare, wrapped with cause, and wrapped without cause: 54 rejections | +| Accurate finite covariance and 1% covariance variations, for default/custom | 4 accepted | +| Half covariance, missing correlations, nonfinite covariance, wrong model, wrong residual, input mutation on success, and mutation followed by numerical RuntimeError, for default/custom | 14 rejected | +| Total independent controls | 82/82 matched expected outcomes; launcher exit 0 | + +These temporary controls are proof evidence, not additional repository scenarios +or collected tests. No stub replaces the production public-entry-point spectrum +run. The stand-ins establish discrimination and do not establish product +correctness or a specific internal algorithm. + +### Preserved covariance mathematics and mapping + +The independently reviewed fixture, analytic Gaussian Jacobian, actual-returned +RSS scaling, real custom solver, and absolute normalized covariance tolerance +0.03 are byte/AST preserved from B8. For `z=(x-c)/s`, `G=exp(-z²/2)` and +`m=b0+A*G`, physical model Jacobian columns remain +`[G, A*G*z/s, A*G*z²/s, 1]`, ordered `(A,c,s,b0)`. +With N=201 observations and P=4 parameters, residual degrees of freedom are +N−P=197 and `C=(JᵀJ)⁻¹ * (rᵀr)/197`. The test evaluates J and RSS at the returned +fit; it does not demand generating-truth parameters. Height/background have data +units, center/sigma have x units, and C entries have the product of their two +parameter units. Dividing by `sqrt(C_expected[i,i]*C_expected[j,j])` gives +dimensionless variances/correlations. + +The retained public A12 proof records represented-noise RSS +0.011678621172904968, peak magnitude 0.013916015625 data units, 1e12 binary64 +spacing 0.0001220703125 data units, normalized Jacobian condition +3.2569249135612877, and inverse perturbation bound 0.024771763782000855. B8's +public finding separately records conservative bound 0.02663. Both are below +the unchanged 0.03 tolerance. No tolerance was fitted to candidate output. +These values are retained reviewed evidence, not a new covariance-proof run. + +Original 285 cases keep their exact approved bytes, AST, oracles, tolerances, +and mapping. The two covariance cases remain +`test_s06_s09_s31_large_constant_background_full_physical_covariance[default_lm]` +and `[analytic_custom_trf]`, covering S06 constant background, S09 physical +covariance units, and S31 physical custom protocol/covariance. Spectrum +collection/execution is 287 cases, zero skips; scenario count remains 33. +Counts below overlap where a case exercises multiple approved scenarios. + +| Scenario | Collected cases | +|---|---:| +| S01 | 8 | +| S02 | 4 | +| S03 | 2 | +| S04 | 10 | +| S05 | 8 | +| S06 | 5 | +| S07 | 3 | +| S08 | 9 | +| S09 | 6 | +| S10 | 3 | +| S11 | 3 | +| S12 | 2 | +| S13 | 4 | +| S14 | 5 | +| S15 | 3 | +| S16 | 12 | +| S17 | 45 | +| S18 | 6 | +| S19 | 8 | +| S20 | 8 | +| S21 | 6 | +| S22 | 5 | +| S23 | 7 | +| S24 | 8 | +| S25 | 6 | +| S26 | 4 | +| S27 | 37 | +| S28 | 8 | +| S29 | 14 | +| S30 | 1 | +| S31 | 20 | +| S32 | 6 | +| S33 | 27 | + +### Mechanical preservation + +- Candidate tests SHA-256: `33501511bff9dcf676821606341516790eb30ffec1319eec27dbb730835e617d`. +- Approved A11 tests: exact 62,168-byte prefix, SHA-256 + `50bb168312355b5c3035f534195260f4a0081a6f171cdd6a6de52235774837dd`; + all 83 original top-level AST nodes match. All 285 cases are unchanged. +- Full B8 file reconstructs byte-for-byte after reversing only the authorized + marker/list, assertion capture, and outer assertion changes. Covariance math, + fixture, solver, finite/model/mutation checks, and tolerance bytes are retained. +- Entire report before this append: 148,077 bytes, SHA-256 + `f6c0cc3799ad642a96935db54eca96d64b793bb34396318dd2fb4953c41f6a82`. + It was hashed as bytes and never semantically read. The older A11 136,921-byte + report prefix also retains SHA-256 + `73bf5276ddf3a315544f37627701615ab9651b9f967f9aebe96c8bb0b528c55c`. +- Final report hash and final preservation evidence are recorded outside its + tracked tree in the final handoff manifest and conversation to avoid a + self-referential hash. + +### Exact-candidate baseline and checks + +All checks used clean CPython 3.12.14 at +`/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3`, with +`MPLBACKEND=Agg`, +`MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig`, and +`PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache`. +No environment or configuration changed. Before every probe/check, the temporary +launcher installed source-free warning category/message/file/line rendering and +uncaught exception type/message/failure-status rendering. Pytest always received +`-p no:warnings --tb=no`. Failure records retain type/message and frame +file/line/name without source lines. No accidental implementation source exposure +occurred; warning file/line diagnostics remain visible. + +Commands use that interpreter and +`/private/tmp/dphtools-spectrum-covariance-a13/sourcefree.py`: + +| Launcher arguments | Status/result | +|---|---| +| `black --line-length 99 tests/test_spectrum_fitting.py` | 0; already formatted, unchanged | +| `run /private/tmp/dphtools-spectrum-covariance-a13/preservation-proof.py` | 0; exact approved prefix/AST and narrow-change reconstruction | +| `run /private/tmp/dphtools-spectrum-covariance-a13/bookkeeping-proof.py` | 0; all 82 independent outcomes matched | +| `pytest -q -s -ra tests/test_spectrum_fitting.py` (launcher adds `-p no:warnings --tb=no`) | 1; 285 passed, 2 failed, 287 executed, 1.53 s | +| `black --check --line-length 99 tests/test_spectrum_fitting.py` | 0; unchanged | + +Both public baseline failures are `AssertionError` at the retained +`assert_allclose(normalized, expected_normalized, rtol=0, atol=0.03)`, candidate +line 1648. Default RSS=2.4032688, normalized condition=3.49278348, +covariance error=0.929593142. Custom RSS=0.0116734058, condition=3.25694088, +error=0.907581986. Successful finite outputs reached the actual covariance +assertion; this is not a bookkeeping failure. The observing A classifies these +as retained **product covariance defects against R3**, supported by B8's public +finding and the preserved math, pending final independent B review. Numerical +failure remains a permitted alternative. A neither accepts its own correction +nor repairs product code. + +### Evidence and remaining limits + +All complete logs, scripts, mapping, and proof results are outside Git under +`/private/tmp/dphtools-spectrum-covariance-a13`. SHA-256 values at this append: + +| Artifact | SHA-256 | +|---|---| +| `bookkeeping-proof.py` | `b23c5c9ab50d11082feb1ae5f7a15fbe2be3f9c2c9a5c7cc65c95c9e63cb74d5` | +| `bookkeeping-proof.json` | `c6d3c539e2c694beb9928e7063583d3f6add7825cd0137f00e29accad8334022` | +| `bookkeeping-proof.log` | `3b9d3fbf6353fbd96074ed864510e43095dfd2a08c6f53891aeeabb1af2d6349` | +| `sourcefree.py` | `7b1f3b1129d2845d1a1becb79fb879487b599224de47fb5042d310abb3b99be8` | +| `preservation-proof.py` | `89e2a595b98f662d8a04aab6af7b5b30a29c7c683868f1752dbd9b578c157aa2` | +| `mapping-proof.json` | `66b1933696cb6c409afe194746cd599bb45e6d019940a89566ee106672d4fc9f` | +| `spectrum-session.json` | `7cf33ecfac1ae5a98ffdceedead92dad12a19212d36206823504b47f4cc33964` | +| `spectrum.log` | `547ee0abe75eccc5c5ebac94ffa3dcebe9f947424ce6641d6573c7c0039f4a62` | +| `black99.log` | `1a56c7cd8457fd05d978cfbd139d7bfd86e86acaa9b57668e718ef8bccc0cf48` | + +This root A launch: 1. Delegation, additional author/reviewer/C launches: 0. +Independent bookkeeping proof runs: 1. Spectrum runs: 1. Black format/check +runs: 1/1. Preservation runs: initial 1 plus final post-append verification. +No new scenarios, review window, repair allowance, or budget was created. +Prior spending remains counted; token billing is unavailable and no new fixed +cap was supplied. Final B round 2/2 is next. No product/config/tool/instruction +edits, environment installation, coverage, other tests, broad/platform +verification, commits, push, PR, merge, release, lessons, memory, Superpowers, +or delegation were performed. This remains post-implementation correction +evidence, not original test-first evidence or delivery acceptance. + + +## R4 owner physical-scope correction — independent A proposal, 2026-10-05 + +Only the two appended extreme-offset cases were replaced. The original 285-case +prefix (62,168 bytes) is byte-for-byte equal to the permitted original and has +SHA-256 `50bb168312355b5c3035f534195260f4a0081a6f171cdd6a6de52235774837dd`. +The historical report prefix (159,279 bytes) was hashed without viewing its +content: SHA-256 +`e44db5ca4e2ff95d6cdc49e8873267f2b03999f7e3b6853b9b9fc776b6aa386b`. +It is preserved, with this append after it. The prior 287-case checkpoint at +`e5bc47e1448c5466f981cfb4dd29150e8539552f` is retained historical evidence; +this proposal does not reinterpret its original boundary cases as measurements. + +| Scenario | Replacement public-entry-point evidence | +|---|---| +| S06 | Both variants of `test_s06_s09_s31_resolved_voltage_gaussian_full_physical_covariance`: joint constant background, successful noisy fit, full covariance including peak/background correlations. | +| S09 | Same two variants: V/nm parameter and covariance units, analytic physical Jacobian, input/model/residual/storage invariants. Existing nonuniform-coordinate and sorting cases remain unchanged. | +| S31 | `real_custom_trf` variant uses actual SciPy `least_squares` on the supplied physical residual/bounds/evaluation limit; returns only x, native Boolean success, and message. Existing protocol/schema checks remain unchanged. | + +The default variant omits `optimizer`, so it exercises the public default +Levenberg–Marquardt (LM) path. The unchanged S30 real-solver dispatch test also +passes. Neither replacement permits `RuntimeError`. Total collection remains +287; the approved Contract table remains 33 scenarios. + +Physical fixture: 201 wavelength point observations from 500 to 510 nm at +0.05 nm spacing; Gaussian height 1.2 V, center 505.1 nm, sigma 0.7 nm, constant +background 0.4 V. Full width at half maximum is +`2*sqrt(2*ln(2))*0.7 = 1.64837 nm`, or 32.97 sampling intervals. The window +extends approximately seven sigma on either side. Background/height is 1/3; +height/read-noise standard deviation is 120. Add independent Gaussian read +noise with standard deviation 0.01 V and `default_rng(20261005)`, without +projection, selection, clipping, or weighting. This illustrates analog detector +voltage; it makes no claim about an actual instrument or photon-counting data. + +Fixture/tolerances were frozen before any product call in +[precall-design.md](/private/tmp/dphtools-physical-A-jKfBli/precall-design.md), +SHA-256 `39e0bf4d402ba5c36d2ead477541bb82b108b1654f4f3f89c6a871e9e84bf4a2`. +The independent analytic model Jacobian is +`J = [g, A*g*z/sigma, A*g*z*z/sigma, 1]`, where +`z=(wavelength-center)/sigma` and `g=exp(-z*z/2)`. The residual Jacobian is `-J`. +An independent mathematical probe, before product calls, gave nominal local +noise scales `[0.002538 V, 0.001656 nm, 0.001811 nm, 0.000889 V]` from +`0.01**2 * inv(J.T@J)` at generating truth. Frozen absolute recovery bounds +`[0.020 V, 0.012 nm, 0.012 nm, 0.006 V]` are 6.6–7.9 of those scales, allowing +noise and modest local nonlinearity. These are functional accuracy bounds, +not confidence intervals or repeated-measurement calibration. + +Fit-quality bounds: fitted-versus-noiseless root mean square <0.006 V; +residual root mean square between 0.006 and 0.014 V; residual sum of squares +(RSS) no greater than generating-noise RSS times `1+1e-6`, since generating +truth is feasible; dimensionless column/residual stationarity <`3e-4`. +The latter permits cosine errors of order `sqrt(1e-8)` from ordinary solver +stopping tolerances with a factor-three allowance, while rejecting an appreciably +unfinished fit. Model/residual equality checks use the unchanged result helper's +roundoff allowances (about 1e-10 V, far below noise), plus positivity, shapes, +finiteness and storage preservation. Solver objective, status, numerical +differences, and stopping conventions follow the approved +[SciPy least_squares reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html). + +Full covariance expectation is `Cref = RSS/(201-4) * inv(J.T@J)` at RETURNED +parameters, using ACTUAL residuals. This follows R4 and the +[SciPy local covariance/scaling reference](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html). +Height/background covariance has V² units, center/width covariance nm², +and cross-unit entries V*nm. Each entry is normalized by +`sqrt(Cref_ii*Cref_jj)` and compared with absolute tolerance 0.002, including +correlations and small entries. Symmetry and positive semidefiniteness are checked. +Require column-normalized Jacobian condition <4. Four unit-norm columns imply +smallest singular value >=`2/7`; column derivative errors <=`1e-4` imply +whitened error <=0.0007 and inverse error <=0.001403 in reference uncertainty +units. The 0.002 allowance includes arithmetic roundoff. Smooth binary64 forward +steps give center step/sigma about `1.1e-5`. Independent forward/centered probes +at truth and truth plus/minus recovery bounds gave conditions 2.290–2.307 and +maximum relative column error `6.70e-6`, well inside the bound. This is local +covariance accuracy, not confidence-interval coverage. Discriminating mathematical +examples: deleting correlations gives normalized error 0.411; RSS/N scaling +gives 0.01990; multiplying covariance by 0.99 gives 0.010. All exceed 0.002. + +Baseline on the exact proposed test file (SHA-256 +`0628df219e68d935b1bda6c3d438e5352b6a971b14fcbdd309270bb8cb6ebfb5`): + +| Check | Actual result | +|---|---| +| Source-free spectrum suite, one run | 287 passed, 0 failed, 0 skipped; pytest exit 0; 1.68 s reported runtime. | +| Default LM replacement | Successful; residual rms 0.00921609 V; RSS 0.01707221 V²; condition 2.299717; maximum normalized covariance error `4.47e-11`. | +| Real custom TRF replacement | Successful; residual rms 0.00921609 V; RSS 0.01707221 V²; condition 2.299717; maximum normalized covariance error `5.27e-11`. | +| Black, line length 99 | Temporary full-file formatting left file unchanged; exact protected-prefix comparison passed before installation; final file check exit 0. | + +All commands used the supplied source-free launcher, clean interpreter and +prescribed environment. Pytest used `-p no:warnings --tb=no`, disabled cache, +and fresh diagnostics +[spectrum-suite-001-diagnostics.json](/private/tmp/dphtools-physical-A-jKfBli/spectrum-suite-001-diagnostics.json). +Full attempts, commands, environment and proofs reside only in +[/private/tmp/dphtools-physical-A-jKfBli](/private/tmp/dphtools-physical-A-jKfBli). +Initially passing results are valid baseline evidence; no product-red claim. +Failure classification: none in this proposal. The retained robustness cases +reported expected numerical failures/warnings and passed their existing checks; +they remain outside representative-measurement acceptance. + +No implementation/source/history/diffs, coverage, project/task state, lessons or +prior role reports were inspected. Only permitted report suffix access was used +(it was initially empty). Source-free diagnostics retained warning category, +message, file and line; no source lines/tracebacks were exposed. No accidental +source exposure. Optional memory/delegation and Superpowers were unused. +Only the test suffix and this report append changed in the repository. No +installation, product repair, broad verification, coverage measurement, commit, +push, PR, merge or release occurred. Coverage and platform verification are not +established by this focused A run; product revision was not queried under the +blind packet. Finite-range extremes and weighted/Poisson fitting remain outside +this acceptance, and local covariance does not establish uncertainty calibration. + +Handoff: proposed checkpoint only; fresh B must review expectation sources, +tolerances, scope, prefix proofs and results. No A self-acceptance. Metrics: +33 scenarios, 287 cases, this correction A launch 1 (cumulative A13/B9), one +math probe and one pytest run, two Black invocations; owner-scope A/B window +0/2 before B review. Previous windows 2/2, 1/2, 1/2, 1/2, 2/2, 2/2 remain +accepted/closed with all historical attempts/spending retained. Initial C and +the two authorized repairs remain used 2/2; this correction grants no C repair. +No fixed cap was supplied; token/billing totals are unavailable and no renewed +allowance is inferred. + + + +## Fresh independent A: ordinary wavelength-unit regression within R4 + +This append covers only the evidenced nm-to-m physical-unit gap. It adds six +cases to the exact accepted 287-test prefix, for 293 cases across the same 33 +approved scenarios. It uses the public `spectrum_fit` entry point. The default +cases omit `optimizer`; the custom cases run real SciPy least squares on the +supplied physical residuals. The default is Levenberg–Marquardt (LM). No +optimizer is monkeypatched in these new cases. +This is an A proposal for fresh B review, not self-acceptance or a product repair. + +### Case mapping and expectation sources + +All six cases belong to +`test_s01_s02_s03_s09_s30_s31_resolved_voltage_nm_to_m`. Each case performs both +nm and m fits on the same measured voltage samples. The original 287 mappings, +AST nodes, oracles, tolerances, and bytes remain unchanged. + +| Parameterized case | Existing scenarios | Actual baseline outcome | +|---|---|---| +| default_lm-gauss | S01, S06, S09, S30 | nm checks pass; m recovery fails | +| default_lm-lorentz | S02, S06, S09, S30 | nm checks pass; m recovery fails | +| default_lm-voigt | S03, S06, S09, S30 | nm checks pass; m recovery fails | +| real_custom_trf-gauss | S01, S06, S09, S31 | both units and full comparison pass | +| real_custom_trf-lorentz | S02, S06, S09, S31 | both units and full comparison pass | +| real_custom_trf-voigt | S03, S06, S09, S31 | both units and full comparison pass | + +R4's physical scope, point-sample equations, unweighted objective, physical +parameter/covariance convention, caller preservation, and optimizer protocol +supply the expectations. The unit-transform relation follows directly from +those equations and the chain rule. The permitted primary references opened +during A are [SciPy's Voigt definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.special.voigt_profile.html), +[local covariance convention](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html), +and [least-squares methods, scaling, evaluation limits and status](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html). +Installed SciPy is 1.18.1; these are R4's pinned expectation references, not a +claim that the installed runtime equals the referenced version. + +### Physical data and tolerances frozen before new candidate calls + +The axis is 500–510 nm with 201 samples spaced by 0.05 nm. Each illustrative +analog-voltage spectrum has one resolved positive 1.2 V peak at 505.1 nm, +constant 0.4 V background, and independent unprojected additive normal read noise +with standard deviation 0.01 V. Gaussian uses the accepted R4 fixture unchanged: +sigma=0.7 nm, seed 20261005, starting row (1.0 V,505.0 nm,0.85 nm). Lorentz uses +gamma=0.7 nm, seed 20261006, and the same starting row. Voigt uses sigma=0.55 nm, +gamma=0.30 nm, seed 20261007, starting row (1.0 V,505.0 nm,0.65 nm,0.40 nm). +The metre version multiplies x, centers and widths by 1e-9 and keeps the voltage +data, heights and background unchanged. These are not photon-counting fixtures. + +The full pre-fit freeze is retained at +`/private/tmp/dphtools-physical-unit-a-x8CEk4/frozen-expectations.md`, SHA-256 +`3c3b24f04fa02c8aae7272362a14ae47d911c037694a14a197cba6ce18aaed5a`. +Its append-code SHA-256 is +`969c5fa6ec81142a5e7f7f0455bf30552ea10ddd9d43ea2b82fa8be6cfc6db2b`. +No tolerance or fixture changed after the six new cases were run. + +Recovery bounds in flattened peak/background order are: + +| Family | Absolute bounds in V/nm coordinates | +|---|---| +| Gaussian | 0.020 V, 0.012 nm, 0.012 nm, 0.006 V; accepted R4 bounds | +| Lorentz | 0.0182540 V, 0.0105556 nm, 0.0190753 nm, 0.00674961 V | +| Voigt | 0.0205224 V, 0.0104073 nm, 0.0476363 nm, 0.0766331 nm, 0.00956303 V | + +Lorentz/Voigt bounds equal six local standard-error scales from the independent +generating model, using nominal 0.01 V noise, computed before fitting. They are +generous recovery limits, not confidence-interval coverage requirements. The +code evaluates the unrounded formula. Wavelength bounds scale by 1e-9 in m. + +Fitted voltage must be within 0.006 V root mean square (RMS) of the noiseless +physical model; residual RMS must lie between 0.006 and 0.014 V. A noise RMS +sampling scale is approximately 0.01/sqrt(402)=0.000499 V. Model fluctuation is +approximately 0.01*sqrt(p/201), at most 0.00158 V for five parameters. These +estimates make the chosen bounds generous without permitting unfinished fits. +Residual sum of squares (RSS) must not exceed the feasible generating-model +noise RSS times (1+1e-6). The maximum Jacobian-column/residual cosine must be +below 3e-4, checking first-order stationarity independently of solver status. + +### Independent covariance and unit relation + +Gaussian reuses the accepted analytic physical Jacobian. Lorentz differentiates +the approved unit-height model analytically. Voigt uses five-point differences +of the independent public model in canonical nm coordinates, with +`h=eps**(1/5)*scale`, where eps is machine epsilon, and local scales +(1.2 V,0.55 nm,0.55 nm,0.30 nm,0.4 V). +It does not use the old absolute-step helper on metre coordinates. Fourth-order +truncation is O(eps**(4/5)); center subtraction contributes about +spacing(505 nm)/h, below 2e-10. A source-free, product-free mathematical probe +checked analytic versus finite differences for Gaussian/Lorentz and halved-step +agreement for Voigt; maximum column-relative discrepancies were 4.77e-11, +4.77e-11, and 2.09e-10. No fitted candidate output entered this probe. + +Column-normalized condition limits are Gaussian<4, Lorentz<5, Voigt<20. +Independent generating-model values are 2.29857, 3.43081, 13.63981. Expected +covariance is `inverse(J.T@J)*RSS/(201-p)` at each returned physical solution. +All entries, including background correlations, are compared in their natural +`sigma_i*sigma_j` scales. Absolute normalized tolerance is 0.002 for +Gaussian/Lorentz and 0.01 for Voigt; the latter accommodates stronger width +correlations while remaining far above oracle roundoff. These limits prescribe +observable covariance accuracy, not product derivative machinery. Normalized +symmetry tolerance is 1e-10 and minimum eigenvalue must be >=-1e-10. Independent +truth-model background correlations reach about 0.40, 0.62 and 0.81, making +omitted background correlations discriminating. + +For `U=diag(1,1e-9,1e-9[,1e-9],1)`, the same physical model has +`C_m=U@C_nm@U.T`. Returned covariance is converted back to nm before the oracle +and cross-unit comparisons. Thus tiny m variances and V*m cross-covariances +cannot pass via a loose absolute tolerance. Peak/background recovery, physical +model reconstruction, fit quality, stationarity and independent covariance are +checked before unit agreement, preventing two equally wrong results from +validating each other. Parameter agreement across units allows 0.05 nominal +generating-model standard-error scales; fitted/residual voltage agreement allows +2e-5 V, 0.2% of the read-noise scale. Cross-unit covariance allows 2.5 times the +individual normalized tolerance, accommodating two approximation errors and +small fitted-point differences. These are numerical consistency checks. + +The custom adapter maps physical initial/bounds into invertible local affine +variables, with center origin x[0] and voltage/width scales from the fixture. +It runs real `least_squares(method="trf", jac="3-point")` on the supplied +residual callable, forwards `max_nfev=2000`, and uses ftol/xtol/gtol=1e-11. +It returns the actual solver x in physical units and actual success/message. +It supplies no fabricated optimum, covariance or Jacobian. Real custom statuses +were 2 in both units; evaluation counts were 6, 6 and 7 for the three families, +within the supplied limit. Existing protocol matrices remain untouched. +Every new case checks caller x/data/guesses and storage preservation by modifying +writable returned arrays and rechecking caller snapshots. + +### Real baseline evidence and classification + +| Family | Default nm RSS (V²) | Default m RSS (V²) | Feasible truth RSS (V²) | Default m RMS (V) | Default m stationarity | +|---|---|---|---|---|---| +| Gaussian | 0.0170722116 | 1.19075167 | 0.0172294242 | 0.0769684 | 0.537673 | +| Lorentz | 0.0188368985 | 0.747881114 | 0.0191605702 | 0.0609984 | 0.634971 | +| Voigt | 0.0180133489 | 1.02348407 | 0.0184590891 | 0.0713580 | 0.549989 | + +Each default m case returns a result but first fails the independent physical +recovery assertion. It also has objective, residual-noise and stationarity +values far outside frozen bounds. The corresponding nm fit passes all per-unit +checks. Each real custom case passes both units, full physical covariance and +the cross-unit comparisons, at the same successful RSS shown in the nm column. +These are black-box product defects at approved representative behavior, not +environment/tooling failures, oracle defects, unresolved requirements, or +manufactured product-red. Implementation/root cause was not inspected. + +Accepted prefix baseline: 287 passed in 1.66 s, actual exit status 0. +Proposed spectrum suite: 293 collected, 290 passed, 3 failed in 1.74 s, actual +exit status 1. All original 287 node IDs retain their passing outcomes. Black99 +check passes, exit 0, and reports the entire test file would be unchanged. +Mathematical discrimination passes, exit 0. No rerun or output tuning occurred. + +Runtime: clean specified interpreter, Python 3.12.14, macOS-27.0 arm64, +NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. Each probe/test/Black +used the supplied `physical-sourcefree.py` renderer and the required +MPLBACKEND=Agg, MPLCONFIGDIR, PIP_CACHE_DIR and PYTHONDONTWRITEBYTECODE=1 values. +Each pytest invocation has its own diagnostics destination. Full source-free +logs, diagnostics, recorded outcomes, versions and preservation proofs are in +`/private/tmp/dphtools-physical-unit-a-x8CEk4/`. Key files are +`baseline287.log`, `baseline287-diagnostics.json`, `candidate293.log`, +`candidate293-diagnostics.json`, `black293.log`, `mathematical-probe.log`, +`freeze-proof.json` and `evidence-summary.json`. Log hashes are retained in the +summary; final file hashes are recorded outside the tracked tree to avoid +self-reference. Commands were the specified interpreter and source-free +launcher with `pytest -q -s tests/test_spectrum_fitting.py`, or +`black --config /dev/null --check --line-length 99 tests/test_spectrum_fitting.py`. + +### Preservation proof, exposure and limits + +The entire accepted 67,472-byte test prefix is exactly the supplied approved +file, SHA-256 +`0628df219e68d935b1bda6c3d438e5352b6a971b14fcbdd309270bb8cb6ebfb5`. +All 86 original top-level abstract syntax tree (AST) nodes remain identical; +only two helpers and one +six-case parameterized test are appended. The entire 167,691-byte report prefix +is preserved, SHA-256 +`e121055b3f63d6ff704352b5e388e2de95d85fbe1e69257368512fd8f41311e5`. +The report prefix was hashed as opaque bytes and never decoded or read. +Post-Black and final preservation proofs are retained externally. Only the new +report append is readable by this A. Only the test/report suffixes were authored +in tracked files; no product, fixture file, contract, settings, workflow, +instruction or environment changes were made. No repository diff/history or +implementation source was inspected. + +No accidental product-source exposure occurred. Source-free warnings retain +category, message, pathname and line number. Failure messages retain exception +type, test assertion diagnostics and true pytest status; no product source or +traceback lines were printed. Optional memory and delegation were not used. +Only the permitted public inputs, root AGENTS.md, design-tests skill, own new +temporary evidence and opened R4 primary references were used. + +The test proposal does not add extreme-offset, subnormal, dynamic-range, +Poisson/weighted-fit or interval-calibration acceptance requirements. It makes +no global-optimum or repeated-measurement uncertainty claim. Coverage and full +platform verification were not measured in this narrow A run. No commit, push, +pull request, merge or release was attempted. + +Metrics/allowances retained: 33 scenarios, 293 proposed cases; this fresh A is +one author launch, zero reviewer launches/delegation. Completed cumulative +launches are A14/B10 after this A, versus supplied A13/B10 beforehand. The new +physical-unit A/B window remains 0/2 until fresh B review. Prior closed windows +2/2,1/2,1/2,1/2,2/2,2/2,1/2 and all attempts/spending remain retained. Initial C +and two C repairs (2/2) remain spent; no new C authority is granted here, and +the owner physical-coordinate scaling extension is pending. Two spectrum-suite +invocations consumed 3.40 s of pytest-reported time; the six added cases consumed +about 0.0204 s in their case reports. No fixed execution cap was supplied; +token/billing totals are unavailable. Fresh independent B is the next role. + +### A15 — R4 linear-background physical-unit gap, before first B round + +Fresh independent ROOT A used `design-tests`. Owner authorization is the narrow +gap-completion request: append three end-to-end cases to A14's 293 proposed cases, +without changing the 33 approved scenarios. This is additional evidence before B, +not a rejection, new scenario, reset, or self-acceptance. No optional memory, +delegation, Superpowers, implementation inspection, or private-role context was used. +Permitted inputs were root instructions, the selected skill, public contract R4, +the proposed test file, and the four supplied public gap/prefix/source-free files. +The prior report was read as bytes solely for hashing; it was never decoded. + +The three appended cases are +`test_s01_s02_s03_s07_s09_s30_resolved_linear_voltage_nm_to_m[gauss|lorentz|voigt]`. +Each calls `spectrum_fit` twice with full peak guesses, a linear background, +`max_nfev=2000`, and the optimizer omitted. These are actual default fits; no +custom oracle returns a planted optimum. The unchanged S30 subprocess test +observes the actual SciPy LM method before importing the opaque public product. + +| Appended variant | Approved rows | Independent observations | +|---|---|---| +| Gaussian | S01, S07, S09, S30 | Positive recovered Gaussian, line convention, voltage objective, full physical covariance, nm/m conversion, input/storage preservation | +| Lorentzian | S02, S07, S09, S30 | Same checks with Lorentzian half-width | +| Voigt | S03, S07, S09, S30 | Same checks with two independently estimated positive widths | + +Thus `293 + 3 = 296` proposed cases, still 33 scenarios. A14's six constant +background unit cases remain intact: three families times default LM and real +custom TRF. Fresh B should review all nine new unit cases together, retaining +A14's real custom controls. A15 adds no custom protocol/invalid-input matrix. + +#### Physical fixture and mathematical expectation sources + +Reuse `_resolved_unit_voltage_fixture` without editing it: 201 voltage samples at +500–510 nm, 0.05 nm spacing, component height 1.2 V, center 505.1 nm, and b0=0.4 V +at the first sample. Gaussian sigma=0.7 nm; Lorentzian gamma=0.7 nm; Voigt +sigma=0.55 nm and gamma=0.3 nm. Full peak guesses remain those of the supplied +fixtures: (1.0 V, 505.0 nm, 0.85 nm), or Voigt (1.0 V, 505.0 nm, 0.65 nm, 0.4 nm). +The same family-specific ordinary independent additive read-noise draws remain +unprojected, with nominal standard deviation 0.01 V and original seeds +20261005/20261006/20261007. They are illustrative analog voltage, not photon counts. + +Only add `0.015*(x_nm-x_nm[0]) V` to the noiseless spectrum and measured data. +The line rises `0.015 V/nm * 10 nm = 0.15 V`; b0 remains 0.4 V. In metres, +x spans 5.00e-7–5.10e-7 m, center=5.051e-7 m, Gaussian/Lorentzian width=7e-10 m, +Voigt widths=5.5e-10 m and 3e-10 m, and slope=1.5e7 V/m. The two calls receive +identical voltage arrays. All full-guess center/width coordinates multiply by +1e-9; height remains V. + +The public R4 equations define every expected spectrum and line origin. +[SciPy's Voigt definition](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.special.voigt_profile.html) +supplies the two width conventions and convolution; normalize it to unit peak +height per R4. The unweighted objective and local residual scaling follow R4 and +[SciPy's covariance convention](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html). +The retained actual-default observation is consistent with the approved +[SciPy optimizer interface](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html). +All three primary-reference URLs were opened in this A session; no product source +was opened. + +`_linear_unit_voltage_nm_jacobian` reuses the approved tests' analytic Gaussian +and Lorentzian peak derivatives. It adds EXACT columns `1` and `x-x[0]` for b0 +and b1. For Voigt, the height column is the exact unit-height profile; center, +sigma, and gamma use independent five-point O(h^4) differences of the peak alone +after subtracting the wavelength origin. Steps are +`eps**0.2 * (0.55, 0.55, 0.3) nm`; this balances h^4 truncation against eps/h +roundoff at resolved local width scales. Half-step differences control the +derivatives at generating truth before fitting and at each fitted vector. No +metre-sized absolute differencing step or approximate line column is used. + +For fitted physical parameter vector theta in nm units, compute the independent +model Jacobian J and RSS from the actual residual voltage. The oracle is +`C_nm = inv(J.T @ J) * RSS/(201-p)`, with p=5 for Gaussian/Lorentzian and p=6 +for Voigt: 196 or 195 residual degrees of freedom, including BOTH line columns. +Normalize each covariance entry by its own `sqrt(C_ii*C_jj)`; compare the complete +matrix, not only diagonal uncertainties. Check symmetry and positive +semidefiniteness in these same uncertainty units. This includes all peak/line +cross terms and the substantial b0/b1 correlation. + +The parameter-unit transformation is diagonal U: height and b0 have 1; +center and every width have 1e-9; b1 has 1e9. Thus `theta_m=U*theta_nm` and +`C_m=U*C_nm*U.T`. Divide both covariance indices by U to compare in nm uncertainty +units, which cannot accept erroneous tiny metre variances merely through an +absolute tolerance. Each unit result first must satisfy independent physical +truth, voltage, objective, stationarity, and covariance checks. Two wrong fits +cannot establish correctness by agreeing with each other. + +#### Bounds frozen before any A15 product call + +Independent model-only probe and artifacts: +`/private/tmp/dphtools-spectrum-a15-ju3ARF/linear_math.py`, +`math-prefreeze.json`, and `math-prefreeze.log`. The probe imported the permitted +test equations/fixtures but never called or inspected the product. It computed +the generating-truth Jacobian and checked all corners of the six-scale parameter +recovery box. Noise draws were never changed or projected; a linearized projection +was calculated only to assess expected error magnitude, not to manufacture data. + +| Model-only quantity | Gaussian | Lorentzian | Voigt | +|---|---:|---:|---:| +| Scaled-J condition at truth | 4.65635 | 5.17534 | 14.95847 | +| Largest recovery-box corner condition | 4.67010 | 5.21400 | 16.49446 | +| Realized read-noise rms, V | 0.00925843 | 0.00976351 | 0.00958312 | +| Linearized fit error rms, V | 0.00136468 | 0.00141593 | 0.00150167 | +| b0/b1 correlation at truth | -0.808087 | -0.731647 | -0.591276 | + +Voigt half-step relative column differences across all corners were at most +3.18e-12. The runtime control bound is 1e-7, leaving a wide floating-point margin +while checking stability independently. Condition limits are 6 for Gaussian and +Lorentzian, 20 for Voigt. Corner checks are evidence of representative conditioning, +not a proof about every point in the box; runtime checks also test actual solutions. + +Recovery limits are six local standard-error scales at truth, +`6 * 0.01 V * sqrt(diag(inv(J_truth.T@J_truth)))`. They are generous deterministic +acceptance margins, not claims of confidence-interval calibration. Ordered bounds: + +| Family | Height, V | Center, nm | Width(s), nm | b0, V | b1, V/nm | +|---|---:|---:|---|---:|---:| +| Gaussian | 0.0152272 | 0.0100825 | sigma 0.0108687 | 0.00905412 | 0.00148072 | +| Lorentzian | 0.0182547 | 0.0107436 | gamma 0.0190816 | 0.00990137 | 0.00148564 | +| Voigt | 0.0205347 | 0.0105996 | sigma 0.0476655; gamma 0.0766996 | 0.0118579 | 0.00148754 | + +Voltage model error must be <0.006 V rms, compared to the independently generated +noiseless line-plus-peak. Residual rms must lie between 0.006 and 0.014 V, retaining +ordinary read noise. RSS must not exceed the feasible generating-truth noise RSS +times (1+1e-6). Require `max(abs(J.T@r)/(column_norms*norm(r))) < 3e-4`; normalization +makes stationarity unit invariant and detects an unfinished least-squares fit. +The same predeclared margins apply in both coordinate units. RuntimeError fails. + +Covariance absolute normalized-entry bounds are 0.003 for Gaussian/Lorentzian and +0.012 for Voigt, with zero relative tolerance. A desired per-column derivative +accuracy budget delta=1e-4 gives `eta <= kappa*sqrt(p)*delta`. Gram inverse perturbation +then gives normalized-entry error at most `(1-eta)**-2 - 1`, bounded above by +`2*eta/(1-eta)**2`: <0.00270 for kappa=6,p=5; <0.00990 for kappa=20,p=6. The chosen +margins also cover controlled-oracle differentiation and arithmetic. This is an +independent accuracy budget, not a claim about the opaque product's derivatives. +Normalized symmetry and eigenvalue roundoff bounds are 1e-10. The unchanged public +schema checks enforce shapes, finite real arrays, positive widths/heights, point +model voltage agreement (2e-10 relative/absolute), and residual identity (2e-12). + +Unit invariance limits are 0.05 truth standard-error scales for every converted +parameter, 2e-5 V pointwise for fitted/residual voltage (0.2% of nominal read noise), +and 2.5 times the covariance accuracy bound after conversion and normalization in +truth uncertainty units. The covariance margin allows two independent numerical +accuracy allowances plus curvature/roundoff margin; ideal unit conversion is exact. +These invariance checks supplement the absolute model checks. Input arrays are +compared exactly even on failed calls. Writable result arrays are then mutated +individually and inputs rechecked exactly, to exercise result-storage preservation. + +All bounds, models, seeds, transformations, and checks were written and frozen +before product calls. They will not be adjusted using observed product outputs. +Black 99 formats only the new private append first; the final combined file must +pass Black and retain the complete byte prefix. No existing helper, case, oracle, +tolerance, fixture, or scenario changes. + +#### Preservation, scope, and retained spending + +Initial byte proof matched the complete existing 76,388-byte test file to the +supplied approved snapshot: SHA256 +`9bc96c8ec1f493e06ed5a034a470362ded7657f02e5c1d5acff99968ab3229ae`. +All 89 top-level AST nodes, including line/column attributes, are retained; +canonical AST-list SHA256 +`ff74e2d13fdd97af8114a508c9184fa47d35b07089be4adcb6751f8ef926438d`. +The complete 180,616-byte prior report was hashed without decoding: +`16b45cf72e899843935bb6d934e1d362d79680a2b170fc8f3613f188f3161c4e`. +Final prefix proof and candidate hashes are kept outside the tracked candidate in +the A15 private directory and returned in the handoff. Only these two tracked +files may be appended. No Current state section is added to this report. + +Preserve prior spending exactly as supplied: completed A14/B10; prior closed +windows 2/2, 1/2, 1/2, 1/2, 2/2, 2/2, 1/2. The physical-unit window remains 0/2 +before B. C initial plus two repairs exhausted 2/2; additional C authority remains +pending. A15 is one fresh A launch for gap completion, with no B/C/D launch, no +allowance reset, and no fixed execution cap. Billing/token totals are unavailable. +The supplied A14 baseline is 290 pass / 3 product-red default-SI failures; it is +prior evidence, not an A15 rerun result. New exact-candidate results follow below. + +No installs, product/private-helper/history/diff/coverage reads, project/task-state +reads, lessons, private reports/events/conversations, commits, pushes, full +verification, PRs, merges, or releases. Imports/public calls are opaque. No +repeated-measurement uncertainty calibration, weighted/Poisson model, extreme or +subnormal accuracy guarantee, or new numerical scenario is requested. Source-free +rendering precedes every mathematical probe, pytest, and Black check. Actual +warnings, exception type/message, and real status must remain in private logs. + +#### A15 exact-candidate baseline and handoff evidence + +The single spectrum-suite run collected exactly 296 cases. Real pytest status was +1: **290 passed / 6 failed**, in 1.75 s. There were no skipped or xfailed cases. +The retained prefix contributes the same 290 passes and three default-SI failures +as the supplied A14 baseline. A15 contributes three additional product-red cases. +All three new nm calls completed all recovery, voltage model, residual/objective, +stationarity, full covariance, and input/result-storage checks before their m call. +The SI outcomes are not accepted alternatives for these representative spectra. + +| New case / unit | Observed RSS, V^2 | Residual rms, V | Normalized stationarity | Scaled-J condition | Outcome | +|---|---:|---:|---:|---:|---| +| Gaussian / nm | 0.0168548119 | 0.00915722588 | 2.57605489e-7 | 4.65813428 | All per-unit assertions passed | +| Gaussian / m | unavailable | unavailable | unavailable | unavailable | RuntimeError: Spectrum residual is not finite | +| Lorentzian / nm | 0.0187581038 | 0.00966042957 | 2.79055724e-7 | 5.17987751 | All per-unit assertions passed | +| Lorentzian / m | 0.729234358 | 0.060233144 | 0.625152071 | 5.52732264 | Physical recovery AssertionError | +| Voigt / nm | 0.0179984647 | 0.00946280089 | 1.56473949e-8 | 14.9582164 | All per-unit assertions passed | +| Voigt / m | 1.04026893 | 0.0719407208 | 0.557151431 | 17.7285307 | Physical recovery AssertionError | + +The failed Lorentzian m fit retained the full-guess peak at 1.0 V, 505.0 nm, +0.85 nm after conversion, instead of truth 1.2 V, 505.1 nm, 0.7 nm. Its b0 was +0.394743650 V and slope 0.0165587398 V/nm. Voigt likewise retained peak guesses +1.0 V, 505.0 nm, 0.65 nm, 0.4 nm; its b0 was 0.376095911 V and slope +0.0159919704 V/nm. Recovery errors exceed independently frozen bounds; objective +and stationarity diagnostics also disagree strongly with a completed fit. +Source-free warning diagnostics include overflow in exp and invalid multiply +during the new-case execution. Their category/message/file/line remain in the +log; no product source was rendered. The retained numerical-boundary warnings +and covariance warnings also remain in that log, with their original statuses. + +Classification: all six failures are **product defects under R4**, subject to +fresh B's independent test review. Three are the preserved A14 default constant +background SI recovery failures. The new Gaussian is a prohibited representative +fit failure; new Lorentzian/Voigt return unfinished SI solutions. There is no +observed environment/tooling failure, test defect, or unresolved unit requirement. +Confidence is high in this observable classification: the models are independently +specified, generating truth is feasible, nm checks pass, and measurement units +do not change the physical objective. Product-source diagnosis is outside A's +inputs, and no implementation cause is asserted. B finding an oracle/fixture +defect would change the classification; no repair scope or C allowance is granted. + +Limit: the new m full-covariance and cross-unit invariance assertions were not +reached after the earlier failures. They remain requirements of the proposal; +this baseline proves neither their SI success nor general covariance calibration. +The nm matrix checks were reached and passed, including peak/line and b0/b1 terms. +All callers' input-preservation finally checks ran, including the Gaussian m +RuntimeError. Result-storage checks ran for each successful nm result; SI failures +prevent the later result-storage assertions. No accuracy assertion was loosened. + +The independent mathematical discrimination probe used zero product calls and +passed with real status 0. It checks the final appended Jacobian and bounds, +without changing product code or test data. For Gaussian/Lorentzian/Voigt it rejects: + +| Wrong mathematical result | Gaussian normalized error | Lorentzian | Voigt | +|---|---:|---:|---:| +| Diagonal-only covariance | 0.808087 | 0.731647 | 0.959748 | +| Missing peak/line cross-covariance | 0.224040 | 0.402103 | 0.631730 | +| Missing b0/b1 correlation | 0.808087 | 0.731647 | 0.591276 | +| RSS/n rather than RSS/(n-p) | 0.0248756 | 0.0248756 | 0.0298507 | +| Missing slope covariance unit factor | 1.0 | 1.0 | 1.0 | + +All exceed the frozen 0.003/0.012 entry bounds. Failing to transform the slope +would cause ~0.015 V/nm recovery error, against bounds ~0.00149 V/nm. Using x +rather than x-x[0] with the approved b0 produces a 7.5 V model error, against +the 0.006 V model-rms bound. These controls discriminate the intended linear-unit +gap and full covariance without adding pytest cases or an unrelated matrix. + +Exact commands (all from the worktree; identical required environment): + +```text +MPLBACKEND=Agg +MPLCONFIGDIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/mplconfig +PIP_CACHE_DIR=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/pip-cache +PYTHONDONTWRITEBYTECODE=1 +python=/private/tmp/dphtools-spectrum-clean-env-w0q0h1ko/python/bin/python3 +launcher=/private/tmp/dphtools-spectrum-delivery/physical-sourcefree.py + +$python $launcher run /private/tmp/dphtools-spectrum-a15-ju3ARF/linear_math.py + status 0; model-only freeze probe +$python $launcher black --line-length 99 /private/tmp/dphtools-spectrum-a15-ju3ARF/append_tests.py + status 0; only the private new append formatted +$python $launcher black --check --line-length 99 tests/test_spectrum_fitting.py + status 0; full candidate left unchanged +SPECTRUM_DIAGNOSTICS_JSON=/private/tmp/dphtools-spectrum-a15-ju3ARF/spectrum-suite-first.json +$python $launcher pytest -q -s tests/test_spectrum_fitting.py + status 1; 296 collected, 290 pass, 6 fail; one spectrum-suite run +$python $launcher run /private/tmp/dphtools-spectrum-a15-ju3ARF/discrimination.py + status 0; model-only discrimination; zero product calls +``` + +Environment recorded by the source-free probe: Python 3.12.14, darwin, +NumPy 2.5.3, SciPy 1.18.1, pytest 9.1.1, Black 26.5.1. The frozen primary references +are the R4-linked SciPy 1.15.3 manuals; no installed-version source was inspected. +There were no installs or persistent environment edits. Every pytest invocation +(one total) named a fresh own-private diagnostics JSON. Logs preserve actual +warnings, exception type/message, and subprocess/pytest status without tracebacks +or source lines. Permitted test assertion messages appear as exception messages. +Accidental forbidden source exposure: **none observed**. + +Private artifacts in `/private/tmp/dphtools-spectrum-a15-ju3ARF/` include +`prefix-proof-initial.json`, `prefit-freeze.json`, `math-prefreeze.json/log`, +`black-append.log`, `black-candidate.log`, `spectrum-suite-first.json/log`, +`discrimination.json/log`, and final prefix/candidate evidence. The freeze record +precedes every A15 product call; the test candidate hash remains unchanged afterward. +No new pytest runs are needed for documentation-only completion. + +A15 measurable spending: one fresh A session, one model-only conditioning probe, +one model-only discrimination probe, one 296-case spectrum-suite execution, one +private-append Black formatting run, one whole-test-file cheap Black check. +No B/C/D sessions or repairs. No fixed execution cap was supplied; token/billing +totals are unavailable. All supplied prior windows, A14/B10, and C initial plus +two repairs remain counted. Open physical-unit B window stays 0/2. The handoff is +an **unreviewed proposed 296-case checkpoint** for fresh B to review the combined +nine physical-unit cases. Additional C authority is still pending. Full verification, +coverage, publication, and merge are outside this A15 task. + + +## A18 — caller-warning preservation correction (R4), round 3/3 + +This replaces only the rejected appendix after the approved 296-case checkpoint. +It retains one genuine smallest-positive-width custom Levenberg-Marquardt (LM) +case: **296 accepted + 1 replacement = 297 spectrum cases**, with **the same 33 +approved scenarios**. This is a post-implementation numerical-robustness test +correction. The A16/B12 and A17/B13 attempts and nonacceptances remain retained. +The owner-authorized additional round is **round 3/3 of the SAME correction +window**, with no reset or further rewrite authority. + +The [approved public resolution](/private/tmp/dphtools-spectrum-delivery/diagnostic-correction-public-resolution.md) +resolves [B13's public diagnosis](/private/tmp/dphtools-spectrum-B17-sqf9i07c/review.md). +The rejected English substring matcher admitted display-only warnings and +rejected conventional numerical errors. A18 removes that matcher. The real +caller's adapter now emits an applicable numerical warning after its successful +solve; the test requires that caller-owned warning's category/message to survive. +Numerical-output correctness remains separately checked. No new product message, +warning category or internal algorithm is required. + +### Fresh frozen expectations, scope and scenario mapping + +[Fresh expectations](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/expectations-frozen.md), +SHA256 `c1129145314f8f2dbd8c359172651f57090cdc44ef68159bf0151561ac756a17`, +were frozen before all new probes. [Freeze metadata](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/freeze-metadata.json) +records 2026-10-06 05:45:27 UTC; the first observer probe began at 05:47:01 UTC. +This records chronology, not tamper-proof attestation. Expectations derive from +public R4, independent model/precision mathematics and the approved resolution. +They were not selected from candidate output. + +| Existing scenario | Public-entry-point mapping | Expectation basis | +|---|---|---| +| S31 and R4 numerical robustness/covariance warnings | `test_s29_s31_smallest_positive_gaussian_width_real_custom_lm`: real custom LM, physical residual/initial/bounds/result protocol, forwarded evaluation limit, caller-warning preservation, honest uncertainty or numerical failure | R4 custom callable, least-squares objective, warning preservation and independent identifiability | +| S29, supported by R4 numerical-robustness text | Same single case preserves caller inputs and records a numerical-inability outcome | R4 failure/input rules; S29's literal exhaustion/nonconvergence row remains unchanged | +| S01–S28, S30, S32–S33; earlier S29/S31 cases | All 296 accepted cases and their existing mappings remain exact | Approved prefix expectations and tolerances | + +The accepted default smallest-width checks and representative resolved physical +spectra remain exact. This replacement adds neither an accurate unresolved-width +recovery requirement nor the excluded artificial 1e12-background accuracy +requirement. Its units are abstract sample/coordinate units for robustness. + +Sources are the [R4 public contract](/Users/davidhoffman/.codex/worktrees/spectrum-fitting/dphtools/docs/tasks/SPECTRUM-FITTING-001-contract.md), +[corrected public gap](/private/tmp/dphtools-spectrum-delivery/steps-boundary-corrected-public-gap.md), +approved resolution and independent mathematics below. Opened primary references: +[SciPy least_squares](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.least_squares.html) +documents LM's lack of explicit bounds, linear least-squares objective and +zero-residual termination. +[SciPy covariance](https://docs.scipy.org/doc/scipy-1.15.3/reference/generated/scipy.optimize.curve_fit.html) +documents approximate covariance, variance diagonals and covariance-estimation +warnings. R4 governs the covariance convention; curve_fit's specific rank +handling is not imposed. + +### Retained independent mathematical proof and tolerances + +Use 81 coordinates from -1 to 1 coordinate units, height 1 sample unit, center 0, +Gaussian sigma `nextafter(0,1)=2^-1074` coordinate units and background 0.5 sample +units. The central/offcenter data are binary-exact 1.5/0.5 sample units. +R4's point-sampled model is `b+a*exp(-0.5*((x-c)/sigma)^2)`. + +The nearest offcenter distance is about 0.025 coordinate units (>0.024). Even +for twice sigma its ratio to width exceeds 2.4e321, so offcenter Gaussian samples +round to zero, while the central profile is one. Two distinct positive widths +produce identical represented samples. The unchanged independent accepted +stable-narrow helper avoids overflowing division and confirms zero represented +scaled center/width sensitivity. Height/background columns are independent: +represented local rank 2 among 4 parameters. Positive residual degrees of freedom +`81-4=77` do not restore missing information. Residual sum of squares (RSS) zero +is a global minimum of a nonnegative squared objective; it does not establish +finite individual center/width uncertainty. [Executed math evidence](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/oracle-controls-final-diagnostics.json) +independently verifies these claims. + +The adapter receives physical `[height,center,sigma,background]`, lower bounds +`[0,-inf,0,-inf]`, upper bounds `[inf,inf,inf,inf]` and `max_nfev=1000`. +It applies log/exp only to its own positive coordinates, calls actual SciPy +`least_squares(...,method="lm")` on the supplied residual and returns the actual +solver solution transformed to physical coordinates. No fixture truth supplies +the optimizer output. Every evaluation checks finite parameters, positive +height/width and the independent physical residual. Solver convergence and +nonincreasing RSS within roundoff are required. Background initialization and +exact solver evaluation counts remain delegated. + +Float64 epsilon is `2^-52`. The unchanged precision allowance is +`64*eps*1.5 = 3*2^-47 = 2.1316282072803006e-14` sample units. +Objective-growth allowance is `81*(3*2^-47)^2 = 729*2^-94` +`= 3.6805094393991527e-26` sample units squared. This conservative 64-epsilon +bound covers elementary transforms, model addition and residual subtraction; +the formula is independent of observed output. The A17 written decimal erratum +is retained; neither its executed formula nor this tolerance changed. + +| Check | Relative tolerance | Absolute tolerance | Rationale | +|---|---:|---:|---| +| Supplied residual versus independent physical model | 0 | 2.1316282072803006e-14 sample units | 64-epsilon arithmetic allowance | +| Sampled fitted data | 0 | 2e-7 sample units | Unchanged `MODEL_ATOL`; 2e-7 of unit height | +| Fitted versus returned physical model | 2e-10 | 2e-10 sample units | Conservative model roundoff | +| Residual versus data minus fitted | 2e-12 | 2e-12 sample units | Conservative subtraction consistency | +| Covariance versus transpose | 2e-10 | 2e-12 in each compared covariance entry's units | Unchanged symmetry allowance, matching NaNs permitted | + +No absolute covariance-accuracy bound or accurate center/width-recovery bound +is imposed on this unresolved boundary. + +### Warning preservation and informative-error review + +After its real successful solve, the adapter emits a genuine caller-owned +`OptimizeWarning`: "Caller LM solve reached the sampled minimum; center/width +uncertainty is unavailable." Its numerical applicability comes from the +independent sampled-identifiability proof. The exact **caller-owned** category +and message must survive on both allowed outcomes. This tests upstream warning +preservation under R4. It fixes neither the product's own text nor its category. + +On success, retain all separate checks: real finite correctly shaped positive +physical parameters; sampled accuracy; model/residual consistency; physical +parameter equality to the real optimizer output; real symmetric 4-by-4 +covariance; every variance NaN or nonnegative; center/width variances NaN or +positive infinity; input preservation and returned-storage independence. +Partial/all NaNs, positive infinity and mixed uncertainty alternatives remain +allowed. Negative finite/-infinite variances, fabricated finite zero +center/width uncertainty, inaccurate/incoherent output and silent None fail. + +On RuntimeError, the automated helper checks only class and nonempty text. +**This minimum is not informative-error acceptance.** B/D must independently +assess the actual caught error and retained numerical warnings against the +witness. A nonempty display error still fails that substantive review criterion. +No arbitrary English token matcher remains. + +The actual caught error was **"Spectrum covariance differences are not +representable"**, after a genuine successful zero-RSS solve. This identifies +covariance-difference representability as the unavailable numerical operation. +Its relevance is supported by the independent smallest-width/identifiability +proof and seven retained overflow warnings. A successful sampled objective can +coexist with unavailable local uncertainty. This is numerical-relevance evidence +for B14/D, rather than a requirement for this exact wording or difference +algorithm. [Structured actual observations and explicit assessment](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/observations-diagnostics.json) +retain the error and each warning's category/message/file/line. + +"Singular matrix" and "SVD did not converge" also describe applicable +linear-algebra inability in this numerical call. "Fit legend unavailable" and +"Optimizer progress display failed" describe display failures and supply no +numerical-inability diagnosis. These are explicit review distinctions, not a +new automated wording whitelist. Numerical warnings alone cannot stand in for +a separately emitted controlled caller warning; failure of that preservation +check does not reject their numerical relevance. + +### Independently frozen discrimination controls + +[Observer script](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/oracle-controls.py) +and [final-candidate results](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/oracle-controls-final-diagnostics.json) +retain **7 good/15 bad numerical-output controls**, all with preselected outcomes. +The seven good controls cover partial/all NaNs, all positive infinity, mixed +NaN/infinity, infinite diagonal, indistinguishable doubled width and a coherent +1e-7 sample-unit background offset. Good controls include the caller warning +alongside the original applicable diagnostics. Bad numerical-output controls +also include it, so preserved warnings cannot mask invalid output. The fifteen +bad controls retain negative variances, warning absence/empty/unrelated/ +success/progress-only cases, fabricated zero covariance, asymmetry/complexity, +coherent 5e-7 sample-unit error, inconsistent residual/model and None. + +**20 warning-preservation controls** adapt the original nine diagnosis examples +and add B13's display/numerical examples and controlled-warning presence/absence. +Display-only messages fail even with coherent numerical output. "Singular +matrix" and "SVD did not converge" pass with the caller warning; alone they +fail preservation of the distinct upstream warning. The caller warning alone, +or with either display message, passes; altered caller category or text fails. +Controls use genuine captured warnings and the actual appended success oracle. + +**12 error-minimum controls** confirm that seven meaningful numerical messages +and both display messages satisfy only the class/nonempty minimum, while empty, +whitespace and wrong-class examples fail. Separate explicit review labels mark +the display messages inapplicable. Thus the controls expose the minimum's limit +instead of claiming it supplies semantic acceptance. All **54** preselected +controls matched. They call no product, supply no fake optimizer solution and +manufacture no product-red evidence. Controls passed before the first new +product call and were rerun after formatting on the final test bytes. + +### Actual statuses, classification and exposure + +The [corrected-case invocation](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/corrected-case-invocation.json) +and [pytest diagnostics](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/corrected-case-diagnostics.json) +record 1 collected/passed, 0 failures/skips, process/pytest exit 0. +Actual real LM success was True, initial/final RSS zero; its observed physical +solution was `[1,0,2^-1074,0.5]`. All seven observed physical evaluations were +finite and positive in constrained coordinates; reported `nfev=1` is evidence, +not a test requirement. The fitter raised the numerical covariance-inability +RuntimeError and preserved inputs. Seven overflow RuntimeWarnings and the +genuine caller OptimizeWarning were preserved. +[Source-free stdout](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/corrected-case.stdout.log) +and [source-free warnings](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/corrected-case.stderr.log) +retain actual diagnostics without source snippets or tracebacks. + +The [one-file spectrum regression invocation](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/spectrum297-invocation.json) +and [structured results](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/spectrum297-diagnostics.json) +record **297 collected/passed, 0 failed/skipped, process/pytest exit 0**. +The preserved representative physical controls passed alongside the replacement. +[Black99 check](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/black99-check-invocation.json) +exited 0 and left the test file unchanged. Formatting beforehand also exited 0; +the full accepted prefix survived it exactly. + +Classification: B13's findings are test-oracle defects, resolved by this +authorized tests-only proposal. The supplied gap is post-implementation test-gap +evidence. The observed RuntimeError is a permitted numerical-inability outcome, +not a failed pytest or product-red observation. All harnesses and checks passed; +expected bad-control rejections are observer evidence. A18 observed no product +defect, unresolved requirement or environment/tooling failure. No product repair +was performed or authorized. + +No accidental implementation/source/private-helper/history/diff/raw-coverage, +PROJECT/task-state/LESSONS/private-role/event/conversation exposure was observed. +Permitted test helpers/public packets were read; public imports/calls stayed +opaque. Diagnostic filenames/line numbers remained opaque. Report bytes before +199983 were never decoded. The prescribed source-free renderer, clean +interpreter, environment and fresh own diagnostics destination preceded every +probe/test/Black run. Full stdout/stderr and actual statuses are retained. +Optional memory interfaces, delegation and Superpowers were not used. A18 +performed only the two authorized tracked appendix replacements and own +temporary evidence writes. No other suites/full/coverage, instructions/tooling/ +configuration/workflow/product edits, installs, commits/pushes/PR, merge or +release actions occurred. Other concurrent work was not inspected or modified. + +### Exact identities and retained allowances + +| Protected artifact | Bytes | SHA256 | Proof | +|---|---:|---|---| +| Approved 296-case test prefix | 84331 | `b3fe64dc5da2bdb40e4af19832f39684507b77957ac7361dc789431af09069d0` | Exact approved-copy bytes; all 91 top-level Abstract Syntax Tree (AST) nodes including locations equal the parsed prefix and first 91 candidate nodes | +| Approved report prefix | 199983 | `f345641c2d7697ff064ac73c59ec0478f216183b469243331e0791211db6ee0d` | Exact hash; never decoded | + +The corrected test file has 92473 bytes and 95 top-level AST nodes: 91 accepted +nodes plus three appendix helpers and one single-case test. Prefix proofs before +and after Black99 are retained. Final complete candidate hashes, suffix equality +and exact AST/report proofs are outside the tracked candidate in +[final-prefix proof](/private/tmp/dphtools-spectrum-A18-SdGfZtGB/prefix-final-diagnostics.json). +The rejected A17 test/report suffixes were retained in this evidence directory, +alongside the original permitted A17 packet; earlier rejected attempts remain +retained by the coordinator. Accepted report bytes and test helpers/cases/ +tolerances were untouched. + +Metrics: **33 scenarios; 296 accepted + 1 replacement = 297 cases; author A18 +launch 18; no reviewer/delegate launched by A18; A16/B12 round 1/2 and A17/B13 +round 2/2 rejected and retained; A18/B14 round 3/3 SAME correction window.** +Prior closed windows **2/2,1/2,1/2,1/2,2/2,2/2,1/2,1/2** remain retained. +Initial C plus three repairs remain **3/3 spent**, with no product-edit +authority. All attempts/durations/statuses are retained in own invocation +records. No fixed execution cap was supplied; token/billing totals are +unavailable. No further review, rewrite or repair allowance is inferred. + +Confidence is high in prefix preservation, numerical-output discrimination and +controlled-warning preservation. Informative-error acceptance still requires +B14's independent assessment of the actual recorded numerical evidence. +B14 must review this identified correction at round 3/3 before coordinator +commitment. Exact coverage-gap closure, full verification, D and required +platform acceptance remain separate coordinator obligations; A18 did not +inspect or establish them. diff --git a/docs/tasks/SPECTRUM-FITTING-001.md b/docs/tasks/SPECTRUM-FITTING-001.md new file mode 100644 index 0000000..f99e9a3 --- /dev/null +++ b/docs/tasks/SPECTRUM-FITTING-001.md @@ -0,0 +1,67 @@ +# SPECTRUM-FITTING-001: fit selectable profiles to a 1-D spectrum + +**Version 1.0.** Public contract R4 is in +[SPECTRUM-FITTING-001-contract.md](SPECTRUM-FITTING-001-contract.md). +This scientific task is separate from the routine-maintenance ten-task pilot. + +## Current state + +- Approval/scope: R3, the 33-scenario exception and R4 physical-plausibility + clarification remain approved. The owner requested repair of failed CI for + [PR #18](https://github.com/david-hoffman/dphtools/pull/18). This update pins + interpreter patch versions consistently across collection, workers and + aggregation. It changes no numerical behavior, contract, tests or check gates. + Existing authorization covers updating `codex/spectrum-fitting`; merge/release + remains unauthorized. +- Route: environment/tooling repair, with independent diagnosis and fresh + infrastructure diff review. Reuse the accepted R4 numerical checkpoint and + existing runtime. No test window or scientific product repair is opened; + prior allowances stay spent. +- Candidate pointer: documentation candidate + `0634499296e143bab6a16daba3f504b5c6e4391d` passed exact canonical local full + verification. Its [hosted run](https://github.com/david-hoffman/dphtools/actions/runs/37483079692) + failed before worker 0 ran tests: Linux collection used Python 3.10.21, while + worker 0 and aggregation used 3.10.22. The interpreter patch was the only + differing portable manifest field, independently confirmed from artifacts. + Pin Linux to 3.10.22 and macOS ARM64/Windows to their supported 3.10.11 builds; + preserve exact identity checks. Prepare this record before verifying the repair; + record the new candidate's own hash/results outside its tracked tree in PR #18. +- Runtime/checkpoint: C3 product commit + `f3a655709920f6c7f13a392c5c6265ae8d2f045e` remains unchanged, including internal + physical-unit conditioning. Accepted A18/B14 checkpoint: 297 cases across + unchanged 33 scenarios, same correction window accepted/closed at 3/3. + Tests SHA-256 `22083617a37363f398116749087c4b0a8f080cc3cb27e4ebcaaeb0093342bf35`; + report SHA-256 `b376f8eb13184564ec3836af0c818b65c011b3f5bf385a9f587cfa41f4d8e5ea`. +- Human plots: preserve the original [single-peak examples](../examples/spectrum-fitting/README.md). + Add [multi-peak examples](../examples/spectrum-fitting/MULTI-PEAK.md): six + Gaussian peaks found automatically, seven Lorentzian peaks fitted from center + guesses, and eight Voigt peaks fitted from full guesses, including overlapping + pairs. Four PNGs show totals, individual fitted/true components and residuals; + NPZ/JSON retain physical samples, truth, guesses, fits, covariance, seeds and + hashes. Three actual default-LM calls returned without warnings and preserved + inputs. Residual RMS is 0.00965401–0.01041662 V for 0.01 V simulated read noise. + These are human gut checks, not new numerical acceptance or uniqueness claims. +- Review/check pointer: the multi-peak visual/documentation review accepted the + published plots/data. Runtime, tests, fixtures and verification programs remain + byte-identical to the accepted base. For this CI repair, retain the failed run's + diagnostics, verify the narrow workflow diff independently, and run canonical + full on the exact candidate before pushing under the owner's supplied rules. + A fresh complete hosted workflow must pass; its outcome remains pending until + actual completion. Failed-job-only retries cannot reuse the prior attempt's + sealed collection manifest. +- History/limits: D1/D2 rejections, failed coverage receipt, all rejected test + rounds, numerical failures, cancelled CI and previous spending remain retained. + The artificial 1e12-offset accuracy example stays outside R4. Python 3.8 and + native wider-than-float64 remain unverified; shell-hook coverage is unsupported. + The ten-task maintenance pilot is separate and remains incomplete. +- Next: review and verify the interpreter pin; push the passing candidate to + PR #18, then confirm the complete Linux/macOS/Windows CI gate succeeds. +- Metrics: 33 unchanged scenarios; 297 accepted spectrum cases. Historical + specialist launches A/B 18/14, initial C one, repairs 3/3 spent, D1/D2 + rejected and D3 accepted; 39 completed formal roles. Current documentation + author one; fresh visual reviewer one; design helper one. Current CI repair: + author one, independent diagnosis one, fresh infrastructure reviewer one. Closed A/B windows + 2/2, 1/2, 1/2, 1/2, 2/2, 2/2, 1/2, 1/2, 3/3 remain counted. Historical formal + usage: 50,825,949 input (47,700,736 cached subset), 765,941 output (290,725 + reasoning subset). Current parent/helper and exact billing metering unavailable. + No fixed execution cap, spending reset, further test rewrite or C repair authority. diff --git a/dphtools/utils/_spectrum_fit.py b/dphtools/utils/_spectrum_fit.py new file mode 100644 index 0000000..ba17f45 --- /dev/null +++ b/dphtools/utils/_spectrum_fit.py @@ -0,0 +1,339 @@ +"""Joint unweighted fits of positive, unit-height spectrum components.""" + +import warnings +from types import SimpleNamespace + +import numpy as np +from scipy.optimize import OptimizeWarning, least_squares +from scipy.signal import find_peaks +from scipy.special import voigt_profile + + +def _real_array(value, name): + """Copy real numeric input into finite floating-point storage.""" + array = np.asarray(value) + if array.dtype.kind not in "biuf": + raise ValueError(f"{name} must contain real numeric values") + array = array.astype(float, copy=True) + if not np.isfinite(array).all(): + raise ValueError(f"{name} must contain finite values") + return array + + +def _estimate_rows(data, x, centers, baseline, family): + """Estimate heights and widths without selecting additional components.""" + corrected = data - baseline + floor = max(np.max(np.abs(data)) * 1e-3, np.finfo(float).eps) + rows = [] + for center in centers: + amplitude = max(np.interp(center, x, corrected), floor) + index = np.argmin(np.abs(x - center)) + left = np.flatnonzero(corrected[:index] < amplitude / 2) + right = np.flatnonzero(corrected[index + 1 :] < amplitude / 2) + index + 1 + # Interpolate the half-height crossings in physical coordinates. + lo, hi = x[0], x[-1] + if left.size: + j = left[-1] + lo = x[j] + (x[j + 1] - x[j]) * (amplitude / 2 - corrected[j]) / ( + corrected[j + 1] - corrected[j] + ) + if right.size: + j = right[0] + hi = x[j - 1] + (x[j] - x[j - 1]) * (amplitude / 2 - corrected[j - 1]) / ( + corrected[j] - corrected[j - 1] + ) + fwhm = max(hi - lo, np.min(np.diff(x))) + # Overlap can inflate the measured width of the combined spectrum. + separations = np.abs(centers - center) + separations = separations[separations > 0] + if separations.size: + fwhm = min(fwhm, 2 * separations.min()) + if family == "gauss": + rows.append([amplitude, center, fwhm / np.sqrt(8 * np.log(2))]) + elif family == "lorentz": + rows.append([amplitude, center, fwhm / 2]) + else: + # Split the initial broadening between the two independent widths. + rows.append([amplitude, center, fwhm / 3.6, fwhm / 3.6]) + return np.array(rows) + + +def _covariance(residual, parameters, scales, data_scale, dof): + """Calculate residual-scaled covariance in physical parameter coordinates.""" + # Differentiate on local parameter scales, rather than an absolute x-unit + # floor. Work in dimensionless columns until restoring physical covariance. + steps = np.cbrt(np.finfo(float).eps) * scales + if not np.isfinite(steps).all() or np.any(steps <= 0): + raise RuntimeError("Spectrum covariance differences are not representable") + jacobian = [] + for index, step in enumerate(steps): + delta = np.zeros_like(parameters) + delta[index] = step + upper, lower = parameters + delta, parameters - delta + span = (upper[index] - lower[index]) / scales[index] + jacobian.append((residual(upper) / data_scale - residual(lower) / data_scale) / span) + jacobian = np.column_stack(jacobian) + if not np.isfinite(jacobian).all(): + raise RuntimeError("Spectrum covariance Jacobian is not finite") + norms = np.max(np.abs(jacobian), axis=0) + jacobian /= np.where(norms > 0, norms, 1) + try: + _, singular, vectors = np.linalg.svd(jacobian, full_matrices=False) + except np.linalg.LinAlgError as error: + raise RuntimeError("Spectrum covariance decomposition failed") from error + threshold = np.finfo(float).eps * max(jacobian.shape) * singular[0] + if np.any(singular <= threshold): + warnings.warn("Spectrum parameter covariance could not be estimated", OptimizeWarning) + return np.full((parameters.size, parameters.size), np.inf) + error_scale = np.linalg.norm(residual(parameters) / data_scale) / np.sqrt(dof) + factor = (vectors.T / singular) * (error_scale / norms)[:, None] * scales[:, None] + covariance = factor @ factor.T + if not np.isfinite(covariance).all(): + raise RuntimeError("Spectrum covariance is not representable") + return covariance + + +def spectrum_fit( + data, + xdata=None, + *, + peak_type="gauss", + guesses=None, + background="constant", + prominence=None, + distance=None, + max_nfev=10000, + optimizer="lm", +): + """Fit positive Gaussian, Lorentzian or Voigt peaks jointly to a spectrum. + + Parameters + ---------- + data : array_like + Nonempty finite real one-dimensional point observations. Not modified. + xdata : array_like, optional + Matching finite real strictly increasing coordinates, possibly nonuniform. + Defaults to sample indices. + peak_type : {'gauss', 'lorentz', 'voigt'}, optional + Profile family for all components. Names are case insensitive; + 'gaussian' and 'lorentzian' are aliases. + guesses : array_like, optional + Nonempty one-dimensional centers, or full parameter rows. Gaussian rows + are (height, center, sigma), Lorentzian rows are (height, center, gamma), + and Voigt rows are (height, center, sigma, gamma). Heights and widths + must be positive. Centers are free, even outside the observed interval. + Omission detects local maxima. Supplied guesses are not modified. + background : {'none', 'constant', 'linear'}, optional + Jointly fitted background. A line is b0 + b1 * (x - xdata[0]). + prominence : float, optional + Nonnegative minimum prominence in data units for automatic discovery. + distance : float, optional + Minimum separation in samples, at least one, for automatic discovery. + Neither discovery control may be combined with explicit guesses. + max_nfev : int, optional + Positive optimizer evaluation limit, counted by the selected solver. + optimizer : {'lm'} or callable, optional + Default SciPy Levenberg-Marquardt least squares uses internal log + transforms for positive parameters and coordinate-span conditioning. + Data and returned parameters retain physical units. A callable receives + ``optimizer(residual, initial, bounds=(lower, upper), max_nfev=max_nfev)``. + All callable parameters use physical units in fixed input row order, + followed by background coefficients; residual is data minus model. + Heights/widths have lower bounds zero and upper bounds infinity, while + centers/background are unbounded. Final heights/widths must be positive. + The callable must honor the limit and return finite real 1-D ``x`` and + Boolean ``success``; ``message`` is optional. + + Returns + ------- + result : object + Attributes are ``peak_parameters`` (rows sorted by fitted center), + ``background_parameters``, full ``covariance``, ``fitted`` and + ``residuals`` (data minus fitted). Covariance follows flattened sorted + peak rows then background, including cross correlations in physical units. + Returned arrays do not share storage with caller inputs. + + Raises + ------ + ValueError + For invalid inputs/options, no eligible peaks, or samples not exceeding + the number of fitted parameters. + RuntimeError + For numerical optimizer failure/exhaustion or malformed/nonphysical output. + + Notes + ----- + Amplitudes are individual component peak heights, not areas. Gaussian sigma + is standard deviation; Lorentzian gamma is half width at half maximum. + Voigt is ``voigt_profile(x-center, sigma, gamma) / voigt_profile(0, sigma, + gamma)`` and requires both widths positive. Centers/widths use x units. + + Minimize unweighted squared residuals at the supplied samples, including + nonuniform coordinates. Covariance is the local inverse Jacobian information + scaled by residual sum of squares / (samples - parameters); it is approximate + and does not establish identifiability or global optimality. Singular local + information emits OptimizeWarning and returns infinite covariance. + Discovery promises local-maximum candidates, not endpoint or unresolved peaks. + """ + data = _real_array(data, "data") + if data.ndim != 1 or not data.size: + raise ValueError("data must be a nonempty one-dimensional spectrum") + x = np.arange(data.size, dtype=float) if xdata is None else _real_array(xdata, "xdata") + if x.ndim != 1 or x.shape != data.shape or np.any(np.diff(x) <= 0): + raise ValueError("xdata must match data and be strictly increasing") + aliases = { + "gauss": "gauss", + "gaussian": "gauss", + "lorentz": "lorentz", + "lorentzian": "lorentz", + "voigt": "voigt", + } + if not isinstance(peak_type, str) or peak_type.lower() not in aliases: + raise ValueError("Unsupported peak_type") + family = aliases[peak_type.lower()] + backgrounds = {"none": 0, "constant": 1, "linear": 2} + if not isinstance(background, str) or background not in backgrounds: + raise ValueError("Unsupported background") + nbackground = backgrounds[background] + if ( + isinstance(max_nfev, (bool, np.bool_)) + or not isinstance(max_nfev, (int, np.integer)) + or max_nfev <= 0 + ): + raise ValueError("max_nfev must be a positive integer") + default_optimizer = isinstance(optimizer, str) and optimizer == "lm" + if not default_optimizer and not callable(optimizer): + raise ValueError("optimizer must be 'lm' or a callable") + for name, value, minimum in (("prominence", prominence, 0), ("distance", distance, 1)): + if value is not None: + control = _real_array(value, name) + if control.ndim != 0 or control < minimum: + raise ValueError(f"{name} must be a scalar at least {minimum}") + if guesses is not None and (prominence is not None or distance is not None): + raise ValueError("Discovery controls require guesses=None") + row_size = 4 if family == "voigt" else 3 + if guesses is None: + indices, _ = find_peaks(data, prominence=prominence, distance=distance) + guesses = x[indices] + guesses = _real_array(guesses, "guesses") + if guesses.ndim not in (1, 2) or not guesses.size: + raise ValueError("Expected nonempty centers or full parameter rows") + if guesses.ndim == 2 and guesses.shape[1] != row_size: + raise ValueError("Full guess rows have the wrong number of parameters") + npeak = len(guesses) + size = npeak * row_size + nparameter = size + nbackground + if data.size <= nparameter: + raise ValueError("More samples than fitted parameters are required") + elapsed = x - x[0] + slope = (data[-1] - data[0]) / elapsed[-1] if nbackground == 2 else 0 + intercept = np.min(data - slope * elapsed) + coefficients = np.array([intercept, slope])[:nbackground] + baseline = intercept + slope * elapsed if nbackground else np.zeros_like(data) + rows = _estimate_rows(data, x, guesses, baseline, family) if guesses.ndim == 1 else guesses + initial = np.concatenate((rows.ravel(), coefficients)) + positive = np.r_[np.tile(np.arange(row_size) != 1, npeak), np.zeros(nbackground, dtype=bool)] + if np.any(initial[positive] <= 0): + raise ValueError("Initial amplitudes and widths must be strictly positive") + + def residual(parameters): + """Evaluate the unweighted residual in fixed physical row order.""" + model = np.zeros_like(data) + for row in parameters[:size].reshape(npeak, row_size): + amplitude, center, width = row[:3] + d = x - center + if family == "gauss": + profile = np.exp(-0.5 * (d / width) ** 2) + elif family == "lorentz": + profile = 1 / (1 + (d / width) ** 2) + else: + profile = voigt_profile(d, width, row[3]) / voigt_profile(0, width, row[3]) + model += amplitude * profile + if nbackground: + model += parameters[size] + if nbackground == 2: + model += parameters[size + 1] * elapsed + errors = data - model + if not np.isfinite(errors).all(): + raise RuntimeError("Spectrum residual is not finite") + return errors + + # SciPy also uses ValueError for numerical residual failures. A custom + # callable's ordinary ValueError may instead indicate a programming error. + optimization_errors = ( + ArithmeticError, + ValueError if default_optimizer else np.linalg.LinAlgError, + ) + try: + if default_optimizer: + # Make coordinate steps independent of x units and origin. A line's + # private slope is its physical change across the observed span. + parameter_scale = np.ones(nparameter) + parameter_scale[:size].reshape(npeak, row_size)[:, 1:] = elapsed[-1] + parameter_origin = np.zeros(nparameter) + parameter_origin[:size].reshape(npeak, row_size)[:, 1] = x[0] + if nbackground == 2: + parameter_scale[-1] = 1 / elapsed[-1] + + def physical(parameters): + """Undo private conditioning and positive-parameter log transforms.""" + values = parameters.copy() + values[positive] = np.exp(values[positive]) + return values * parameter_scale + parameter_origin + + transformed = (initial - parameter_origin) / parameter_scale + transformed[positive] = np.log(transformed[positive]) + solution = least_squares( + lambda values: residual(physical(values)), + transformed, + method="lm", + x_scale="jac", + max_nfev=max_nfev, + ) + parameters = physical(solution.x) + else: + lower = np.full(nparameter, -np.inf) + lower[positive] = 0 + solution = optimizer( + residual, + initial.copy(), + bounds=(lower, np.full(nparameter, np.inf)), + max_nfev=max_nfev, + ) + parameters = getattr(solution, "x", None) + except optimization_errors as error: + raise RuntimeError("Spectrum optimization failed") from error + success = getattr(solution, "success", None) + if not isinstance(success, (bool, np.bool_)) or not success: + raise RuntimeError( + f"Spectrum optimization failed: {getattr(solution, 'message', 'invalid status')}" + ) + try: + parameters = _real_array(parameters, "optimizer parameters") + except (ValueError, TypeError) as error: + raise RuntimeError("Malformed optimizer parameters") from error + if parameters.shape != (nparameter,) or np.any(parameters[positive] <= 0): + raise RuntimeError("Optimizer returned parameters outside the physical domain") + scales = np.abs(parameters) + peak_scales = scales[:size].reshape(npeak, row_size) + # A center perturbation follows its component's width, even at center zero + # or after translating the coordinate origin. + peak_scales[:, 1] = np.max(peak_scales[:, 2:], axis=1) + data_scale = max(np.max(np.abs(data)), np.max(peak_scales[:, 0])) + if nbackground: + scales[size] = data_scale + if nbackground == 2: + scales[size + 1] = data_scale / elapsed[-1] + covariance = _covariance(residual, parameters, scales, data_scale, data.size - nparameter) + order = np.argsort(parameters[:size].reshape(npeak, row_size)[:, 1], kind="stable") + permutation = np.r_[ + np.arange(size).reshape(npeak, row_size)[order].ravel(), np.arange(size, nparameter) + ] + errors = residual(parameters) + return SimpleNamespace( + peak_parameters=parameters[:size].reshape(npeak, row_size)[order].copy(), + background_parameters=parameters[size:].copy(), + covariance=covariance[np.ix_(permutation, permutation)], + fitted=data - errors, + residuals=errors, + ) diff --git a/dphtools/utils/fitfuncs.py b/dphtools/utils/fitfuncs.py index cb0354f..61176ca 100644 --- a/dphtools/utils/fitfuncs.py +++ b/dphtools/utils/fitfuncs.py @@ -14,6 +14,7 @@ from scipy.special import betaln, exprel, gammaln, zeta from scipy.stats import nbinom +from ._spectrum_fit import spectrum_fit from .lm import curve_fit diff --git a/tests/test_spectrum_fitting.py b/tests/test_spectrum_fitting.py new file mode 100644 index 0000000..c2172d9 --- /dev/null +++ b/tests/test_spectrum_fitting.py @@ -0,0 +1,2149 @@ +"""Independent public-entry-point tests for SPECTRUM-FITTING-001 contract R3. + +Expected spectra use the contract's unit-height definitions, not product helpers. +See docs/tasks/SPECTRUM-FITTING-001-tests.md for mapping and tolerances. +Run with the source-free launcher in the A packet during blind A/B work. +""" + +import json +import math +import os +import subprocess +import sys +import warnings +from types import SimpleNamespace + +import numpy as np +import pytest +from numpy.testing import assert_allclose, assert_array_equal +from scipy.optimize import least_squares +from scipy.signal import find_peaks +from scipy.special import voigt_profile + +FAMILIES = ("gauss", "lorentz", "voigt") +ROW_SIZE = {"gauss": 3, "lorentz": 3, "voigt": 4} +PARAM_RTOL = 3e-5 +PARAM_ATOL = 2e-5 +MODEL_ATOL = 2e-7 + + +@pytest.fixture(scope="session") +def fit(): + # Importing the approved public module is allowed; never inspect its source. + from dphtools.utils.fitfuncs import spectrum_fit + + return spectrum_fit + + +def _model(x, rows, family, background=()): + """Contract equations: point samples, individual component peak heights.""" + x = np.asarray(x, dtype=float) + y = np.zeros_like(x) + for row in rows: + amplitude, center, width = row[:3] + d = x - center + if family == "gauss": + profile = np.exp(-0.5 * (d / width) ** 2) + elif family == "lorentz": + profile = width**2 / (d**2 + width**2) + else: + gamma = row[3] + profile = voigt_profile(d, width, gamma) / voigt_profile(0, width, gamma) + y += amplitude * profile + if len(background): + y += background[0] + if len(background) == 2: + y += background[1] * (x - x[0]) + return y + + +def _rows(family): + rows = np.array([[4.2, 14.3, 1.1], [2.7, 18.2, 1.5]]) + if family == "voigt": + rows[:, 2] = [0.9, 1.2] + rows = np.column_stack((rows, [0.5, 0.7])) + return rows + + +def _starts(rows): + starts = rows.copy() + starts[:, 0] *= 0.88 + starts[:, 1] += np.array([0.12, -0.16])[: len(rows)] + starts[:, 2:] *= 1.15 + return starts + + +def _noise(n): + i = np.arange(n, dtype=float) + return 0.018 * (np.sin(1.63 * i) + 0.6 * np.cos(0.71 * i)) + + +def _jacobian(x, parameters, family, npeak): + """Independent five-point differences of the stated physical model.""" + size = ROW_SIZE[family] * npeak + + def model_at(p): + return _model(x, p[:size].reshape(npeak, -1), family, p[size:]) + + columns = [] + for j, value in enumerate(parameters): + h = np.finfo(float).eps ** 0.2 * max(1.0, abs(value)) + step = np.zeros_like(parameters) + step[j] = h + columns.append( + ( + model_at(parameters - 2 * step) + - 8 * model_at(parameters - step) + + 8 * model_at(parameters + step) + - model_at(parameters + 2 * step) + ) + / (12 * h) + ) + return np.column_stack(columns) + + +def _check_result(result, x, data, family, npeak, nbackground): + peaks = np.asarray(result.peak_parameters) + background = np.asarray(result.background_parameters) + fitted = np.asarray(result.fitted) + residuals = np.asarray(result.residuals) + covariance = np.asarray(result.covariance) + nparameter = npeak * ROW_SIZE[family] + nbackground + assert peaks.shape == (npeak, ROW_SIZE[family]) + assert background.shape == (nbackground,) + assert fitted.shape == residuals.shape == np.asarray(data).shape + assert covariance.shape == (nparameter, nparameter) + for array in (peaks, background, fitted, residuals, covariance): + assert np.isrealobj(array) + assert np.all(np.isfinite(array)) + assert np.all(peaks[:, 0] > 0) + assert np.all(peaks[:, 2:] > 0) + assert np.all(np.diff(peaks[:, 1]) >= 0) + assert_allclose(fitted, _model(x, peaks, family, background), rtol=2e-10, atol=2e-10) + assert_allclose(residuals, np.asarray(data) - fitted, rtol=2e-12, atol=2e-12) + assert_allclose(covariance, covariance.T, rtol=2e-10, atol=2e-12) + + +def _check_covariance_and_objective(result, x, data, family): + """RSS/(n-p) (J.T J)^-1 in sorted physical coordinates; unweighted optimum.""" + peaks = np.asarray(result.peak_parameters) + p = np.concatenate((peaks.ravel(), result.background_parameters)) + j = _jacobian(x, p, family, len(peaks)) + residual = np.asarray(data) - _model(x, peaks, family, result.background_parameters) + norms = np.linalg.norm(j, axis=0) + # The test fixtures must be locally identifiable in dimensionless coordinates. + assert np.linalg.cond(j / norms) < 1e4 + rss = residual @ residual + assert rss > 1e-8 # Nonzero noise makes covariance scale discriminating. + expected = np.linalg.inv(j.T @ j) * rss / (len(x) - len(p)) + scales = np.sqrt(np.diag(expected)) + assert np.all(scales > 0) + # Normalization measures every entry in its natural uncertainty units. + assert_allclose( + np.asarray(result.covariance) / np.outer(scales, scales), + expected / np.outer(scales, scales), + rtol=0.01, + atol=2e-4, + ) + assert np.min(np.linalg.eigvalsh(np.asarray(result.covariance))) >= -1e-12 + # J.T residual == 0 at an unconstrained, unweighted least-squares optimum. + assert np.max(np.abs(j.T @ residual) / (norms * np.linalg.norm(residual))) < 3e-4 + + +def _preserving_call(fit, data, xdata=None, **kwargs): + inputs = [value for value in (data, xdata, kwargs.get("guesses")) if value is not None] + snapshots = [np.array(value, copy=True) for value in inputs] + try: + result = fit(data, xdata, **kwargs) + finally: + for value, snapshot in zip(inputs, snapshots): + assert_array_equal(value, snapshot) + return result + + +@pytest.mark.parametrize("name", ["gauss", "Gaussian", "GAUSS", "gAuSsIaN"]) +@pytest.mark.parametrize("as_list", [False, True]) +def test_s01_full_gaussian_guesses_and_schema(fit, name, as_list): + x = np.linspace(-6, 8, 281) + truth = np.array([[4.7, 0.65, 0.83]]) + guesses = np.array([[3.9, 0.82, 1.0]]) + y = _model(x, truth, "gauss") + if as_list: + x, y, guesses = x.tolist(), y.tolist(), guesses.tolist() + result = _preserving_call(fit, y, x, peak_type=name, guesses=guesses, background="none") + _check_result(result, x, y, "gauss", 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("name", ["lorentz", "Lorentzian", "LORENTZ", "lOrEnTzIaN"]) +def test_s02_full_lorentzian_guesses(fit, name): + x = np.linspace(-8, 9, 341) + truth = np.array([[3.4, -0.9, 1.27]]) + y = _model(x, truth, "lorentz") + result = _preserving_call( + fit, y, x, peak_type=name, guesses=[[2.9, -0.7, 1.5]], background="none" + ) + _check_result(result, x, y, "lorentz", 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("name", ["voigt", "VoIgT"]) +def test_s03_full_voigt_guesses_independent_widths(fit, name): + x = np.linspace(-9, 11, 401) + truth = np.array([[5.3, 0.35, 0.72, 0.46]]) + y = _model(x, truth, "voigt") + result = _preserving_call( + fit, y, x, peak_type=name, guesses=[[4.6, 0.5, 0.85, 0.38]], background="none" + ) + _check_result(result, x, y, "voigt", 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s04_center_guesses_joint_components(fit, family): + x = np.linspace(7, 31, 481) + truth = _rows(family) + y = _model(x, truth, family, [0.7]) + centers = np.array([18.35, 14.15]) + result = _preserving_call(fit, y, x, peak_type=family, guesses=centers) + _check_result(result, x, y, family, 2, 1) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.background_parameters, [0.7], rtol=PARAM_RTOL, atol=PARAM_ATOL) + + +def test_s04_center_guesses_identifiable_overlap_with_one_maximum(fit): + x = np.linspace(-7, 8, 601) + truth = np.array([[3.3, -0.6, 0.8], [2.1, 0.8, 1.0]]) + y = _model(x, truth, "gauss", [0.4]) + assert len(find_peaks(y)[0]) == 1 # Fixture property, not a discovery promise. + result = _preserving_call(fit, y, x, guesses=[-0.65, 0.9]) + _check_result(result, x, y, "gauss", 2, 1) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("truncated_side", ["left", "right"]) +def test_s04_s08_center_guess_broad_component_truncated_half_height(fit, family, truncated_side): + # Post-implementation correction: a missing half-height crossing does not + # remove the curvature that identifies this component's physical width. + row = [3.4, 7.3, 2.5] if family != "voigt" else [3.4, 7.3, 2.5, 1.25] + truth = np.array([row]) + offsets = (-0.4, 3.0) if truncated_side == "left" else (-3.0, 0.4) + x = row[1] + row[2] * np.linspace(*offsets, 241) + y = _model(x, truth, family) + truncated = y[0] if truncated_side == "left" else y[-1] + observed_tail = y[-1] if truncated_side == "left" else y[0] + assert truncated > row[0] / 2 > observed_tail + result = _preserving_call( + fit, y, x, peak_type=family, guesses=np.array([row[1]]), background="none" + ) + _check_result(result, x, y, family, 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert_allclose(result.residuals, 0, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize( + "controls,count", + [ + ({}, 3), + ({"prominence": 0.5}, 2), + ({"distance": 60}, 2), + ({"prominence": 0.1, "distance": 1.5}, 3), + ], +) +@pytest.mark.parametrize("coordinate_scale", [None, 0.01]) +def test_s05_automatic_discovery_controls_and_index_default( + fit, controls, count, coordinate_scale +): + indices = np.arange(201, dtype=float) + truth = np.array([[3.5, 45, 5], [0.25, 90, 4], [2.6, 145, 6]]) + y = _model(indices, truth, "gauss", [0.4]) + x = indices if coordinate_scale is None else coordinate_scale * indices + kwargs = dict(controls) + if coordinate_scale is not None: + kwargs["xdata"] = x + result = _preserving_call(fit, y, **kwargs) + _check_result(result, x, y, "gauss", count, 1) + expected = truth.copy() if count == 3 else truth[[0, 2]].copy() + expected[:, 1:] *= 1.0 if coordinate_scale is None else coordinate_scale + if count == 3: + assert_allclose(result.peak_parameters, expected, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + else: + # The omitted weak component changes the optimum; only candidate count, + # location, reconstruction, and unweighted stationarity are promised. + assert_allclose(result.peak_parameters[:, 1], expected[:, 1], rtol=0, atol=0.003) + _check_covariance_and_objective(result, x, y, "gauss") + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s06_constant_background_and_covariance(fit, family): + x = np.linspace(7, 31, 81) + truth = _rows(family)[:1] + y = _model(x, truth, family, [1.25]) + _noise(len(x)) + result = _preserving_call(fit, y, x, peak_type=family, guesses=_starts(truth)) + _check_result(result, x, y, family, 1, 1) + assert_allclose(result.peak_parameters, truth, rtol=0, atol=0.04) + assert_allclose(result.background_parameters, [1.25], rtol=0, atol=0.01) + _check_covariance_and_objective(result, x, y, family) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s07_linear_background_first_coordinate_origin(fit, family): + x = np.linspace(7, 31, 241) + truth = _rows(family) + coefficients = np.array([1.1, -0.025]) + y = _model(x, truth, family, coefficients) + result = _preserving_call( + fit, y, x, peak_type=family, guesses=_starts(truth), background="linear" + ) + _check_result(result, x, y, family, 2, 2) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.background_parameters, coefficients, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s08_no_background_multicomponent_sum(fit, family): + x = np.linspace(7, 31, 241) + truth = _rows(family) + y = _model(x, truth, family) + result = _preserving_call( + fit, y, x, peak_type=family, guesses=_starts(truth), background="none" + ) + _check_result(result, x, y, family, 2, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s09_nonuniform_units_full_sorted_covariance_and_storage(fit, family): + x = 7 + 24 * np.linspace(0, 1, 181) ** 1.8 + truth = _rows(family) + y = _model(x, truth, family, [1.1, -0.025]) + _noise(len(x)) + guesses = _starts(truth)[::-1].copy() + result = _preserving_call(fit, y, x, peak_type=family, guesses=guesses, background="linear") + _check_result(result, x, y, family, 2, 2) + assert_allclose(result.peak_parameters, truth, rtol=0, atol=0.05) + assert_allclose(result.background_parameters, [1.1, -0.025], rtol=0, atol=0.01) + _check_covariance_and_objective(result, x, y, family) + snapshots = [a.copy() for a in (x, y, guesses)] + for name in ("peak_parameters", "background_parameters", "covariance", "fitted", "residuals"): + array = np.asarray(getattr(result, name)) + if array.size and array.flags.writeable: + array.flat[0] += 1 + for original, snapshot in zip((x, y, guesses), snapshots): + assert_array_equal(original, snapshot) + + +@pytest.fixture +def valid_data(): + x = np.linspace(-4, 4, 41) + return x, _model(x, [[3, 0.2, 0.9]], "gauss") + + +@pytest.mark.parametrize("shape", [(), (41, 1), (1, 41, 1)]) +def test_s10_non_1d_data(fit, valid_data, shape): + x, y = valid_data + if shape == (): + # Scalar rejection also follows from too few observations; only the + # matrix/3-D cases independently discriminate dimension validation. + data, x = y[0], x[:1] + else: + data = y.reshape(shape) + with pytest.raises(ValueError): + fit(data, x, guesses=[[3, 0.2, 0.9]], background="none") + + +@pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf]) +def test_s11_nonfinite_data(fit, valid_data, value): + x, y = valid_data + y[10] = value + with pytest.raises(ValueError): + fit(y, x, guesses=[0.2]) + + +@pytest.mark.parametrize("imaginary", [0, 0.1]) +def test_s12_complex_data(fit, valid_data, imaginary): + x, y = valid_data + y = y.astype(complex) + y[10] += imaginary * 1j + with pytest.raises(ValueError): + fit(y, x, guesses=[0.2]) + + +@pytest.mark.parametrize("shape", ["scalar", "matrix", "short", "long"]) +def test_s13_coordinate_shape_length(fit, valid_data, shape): + x, y = valid_data + coordinates = { + "scalar": x[0], + "matrix": x.reshape(41, 1), + "short": x[:-1], + "long": np.append(x, x[-1] + (x[-1] - x[-2])), + }[shape] + with pytest.raises(ValueError): + fit(y, coordinates, guesses=[[3, 0.2, 0.9]], background="none") + + +@pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf, 1j, 0j]) +def test_s14_coordinate_real_finite_domain(fit, valid_data, value): + x, y = valid_data + if isinstance(value, complex): + x = x.astype(complex) + x[10] += value + else: + x[10] = value + with pytest.raises(ValueError): + fit(y, x, guesses=[0.2]) + + +@pytest.mark.parametrize("change", ["duplicate", "descending", "one_reversal"]) +def test_s15_strict_coordinate_order(fit, valid_data, change): + x, y = valid_data + if change == "duplicate": + x[10] = x[9] + elif change == "descending": + x = x[::-1] + else: + x[[10, 11]] = x[[11, 10]] + with pytest.raises(ValueError): + fit(y, x, guesses=[0.2]) + + +@pytest.mark.parametrize( + "guesses", + [[], np.empty((0, 3)), np.empty((1, 0)), 0.2, np.ones((1, 1, 3)), [[1, 0, 1], [2, 0]]], +) +def test_s16_empty_or_malformed_guesses(fit, valid_data, guesses): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=guesses) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("delta", [-1, 1]) +def test_s16_wrong_full_guess_row_width(fit, valid_data, family, delta): + guesses = np.ones((1, ROW_SIZE[family] + delta)) + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], peak_type=family, guesses=guesses) + + +@pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf, 1j, 0j]) +@pytest.mark.parametrize("centers_only", [False, True]) +def test_s17_guess_real_finite_domain(fit, valid_data, value, centers_only): + guesses = [value] if centers_only else [[3, value, 0.9]] + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=guesses) + + +@pytest.mark.parametrize( + "family,field", + [ + ("gauss", 0), + ("gauss", 2), + ("lorentz", 0), + ("lorentz", 2), + ("voigt", 0), + ("voigt", 2), + ("voigt", 3), + ], +) +@pytest.mark.parametrize("value", [np.nan, np.inf, -np.inf, 1j, 0j]) +def test_s17_nonfinite_amplitude_or_width(fit, valid_data, family, field, value): + guesses = np.ones( + (1, ROW_SIZE[family]), dtype=complex if isinstance(value, complex) else float + ) + guesses[0, field] = value + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], peak_type=family, guesses=guesses) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("value", [0, -1]) +def test_s18_nonpositive_initial_amplitude(fit, valid_data, family, value): + guesses = np.ones((1, ROW_SIZE[family])) + guesses[0, 0] = value + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], peak_type=family, guesses=guesses) + + +@pytest.mark.parametrize( + "family,field", [("gauss", 2), ("lorentz", 2), ("voigt", 2), ("voigt", 3)] +) +@pytest.mark.parametrize("value", [0, -0.5]) +def test_s19_nonpositive_initial_width(fit, valid_data, family, field, value): + guesses = np.ones((1, ROW_SIZE[family])) + guesses[0, field] = value + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], peak_type=family, guesses=guesses) + + +@pytest.mark.parametrize("family", ["gauss", "lorentz"]) +@pytest.mark.parametrize("side", ["left", "right"]) +@pytest.mark.parametrize("initial_outside", [False, True]) +def test_s20_free_initial_and_fitted_edge_tail_centers(fit, family, side, initial_outside): + x = np.linspace(0, 6, 241) + center = -0.4 if side == "left" else 6.4 + start = ( + (-0.65 if initial_outside else 0.15) + if side == "left" + else (6.65 if initial_outside else 5.85) + ) + truth = np.array([[3.5, center, 1.1]]) + y = _model(x, truth, family) + result = _preserving_call( + fit, y, x, peak_type=family, guesses=[[3.1, start, 0.95]], background="none" + ) + _check_result(result, x, y, family, 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert result.peak_parameters[0, 1] < x[0] or result.peak_parameters[0, 1] > x[-1] + + +@pytest.mark.parametrize("value", ["voight", "mixed", "", None, 3, ["gauss"]]) +def test_s21_invalid_profile(fit, valid_data, value): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=[0.2], peak_type=value) + + +@pytest.mark.parametrize("value", ["quadratic", "", None, 3, ["constant"]]) +def test_s22_invalid_background(fit, valid_data, value): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=[0.2], background=value) + + +@pytest.mark.parametrize("value", [-1, np.nan, np.inf, -np.inf, 1j, "high", [0, 10]]) +def test_s23_invalid_prominence(fit, valid_data, value): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], prominence=value) + + +@pytest.mark.parametrize("value", [0, 0.5, np.nan, np.inf, -np.inf, 1j, "far", [2, 3]]) +def test_s24_invalid_distance(fit, valid_data, value): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], distance=value) + + +@pytest.mark.parametrize("guesses", [[0.2], [[3, 0.2, 0.9]]]) +@pytest.mark.parametrize( + "control", [{"prominence": 0}, {"distance": 1}, {"prominence": 0.1, "distance": 2}] +) +def test_s25_explicit_guesses_reject_discovery_controls(fit, valid_data, guesses, control): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=guesses, **control) + + +@pytest.mark.parametrize("data", [np.zeros(41), np.arange(41), -np.arange(41)]) +def test_s26_no_local_maxima(fit, data): + with pytest.raises(ValueError): + fit(data) + + +def test_s26_no_peaks_meet_prominence(fit, valid_data): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], prominence=10) + + +def test_s27_empty_data(fit): + with pytest.raises(ValueError): + fit([]) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("background,nbackground", [("none", 0), ("constant", 1), ("linear", 2)]) +@pytest.mark.parametrize("sample_delta", [-1, 0]) +@pytest.mark.parametrize("npeak", [1, 2]) +def test_s27_positive_residual_degrees_of_freedom( + fit, family, background, nbackground, sample_delta, npeak +): + n = npeak * ROW_SIZE[family] + nbackground + sample_delta + x = np.linspace(-2, 2, n) + guesses = [[3, 0.2, 0.9]] if family != "voigt" else [[3, 0.2, 0.9, 0.4]] + if npeak == 2: + guesses.append([2, -0.7, 0.6] if family != "voigt" else [2, -0.7, 0.6, 0.3]) + y = _model(x, guesses, family) + with pytest.raises(ValueError): + fit(y, x, peak_type=family, guesses=guesses, background=background) + + +@pytest.mark.parametrize("value", [0, -1, 1.5, np.nan, np.inf, "10", None, [10]]) +def test_s28_invalid_evaluation_limit(fit, valid_data, value): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=[0.2], max_nfev=value) + + +def test_s29_exhausted_default_optimizer_preserves_inputs(fit): + x = np.linspace(-6, 6, 121) + y = _model(x, [[4, 0.4, 0.9]], "gauss") + guesses = np.array([[1.0, 2.0, 3.0]]) + with pytest.raises(RuntimeError): + _preserving_call(fit, y, x, guesses=guesses, background="none", max_nfev=1) + + +def test_s29_failed_custom_status_preserves_inputs(fit, valid_data): + def nonconvergent(residual, initial, *, bounds, max_nfev): + return SimpleNamespace( + x=np.asarray(initial).copy(), success=False, message="evaluation limit reached" + ) + + with pytest.raises(RuntimeError): + _preserving_call( + fit, + valid_data[1], + valid_data[0], + guesses=np.array([[3, 0.2, 0.9]]), + optimizer=nonconvergent, + ) + + +def test_s29_finite_subnormal_voigt_numerical_boundary_preserves_inputs(fit): + # V(s*d; s*sigma, s*gamma) = V(d; sigma, gamma)/s. Its unit-height + # ratio stays finite even when the separate normalized densities overflow. + width = np.finfo(float).tiny / 1024 + x = width * np.linspace(-4, 4, 81) + scaled_x = x / width + scaled_truth = np.array([[3.0, 0.0, 1.0, 1.0]]) + y = _model(scaled_x, scaled_truth, "voigt") + guesses = np.array([[3.0, 0.0, width, width]]) + assert np.all(np.isfinite(x)) and np.all(np.diff(x) > 0) + assert np.all(np.isfinite(y)) and np.all(np.isfinite(guesses)) + assert np.all(guesses[:, [0, 2, 3]] > 0) + try: + result = _preserving_call(fit, y, x, peak_type="voigt", guesses=guesses, background="none") + except RuntimeError as failure: + # Numerical inability is allowed; input validation and leaked solver + # exceptions are not. A robust finite solution is also allowed below. + print("Finite subnormal Voigt numerical failure:", failure) + else: + peaks = np.asarray(result.peak_parameters) + assert peaks.shape == (1, 4) + assert np.isrealobj(peaks) and np.all(np.isfinite(peaks)) + assert np.all(peaks[:, [0, 2, 3]] > 0) + scaled_peaks = peaks.copy() + scaled_peaks[:, 1:] /= width + assert_allclose(scaled_peaks, scaled_truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert np.asarray(result.background_parameters).shape == (0,) + covariance = np.asarray(result.covariance) + assert covariance.shape == (4, 4) and np.isrealobj(covariance) + for values in (result.fitted, result.residuals): + values = np.asarray(values) + assert values.shape == y.shape + assert np.isrealobj(values) and np.all(np.isfinite(values)) + assert_allclose( + result.fitted, _model(scaled_x, scaled_peaks, "voigt"), rtol=2e-10, atol=2e-10 + ) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert_allclose(result.residuals, y - result.fitted, rtol=2e-12, atol=2e-12) + assert_allclose(result.residuals, 0, rtol=0, atol=MODEL_ATOL) + print("Finite subnormal Voigt returned a verified finite exact-model fit") + + +def _stable_narrow_component(x, row, family): + """R3 model; physical Jacobian columns scaled by (height, width, width).""" + amplitude, center, width = map(float, row) + model, sensitivity = [], [] + for coordinate in x: + d = float(coordinate) - center + if family == "gauss": + # Even the largest finite height times exp(-64**2/2) rounds to + # zero. Bound d/width before division; never square the width. + z = d / width if abs(d) <= 64 * width else math.copysign(math.inf, d) + value = math.exp(math.log(amplitude) - 0.5 * z * z) + derivatives = (value, value * z, value * z * z) if value else (0.0,) * 3 + else: + scale = max(abs(d), width) + t, u = d / scale, width / scale + denominator = t * t + u * u + # Log evaluation retains height*profile when profile alone would + # underflow. The scaled denominator is at least one. + value = math.exp( + math.log(amplitude) + + 2 * (math.log(width) - math.log(scale)) + - math.log(denominator) + ) + derivatives = ( + value, + value * 2 * t * u / denominator, + value * 2 * t * t / denominator, + ) + model.append(value) + sensitivity.append(derivatives) + return np.asarray(model), np.asarray(sensitivity) + + +@pytest.mark.parametrize("family", ["gauss", "lorentz"]) +def test_s29_smallest_positive_width_point_samples_preserve_inputs(fit, family): + # Post-implementation regression: this is a valid point-sampled component, + # much narrower than the sampling distance, not an identifiable-width case. + x = np.linspace(-1, 1, 41) + y = np.zeros(41) + y[20] = 2 + width = np.nextafter(0.0, 1.0) + guesses = [[2.0, 0.0, width]] + assert width == np.finfo(float).smallest_subnormal and width > 0 + assert np.all(np.isfinite(x)) and np.all(np.diff(x) > 0) + assert np.all(np.isfinite(y)) and np.all(np.isfinite(guesses)) + assert len(y) > 3 + initial_model, _ = _stable_narrow_component(x, guesses[0], family) + assert_array_equal(initial_model, y) + with warnings.catch_warnings(record=True) as diagnostics: + warnings.simplefilter("always") + try: + result = _preserving_call( + fit, y, x, peak_type=family, guesses=guesses, background="none" + ) + except RuntimeError as failure: + print(f"Smallest-positive-width {family} numerical failure: {failure}") + return + except Exception as failure: + print(f"Smallest-positive-width {family} leaked {type(failure).__name__}: {failure}") + raise + finally: + for diagnostic in diagnostics: + print( + warnings.formatwarning( + diagnostic.message, + diagnostic.category, + diagnostic.filename, + diagnostic.lineno, + line="", + ), + file=sys.stderr, + end="", + ) + peaks = np.asarray(result.peak_parameters) + assert peaks.shape == (1, 3) + assert np.isrealobj(peaks) and np.all(np.isfinite(peaks)) + assert np.all(peaks[:, [0, 2]] > 0) + background = np.asarray(result.background_parameters) + assert background.shape == (0,) and np.isrealobj(background) + for values in (result.fitted, result.residuals): + values = np.asarray(values) + assert values.shape == y.shape + assert np.isrealobj(values) and np.all(np.isfinite(values)) + model, jacobian = _stable_narrow_component(x, peaks[0], family) + assert_allclose(result.fitted, model, rtol=2e-10, atol=2e-10) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert_allclose(result.residuals, y - result.fitted, rtol=2e-12, atol=2e-12) + assert_allclose(result.residuals, 0, rtol=0, atol=MODEL_ATOL) + covariance = np.asarray(result.covariance) + assert covariance.shape == (3, 3) and np.isrealobj(covariance) + assert_allclose(covariance, covariance.T, rtol=2e-10, atol=2e-12, equal_nan=True) + diagonal = np.diag(covariance) + assert np.all(diagonal[np.isfinite(diagonal)] >= 0) + # Positive column rescaling preserves physical rank and estimability. + # Check the returned solution, without demanding any particular width. + scales = np.max(np.abs(jacobian), axis=0) + normalized = jacobian / np.where(scales > 0, scales, 1) + rank = np.linalg.matrix_rank(normalized) + if rank < 3: + unidentifiable = [ + j for j in range(3) if np.linalg.matrix_rank(np.delete(normalized, j, axis=1)) == rank + ] + assert np.all(~np.isfinite(diagonal[unidentifiable])) + assert diagnostics + if np.any(~np.isfinite(covariance)): + assert diagnostics + print(f"Smallest-positive-width {family} returned a verified sampled-model fit") + + +def test_s30_default_is_real_lm_with_positive_domain_and_free_centers(): + # A fresh process observes the public SciPy dependency before product import, + # including implementations using a direct imported solver binding. + script = r""" +import warnings +warnings.formatwarning = lambda message, category, filename, lineno, line=None: ( + f"{category.__name__}: {message} ({filename}:{lineno})\n" +) +import sys +import traceback +sys.excepthook = lambda kind, value, tb: traceback.print_exception(kind, value, None, limit=0) +import json +import numpy as np +import scipy.optimize as optimize +real_solver = optimize.least_squares +calls = [] +def observed_solver(*args, **kwargs): + calls.append(kwargs.get("method", "trf")) + return real_solver(*args, **kwargs) +optimize.least_squares = observed_solver +from dphtools.utils.fitfuncs import spectrum_fit +x = np.linspace(0, 6, 241) +y = 3.5 * np.exp(-0.5 * ((x + 0.4) / 1.1)**2) +a = spectrum_fit(y, x, guesses=[[3.1, 0.15, 0.95]], background="none") +b = spectrum_fit(y, x, guesses=[[3.1, 0.15, 0.95]], background="none", optimizer="lm") +print("SPECTRUM_LM_EVIDENCE=" + json.dumps({ + "methods": calls, "default": np.asarray(a.peak_parameters).tolist(), + "explicit": np.asarray(b.peak_parameters).tolist(), + "fitted": np.asarray(a.fitted).tolist(), +})) +""" + child = subprocess.run( + [sys.executable, "-c", script], capture_output=True, text=True, env=os.environ.copy() + ) + # Retain all warnings, messages, and the child exit status, without source. + print("LM probe exit status:", child.returncode) + if child.stderr: + print(child.stderr, end="") + records = [] + for line in child.stdout.splitlines(): + if line.startswith("SPECTRUM_LM_EVIDENCE="): + records.append(json.loads(line.split("=", 1)[1])) + else: + print(line) + assert child.returncode == 0 + assert len(records) == 1 + evidence = records[0] + assert len(evidence["methods"]) >= 2 + assert set(evidence["methods"]) == {"lm"} + truth = [[3.5, -0.4, 1.1]] + assert_allclose(evidence["default"], truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(evidence["explicit"], truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert_allclose(evidence["default"], evidence["explicit"], rtol=2e-8, atol=2e-8) + x = np.linspace(0, 6, 241) + assert_allclose(evidence["fitted"], _model(x, truth, "gauss"), rtol=0, atol=MODEL_ATOL) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize( + "background,coefficients", [("none", ()), ("constant", (1.1,)), ("linear", (1.1, -0.025))] +) +def test_s31_custom_optimizer_physical_protocol_and_covariance( + fit, family, background, coefficients +): + x = 7 + 24 * np.linspace(0, 1, 161) ** 1.8 + truth = _rows(family) + y = _model(x, truth, family, coefficients) + _noise(len(x)) + guesses = _starts(truth)[::-1].copy() + size = guesses.size + n = size + len(coefficients) + calls = [] + + def adapter(residual, initial, *, bounds, max_nfev): + initial = np.asarray(initial) + assert initial.shape == (n,) + assert np.isrealobj(initial) and np.all(np.isfinite(initial)) + assert_allclose(initial[:size], guesses.ravel(), rtol=0, atol=0) + lower, upper = map(np.asarray, bounds) + expected_lower = np.full(n, -np.inf) + for i in range(len(guesses)): + base = i * ROW_SIZE[family] + expected_lower[base] = 0 + expected_lower[base + 2 : base + ROW_SIZE[family]] = 0 + assert_array_equal(lower, expected_lower) + assert_array_equal(upper, np.full(n, np.inf)) + assert max_nfev == 2345 + # Physical parameter ordering stays fixed even though centers are unsorted. + for shift in (0.0, 0.17): + trial = initial.copy() + trial[0] += shift + trial[1] -= shift + trial[2] += shift + if family == "voigt": + trial[3] += 0.5 * shift + if len(coefficients): + trial[size] -= shift + if len(coefficients) == 2: + trial[-1] += 0.1 * shift + values = residual(trial) + assert np.asarray(values).shape == y.shape + assert_allclose( + values, + y - _model(x, trial[:size].reshape(guesses.shape), family, trial[size:]), + rtol=2e-10, + atol=2e-10, + ) + solution = least_squares(residual, initial, bounds=bounds, max_nfev=max_nfev, method="trf") + calls.append(solution.success) + # Deliberately supply no Jacobian or covariance: the fitter owns them. + return SimpleNamespace( + x=solution.x, success=bool(solution.success), message=solution.message + ) + + result = _preserving_call( + fit, + y, + x, + peak_type=family, + guesses=guesses, + background=background, + optimizer=adapter, + max_nfev=2345, + ) + assert calls and all(calls) + _check_result(result, x, y, family, 2, len(coefficients)) + assert_allclose(result.peak_parameters, truth, rtol=0, atol=0.05) + if len(coefficients): + assert_allclose(result.background_parameters, coefficients, rtol=0, atol=0.01) + _check_covariance_and_objective(result, x, y, family) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s31_coincident_components_warn_without_finite_individual_height_uncertainties( + fit, family +): + # Post-implementation correction: (a1+t, a2-t) leaves the spectrum + # unchanged for coincident equal-width components. Both height columns + # of the physical Jacobian are identical, even at an exact minimizer. + row = [2.0, 0.25, 0.9] if family != "voigt" else [2.0, 0.25, 0.9, 0.4] + truth = np.array([row, row]) + x = np.linspace(-4, 4, 161) + y = _model(x, truth, family) + guesses = truth.copy() + calls = [] + + def exact_minimizer(residual, initial, *, bounds, max_nfev): + solution = truth.ravel().copy() + assert_allclose(residual(solution), 0, rtol=0, atol=2e-12) + calls.append(True) + return SimpleNamespace(x=solution, success=True) + + with warnings.catch_warnings(record=True) as diagnostics: + warnings.simplefilter("always") + try: + result = _preserving_call( + fit, + y, + x, + peak_type=family, + guesses=guesses, + background="none", + optimizer=exact_minimizer, + ) + finally: + # Retain warning diagnostics during blind runs without source lines. + for diagnostic in diagnostics: + print( + warnings.formatwarning( + diagnostic.message, + diagnostic.category, + diagnostic.filename, + diagnostic.lineno, + line="", + ), + file=sys.stderr, + end="", + ) + assert calls and diagnostics + peaks = np.asarray(result.peak_parameters) + assert peaks.shape == truth.shape + assert np.isrealobj(peaks) and np.all(np.isfinite(peaks)) + assert_allclose(peaks, truth, rtol=PARAM_RTOL, atol=PARAM_ATOL) + assert np.asarray(result.background_parameters).shape == (0,) + for values in (result.fitted, result.residuals): + values = np.asarray(values) + assert values.shape == y.shape + assert np.isrealobj(values) and np.all(np.isfinite(values)) + assert_allclose(result.fitted, _model(x, peaks, family), rtol=2e-10, atol=2e-10) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert_allclose(result.residuals, y - result.fitted, rtol=2e-12, atol=2e-12) + covariance = np.asarray(result.covariance) + assert covariance.shape == (truth.size, truth.size) and np.isrealobj(covariance) + # No specific nonfinite encoding, warning wording, or covariance algorithm. + assert np.all(~np.isfinite(np.diag(covariance)[[0, ROW_SIZE[family]]])) + + +@pytest.mark.parametrize("optimizer", ["trf", "dogbox", "LM", None, 42, ["lm"]]) +def test_s32_invalid_optimizer_selection(fit, valid_data, optimizer): + with pytest.raises(ValueError): + fit(valid_data[1], valid_data[0], guesses=[0.2], optimizer=optimizer) + + +@pytest.mark.parametrize( + "defect", + [ + "none", + "missing_x", + "missing_success", + "matrix", + "scalar", + "short", + "long", + "nan", + "inf", + "complex", + "success_string", + "success_integer", + "background_nan", + ], +) +def test_s33_malformed_custom_optimizer_output(fit, valid_data, defect): + def malformed(residual, initial, *, bounds, max_nfev): + p = np.asarray(initial).copy() + if defect == "none": + return None + if defect == "missing_x": + return SimpleNamespace(success=True) + if defect == "missing_success": + return SimpleNamespace(x=p) + if defect == "matrix": + p = p.reshape(1, -1) + elif defect == "scalar": + p = 1.0 + elif defect == "short": + p = p[:-1] + elif defect == "long": + p = np.append(p, 0.0) + elif defect == "nan": + p[0] = np.nan + elif defect == "inf": + p[1] = np.inf + elif defect == "complex": + p = p.astype(complex) + p[0] += 1j + elif defect == "background_nan": + p[-1] = np.nan + success = ( + "True" if defect == "success_string" else 1 if defect == "success_integer" else True + ) + return SimpleNamespace(x=p, success=success) + + with pytest.raises(RuntimeError): + _preserving_call( + fit, + valid_data[1], + valid_data[0], + guesses=np.array([[3, 0.2, 0.9]]), + optimizer=malformed, + ) + + +@pytest.mark.parametrize( + "family,field", + [ + ("gauss", 0), + ("gauss", 2), + ("lorentz", 0), + ("lorentz", 2), + ("voigt", 0), + ("voigt", 2), + ("voigt", 3), + ], +) +@pytest.mark.parametrize("value", [0, -0.1]) +def test_s33_invalid_final_positive_domain_and_input_storage( + fit, valid_data, family, field, value +): + guesses = np.array([[3, 0.2, 0.9]]) if family != "voigt" else np.array([[3, 0.2, 0.9, 0.4]]) + + def invalid(residual, initial, *, bounds, max_nfev): + p = np.asarray(initial).copy() + p[field] = value + # Writable working storage must not alias caller guesses. R3 does not + # require the supplied initial array itself to be writable. + working = np.asarray(initial) + if working.flags.writeable: + working[: len(guesses.ravel())] = 9.0 + return SimpleNamespace(x=p, success=True) + + with pytest.raises(RuntimeError): + _preserving_call( + fit, valid_data[1], valid_data[0], peak_type=family, guesses=guesses, optimizer=invalid + ) + + +def test_s09_s31_gaussian_physical_covariance_transforms_with_x_units(fit): + # Supplementary post-implementation regression. Derivatives come directly + # from m=A*exp(-z**2/2), z=(x-c)/sigma, in physical (A,c,sigma) units. + x = np.linspace(-4, 4, 201) + truth = np.array([[3.0, 0.0, 1.0]]) + profile = np.exp(-0.5 * x * x) + model = 3 * profile + jacobian = np.column_stack((profile, model * x, model * x * x)) + norms = np.linalg.norm(jacobian, axis=0) + assert np.linalg.cond(jacobian / norms) < 3 + q, _ = np.linalg.qr(jacobian, mode="reduced") + i = np.arange(len(x)) + raw_noise = 0.01 * (np.sin(1.63 * i) + 0.6 * np.cos(0.71 * i)) + noise = raw_noise - q @ (q.T @ raw_noise) + y = model + noise + residuals = y - model # Use the represented data, including addition roundoff. + rss = residuals @ residuals + assert 0.001 < rss < 0.1 + assert np.max(np.abs(jacobian.T @ residuals) / (norms * np.linalg.norm(residuals))) < 1e-12 + + # For half the residual sum of squares, H=J.T@J-sum(r_i*m_i''). + # Bound the analytic curvature correction to prove a strict local minimum; + # no iterative optimizer output is used to establish the fixture's solution. + gram = jacobian.T @ jacobian + correction = np.zeros((3, 3)) + correction[0, 1] = residuals @ (profile * x) + correction[0, 2] = residuals @ (profile * x * x) + correction[1, 1] = residuals @ (model * (x * x - 1)) + correction[1, 2] = residuals @ (model * (x**3 - 2 * x)) + correction[2, 2] = residuals @ (model * (x**4 - 3 * x * x)) + correction += np.triu(correction, 1).T + assert np.linalg.norm(correction, 2) < 0.01 * np.min(np.linalg.eigvalsh(gram)) + expected = np.linalg.inv(gram) * rss / (len(x) - truth.size) + uncertainties = np.sqrt(np.diag(expected)) + uncertainty_units = np.outer(uncertainties, uncertainties) + converted_covariances = [] + + for unit_scale in (1.0, 1e-6): + units = np.array([1.0, unit_scale, unit_scale]) + physical_truth = truth * units + physical_x = x * unit_scale + calls = [] + + def known_local_minimizer(residual, initial, *, bounds, max_nfev): + assert_allclose(initial, physical_truth.ravel(), rtol=0, atol=0) + assert_array_equal(bounds[0], [0, -np.inf, 0]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf]) + assert max_nfev == 10 + solution = physical_truth.ravel().copy() + assert_allclose(residual(solution), residuals, rtol=2e-12, atol=2e-12) + trial = (truth + [[0.1, 0.05, 0.03]]) * units + assert_allclose( + residual(trial.ravel()), + y - _model(physical_x, trial, "gauss"), + rtol=2e-12, + atol=2e-12, + ) + calls.append(True) + # Return only a legitimate physical minimizer and success status. + # The fitter independently calculates its Jacobian and covariance. + return SimpleNamespace(x=solution, success=True) + + result = _preserving_call( + fit, + y, + physical_x, + guesses=physical_truth.copy(), + background="none", + optimizer=known_local_minimizer, + max_nfev=10, + ) + assert calls == [True] + _check_result(result, physical_x, y, "gauss", 1, 0) + assert_allclose(result.peak_parameters / units, truth, rtol=2e-12, atol=2e-12) + assert_allclose(result.fitted, model, rtol=2e-12, atol=2e-12) + assert_allclose(result.residuals, residuals, rtol=2e-12, atol=2e-12) + # C'=D C D: convert every physical covariance entry back to base units. + converted = np.asarray(result.covariance) / np.outer(units, units) + converted_covariances.append(converted) + assert_allclose( + converted / uncertainty_units, + expected / uncertainty_units, + rtol=0.01, + atol=2e-4, + err_msg=f"Analytic physical covariance at x-unit scale {unit_scale:g}", + ) + + assert_allclose( + converted_covariances[1] / uncertainty_units, + converted_covariances[0] / uncertainty_units, + rtol=0.01, + atol=2e-4, + err_msg="Physical covariance must transform as D C D when x units change", + ) + + +def _stationary_gaussian_failure_fixture(): + """Analytic physical derivatives, projected noise, and positive curvature.""" + x = np.linspace(-4, 4, 201) + truth = np.array([[3.0, 0.0, 1.0]]) + profile = np.exp(-0.5 * x * x) + model = 3 * profile + jacobian = np.column_stack((profile, model * x, model * x * x)) + norms = np.linalg.norm(jacobian, axis=0) + assert np.linalg.cond(jacobian / norms) < 3 + q, _ = np.linalg.qr(jacobian, mode="reduced") + i = np.arange(len(x)) + raw = 0.01 * (np.sin(1.63 * i) + 0.6 * np.cos(0.71 * i)) + y = model + raw - q @ (q.T @ raw) + residuals = y - model + assert np.max(np.abs(jacobian.T @ residuals) / (norms * np.linalg.norm(residuals))) < 1e-12 + gram = jacobian.T @ jacobian + correction = np.zeros((3, 3)) + correction[0, 1] = residuals @ (profile * x) + correction[0, 2] = residuals @ (profile * x * x) + correction[1, 1] = residuals @ (model * (x * x - 1)) + correction[1, 2] = residuals @ (model * (x**3 - 2 * x)) + correction[2, 2] = residuals @ (model * (x**4 - 3 * x * x)) + correction += np.triu(correction, 1).T + assert np.linalg.norm(correction, 2) < 0.01 * np.min(np.linalg.eigvalsh(gram)) + covariance = np.linalg.inv(gram) * (residuals @ residuals) / (len(x) - 3) + return x, truth, y, model, residuals, jacobian, covariance + + +def _numerical_failure_outcome(fit, data, x, **kwargs): + """Accept only RuntimeError failure; retain every warning and input check.""" + result = None + with warnings.catch_warnings(record=True) as diagnostics: + warnings.simplefilter("always") + try: + result = _preserving_call(fit, data, x, **kwargs) + except RuntimeError as failure: + print("Public numerical failure:", failure) + else: + assert result is not None + finally: + for diagnostic in diagnostics: + print( + warnings.formatwarning( + diagnostic.message, + diagnostic.category, + diagnostic.filename, + diagnostic.lineno, + line="", + ), + file=sys.stderr, + end="", + ) + return result, diagnostics + + +def _check_scaled_gaussian_failure_success(result, x, y, truth, data_unit): + """Verify finite public fit arrays without squaring a large data unit.""" + units = np.array([data_unit, 1.0, 1.0]) + peaks = np.asarray(result.peak_parameters) + assert peaks.shape == (1, 3) + assert np.isrealobj(peaks) and np.all(np.isfinite(peaks)) + assert np.all(peaks[:, [0, 2]] > 0) + assert_allclose(peaks / units, truth / units, rtol=2e-12, atol=2e-12) + background = np.asarray(result.background_parameters) + assert background.shape == (0,) and np.isrealobj(background) + fitted, residuals = map(np.asarray, (result.fitted, result.residuals)) + for values in (fitted, residuals): + assert values.shape == y.shape + assert np.isrealobj(values) and np.all(np.isfinite(values)) + scaled_peaks = peaks / units + assert_allclose(fitted / data_unit, _model(x, scaled_peaks, "gauss"), rtol=2e-10, atol=2e-12) + assert_allclose(residuals / data_unit, (y - fitted) / data_unit, rtol=2e-12, atol=2e-12) + return residuals / data_unit + + +def _check_large_unit_covariance(result, expected, units, diagnostics): + """Check physical entries and variance overflow in the returned dtype.""" + covariance = np.asarray(result.covariance) + assert covariance.shape == (3, 3) and np.isrealobj(covariance) + assert_allclose(covariance, covariance.T, rtol=2e-10, atol=2e-12, equal_nan=True) + diagonal = np.diag(covariance) + assert np.all(diagonal[np.isfinite(diagonal)] >= 0) + if np.any(~np.isfinite(covariance)): + assert diagnostics + if not np.any(expected): + # An exact zero-residual identifiable minimizer has zero covariance. + assert_array_equal(covariance, np.zeros((3, 3))) + return + # Keep the limit in the returned dtype: a wider maximum can exceed float64. + log_limit = np.log(np.finfo(covariance.dtype).max) + for j in range(3): + if math.log(expected[j, j]) + 2 * math.log(units[j]) > log_limit: + assert not np.isfinite(diagonal[j]) + assert diagnostics + # Sequential divisions avoid an overflowing outer product of data units. + converted = covariance / units[:, None] / units[None, :] + uncertainty_units = np.sqrt(np.outer(np.diag(expected), np.diag(expected))) + finite = np.isfinite(covariance) + assert_allclose( + converted[finite] / uncertainty_units[finite], + expected[finite] / uncertainty_units[finite], + rtol=0.01, + atol=2e-4, + ) + + +@pytest.mark.parametrize("height", [1e200, np.finfo(float).max]) +def test_s29_s31_extreme_finite_height_exact_custom_minimum(fit, height): + x = np.arange(-4, 5, dtype=float) + profile = np.exp(-0.5 * x * x) + y = height * profile + truth = np.array([[height, 0.0, 1.0]]) + assert np.all(np.isfinite(y)) and np.all(np.isfinite(truth)) + relative_jacobian = np.column_stack((profile, profile * x, profile * x * x)) + assert np.linalg.cond(relative_jacobian) < 3 + calls = [] + + def exact_minimizer(residual, initial, *, bounds, max_nfev): + assert_array_equal(initial, truth.ravel()) + assert_array_equal(bounds[0], [0, -np.inf, 0]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf]) + assert max_nfev == 10 + assert_allclose(residual(initial) / height, 0, rtol=0, atol=2e-12) + calls.append(True) + return SimpleNamespace(x=initial.copy(), success=True) + + result, diagnostics = _numerical_failure_outcome( + fit, + y, + x, + guesses=truth.copy(), + background="none", + optimizer=exact_minimizer, + max_nfev=10, + ) + if result is None: + return + assert calls == [True] + scaled_residuals = _check_scaled_gaussian_failure_success(result, x, y, truth, height) + assert_allclose(result.fitted / height, profile, rtol=2e-10, atol=2e-12) + assert_allclose(scaled_residuals, 0, rtol=0, atol=2e-12) + expected = ( + np.linalg.inv(relative_jacobian.T @ relative_jacobian) + * (scaled_residuals @ scaled_residuals) + / (len(x) - 3) + ) + _check_large_unit_covariance(result, expected, np.array([height, 1, 1]), diagnostics) + print("Extreme finite height returned a verified exact-model fit:", height) + + +def test_s29_s31_large_noise_units_unrepresentable_height_variance(fit): + x, base_truth, base_y, model, residuals, jacobian, expected = ( + _stationary_gaussian_failure_fixture() + ) + data_unit = 1e160 + units = np.array([data_unit, 1, 1]) + truth = base_truth * units + y = base_y * data_unit + assert np.all(np.isfinite(y)) and np.all(np.isfinite(truth)) + assert math.log(expected[0, 0]) + 2 * math.log(data_unit) > math.log(np.finfo(float).max) + # Scaling finite point data is a unit change. In scaled physical coordinates + # the stationary solution and positive objective Hessian are unchanged. + represented_noise = y / data_unit - model + norms = np.linalg.norm(jacobian, axis=0) + assert ( + np.max( + np.abs(jacobian.T @ represented_noise) / (norms * np.linalg.norm(represented_noise)) + ) + < 1e-12 + ) + calls = [] + + def stationary_minimizer(residual, initial, *, bounds, max_nfev): + assert_array_equal(initial, truth.ravel()) + assert_array_equal(bounds[0], [0, -np.inf, 0]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf]) + assert max_nfev == 10 + assert_allclose(residual(initial) / data_unit, residuals, rtol=2e-12, atol=2e-12) + calls.append(True) + return SimpleNamespace(x=initial.copy(), success=True) + + result, diagnostics = _numerical_failure_outcome( + fit, + y, + x, + guesses=truth.copy(), + background="none", + optimizer=stationary_minimizer, + max_nfev=10, + ) + if result is None: + return + assert calls == [True] + actual_noise = _check_scaled_gaussian_failure_success(result, x, y, truth, data_unit) + assert_allclose(result.fitted / data_unit, model, rtol=2e-12, atol=2e-12) + assert_allclose(actual_noise, residuals, rtol=2e-12, atol=2e-12) + _check_large_unit_covariance(result, expected, units, diagnostics) + print("Large noise units returned a verified fit with physical covariance") + + +@pytest.mark.parametrize("boundary", ["optimizer_lstsq", "covariance_svd"]) +def test_s29_s31_public_numerical_backend_failure(fit, monkeypatch, boundary): + x, truth, y, model, residuals, jacobian, expected = _stationary_gaussian_failure_fixture() + calls, injections = [], [] + + def minimizing_adapter(residual, initial, *, bounds, max_nfev): + assert_array_equal(initial, truth.ravel()) + assert_array_equal(bounds[0], [0, -np.inf, 0]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf]) + assert max_nfev == 10 + observed = residual(initial) + assert_allclose(observed, residuals, rtol=2e-12, atol=2e-12) + calls.append(True) + if boundary == "optimizer_lstsq": + # A real Gauss-Newton stationarity check at the proved strict local + # minimum. The unpatched least-squares step is zero to roundoff. + step = np.linalg.lstsq(jacobian, observed, rcond=None)[0] + assert_allclose(step, 0, rtol=0, atol=2e-12) + return SimpleNamespace(x=initial.copy(), success=True) + + def unavailable_decomposition(matrix, *args, **kwargs): + matrix = np.asarray(matrix) + assert matrix.ndim >= 2 and matrix.size + assert np.all(np.isfinite(matrix)) + if boundary == "optimizer_lstsq": + assert matrix.ndim == 2 and np.isrealobj(matrix) + assert_allclose(matrix, jacobian, rtol=0, atol=0) + assert_allclose(args[0], residuals, rtol=2e-12, atol=2e-12) + injections.append(matrix.shape) + print( + "Injecting public numerical failure:", boundary, "finite operand shape:", matrix.shape + ) + raise np.linalg.LinAlgError("Numerical decomposition did not converge (injected)") + + # Patch only documented NumPy boundaries. A covariance implementation may + # use another valid algorithm/binding or recover; verified success is allowed. + with monkeypatch.context() as patch: + patch.setattr( + np.linalg, + "lstsq" if boundary == "optimizer_lstsq" else "svd", + unavailable_decomposition, + ) + result, diagnostics = _numerical_failure_outcome( + fit, + y, + x, + guesses=truth.copy(), + background="none", + optimizer=minimizing_adapter, + max_nfev=10, + ) + assert calls + print("Public numerical boundary:", boundary, "injected operands:", injections) + if result is None: + assert injections # An unrelated rejection cannot satisfy this case. + return + _check_result(result, x, y, "gauss", 1, 0) + assert_allclose(result.peak_parameters, truth, rtol=2e-12, atol=2e-12) + assert_allclose(result.fitted, model, rtol=2e-12, atol=2e-12) + assert_allclose(result.residuals, residuals, rtol=2e-12, atol=2e-12) + uncertainty_units = np.sqrt(np.outer(np.diag(expected), np.diag(expected))) + assert_allclose( + result.covariance / uncertainty_units, + expected / uncertainty_units, + rtol=0.01, + atol=2e-4, + ) + print("Public numerical boundary returned an independently verified successful fit") + + +@pytest.mark.parametrize("family", ["gauss", "lorentz"]) +@pytest.mark.parametrize("optimizer_mode", ["exact", "lm"]) +def test_s29_large_origin_narrow_width_point_boundary(fit, family, optimizer_mode): + # Valid point observations; coordinate spacing exceeds this positive width. + # A numerical failure or a verified success is permitted, not exact recovery. + x = 1e12 + np.arange(9, dtype=float) + guesses = np.array([[2.0, x[4], 1e-6]]) + y, _ = _stable_narrow_component(x, guesses[0], family) + assert np.all(np.isfinite(x)) and np.all(np.diff(x) > 0) + assert np.all(np.isfinite(y)) and np.all(np.isfinite(guesses)) + assert np.all(guesses[:, [0, 2]] > 0) and len(y) > guesses.size + assert guesses[0, 2] < np.spacing(x[4]) < np.min(np.diff(x)) + roundoff_floor = 8 * np.finfo(float).smallest_subnormal + calls = [] + + def exact_minimizer(residual, initial, *, bounds, max_nfev): + assert_allclose(initial, guesses.ravel(), rtol=0, atol=0) + assert_array_equal(bounds[0], [0, -np.inf, 0]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf]) + assert max_nfev == 10 + values = np.asarray(residual(guesses.ravel().copy())) + assert values.shape == y.shape and np.isrealobj(values) + assert np.all(np.isfinite(values)) + # The generating row has zero mathematical RSS, hence is a minimizer. + # Relative-to-data roundoff resolves the narrow Lorentzian tails. + assert np.all(np.abs(values) <= 2e-12 * np.abs(y) + roundoff_floor) + calls.append(True) + return SimpleNamespace(x=guesses.ravel().copy(), success=True) + + options = {"optimizer": exact_minimizer, "max_nfev": 10} if optimizer_mode == "exact" else {} + with warnings.catch_warnings(record=True) as diagnostics: + warnings.simplefilter("always") + try: + result = _preserving_call( + fit, y, x, peak_type=family, guesses=guesses, background="none", **options + ) + except RuntimeError as failure: + print(f"Large-origin {family}/{optimizer_mode} numerical failure: {failure}") + return + except Exception as failure: + print( + f"Large-origin {family}/{optimizer_mode} leaked {type(failure).__name__}: {failure}" + ) + raise + finally: + for diagnostic in diagnostics: + print( + warnings.formatwarning( + diagnostic.message, + diagnostic.category, + diagnostic.filename, + diagnostic.lineno, + line="", + ), + file=sys.stderr, + end="", + ) + if optimizer_mode == "exact": + assert calls == [True] + peaks = np.asarray(result.peak_parameters) + assert peaks.shape == (1, 3) + assert np.isrealobj(peaks) and np.all(np.isfinite(peaks)) + assert np.all(peaks[:, [0, 2]] > 0) + background = np.asarray(result.background_parameters) + assert background.shape == (0,) and np.isrealobj(background) + for values in (result.fitted, result.residuals): + values = np.asarray(values) + assert values.shape == y.shape + assert np.isrealobj(values) and np.all(np.isfinite(values)) + model, sensitivity = _stable_narrow_component(x, peaks[0], family) + assert_allclose(result.fitted, model, rtol=2e-10, atol=roundoff_floor) + assert_allclose(result.fitted, y, rtol=0, atol=MODEL_ATOL) + assert_allclose(result.residuals, y - result.fitted, rtol=2e-12, atol=2e-12) + assert_allclose(result.residuals, 0, rtol=0, atol=MODEL_ATOL) + covariance = np.asarray(result.covariance) + assert covariance.shape == (3, 3) and np.isrealobj(covariance) + assert_allclose(covariance, covariance.T, rtol=2e-10, atol=2e-12, equal_nan=True) + diagonal = np.diag(covariance) + assert np.all(diagonal[np.isfinite(diagonal)] >= 0) + # Assess the returned row analytically; do not prescribe derivative steps. + scales = np.max(np.abs(sensitivity), axis=0) + normalized = sensitivity / np.where(scales > 0, scales, 1) + rank = np.linalg.matrix_rank(normalized) + if rank < 3: + unidentifiable = [ + j for j in range(3) if np.linalg.matrix_rank(np.delete(normalized, j, axis=1)) == rank + ] + assert np.all(~np.isfinite(diagonal[unidentifiable])) + assert diagnostics + if np.any(~np.isfinite(covariance)): + assert diagnostics + print(f"Large-origin {family}/{optimizer_mode} returned a verified sampled-model fit") + + +def _gaussian_background_physical_jacobian(x, parameters): + """R4 analytic model derivatives in (height V, center nm, sigma nm, b0 V).""" + amplitude, center, sigma, _ = parameters + z = (x - center) / sigma + profile = np.exp(-0.5 * z * z) + return np.column_stack( + ( + profile, + amplitude * profile * z / sigma, + amplitude * profile * z * z / sigma, + np.ones_like(x), + ) + ) + + +def _resolved_gaussian_voltage_fixture(): + """Illustrative analog voltage; independent additive read noise, not counts.""" + x = np.linspace(500, 510, 201) # Wavelength in nm; 0.05 nm sample spacing. + physical = np.array([1.2, 505.1, 0.7, 0.4]) # V, nm, nm, V. + noiseless = _model(x, physical[:3].reshape(1, 3), "gauss", physical[3:]) + noise = np.random.default_rng(20261005).normal(0, 0.01, len(x)) # 0.01 V rms. + guesses = np.array([[1.0, 505.0, 0.85]]) + return x, noiseless + noise, guesses, physical, noiseless, noise + + +@pytest.mark.parametrize("optimizer_mode", ["default_lm", "real_custom_trf"]) +def test_s06_s09_s31_resolved_voltage_gaussian_full_physical_covariance(fit, optimizer_mode): + # R4 physical-scope correction. Fixture and bounds were frozen before calls. + # The two successful-fit cases replace only the artificial 1e12-offset cases. + x, data, guesses, truth, noiseless, noise = _resolved_gaussian_voltage_fixture() + snapshots = [value.copy() for value in (x, data, guesses)] + calls = [] + + def real_physical_optimizer(residual, initial, *, bounds, max_nfev): + # Existing S31/S33 cases cover protocol defects. This is a real fit in + # physical coordinates, with no Jacobian/covariance returned to fitter. + solution = least_squares( + residual, + initial, + jac=lambda parameters: -_gaussian_background_physical_jacobian(x, parameters), + bounds=bounds, + method="trf", + x_scale=[1.2, 0.7, 0.7, 0.4], + ftol=1e-10, + xtol=1e-10, + gtol=1e-10, + max_nfev=max_nfev, + ) + calls.append(bool(solution.success)) + return SimpleNamespace( + x=solution.x, success=bool(solution.success), message=str(solution.message) + ) + + options = {} if optimizer_mode == "default_lm" else {"optimizer": real_physical_optimizer} + # Ordinary representative data must fit successfully; RuntimeError fails. + result = _preserving_call( + fit, data, x, guesses=guesses, background="constant", max_nfev=2000, **options + ) + if optimizer_mode == "real_custom_trf": + assert calls == [True] + _check_result(result, x, data, "gauss", 1, 1) + + peaks = np.asarray(result.peak_parameters) + background = np.asarray(result.background_parameters) + fitted = np.asarray(result.fitted) + residuals = np.asarray(result.residuals) + covariance = np.asarray(result.covariance) + physical = np.concatenate((peaks.ravel(), background)) + # Absolute recovery bounds: 0.020 V, 0.012 nm, 0.012 nm, 0.006 V. + # These generous noise-derived bounds are not confidence-interval claims. + recovery_bounds = np.array([0.020, 0.012, 0.012, 0.006]) + assert np.all(np.abs(physical - truth) <= recovery_bounds) + assert np.sqrt(np.mean((fitted - noiseless) ** 2)) < 0.006 # V rms. + rms = np.sqrt(np.mean(residuals**2)) + assert 0.006 < rms < 0.014 # V; resolved noise must remain in residuals. + + jacobian = _gaussian_background_physical_jacobian(x, physical) + norms = np.linalg.norm(jacobian, axis=0) + condition = np.linalg.cond(jacobian / norms) + assert condition < 4 # Bounds the roundoff sensitivity of the inverse oracle. + rss = residuals @ residuals # V**2; actual residuals, not nominal noise variance. + # Generating truth is feasible: a meaningful least-squares fit improves it. + assert rss <= (noise @ noise) * (1 + 1e-6) + assert np.max(np.abs(jacobian.T @ residuals) / (norms * np.linalg.norm(residuals))) < 3e-4 + expected = np.linalg.inv(jacobian.T @ jacobian) * rss / (len(x) - 4) + scales = np.sqrt(np.diag(expected)) + assert np.all(np.isfinite(expected)) and np.all(scales > 0) + units = np.outer(scales, scales) + normalized = covariance / units + expected_normalized = expected / units + # Every variance/correlation is compared in its own sigma_i*sigma_j units. + # A <=1e-4 columnwise derivative error and condition <4 imply <0.00141 + # normalized inverse error; 0.002 also allows arithmetic/solver roundoff. + assert_allclose(normalized, normalized.T, rtol=0, atol=1e-10) + assert np.min(np.linalg.eigvalsh(normalized)) >= -1e-10 + assert_allclose(normalized, expected_normalized, rtol=0, atol=0.002) + print( + f"{optimizer_mode}: residual rms={rms:.9g} V; RSS={rss:.9g} V**2; " + f"condition={condition:.9g}; " + f"max normalized covariance error={np.max(np.abs(normalized - expected_normalized)):.9g}" + ) + + # Result storage as well as the fit/optimizer must preserve caller inputs. + for values in (peaks, background, fitted, residuals, covariance): + if values.flags.writeable: + values.flat[0] += 1 + for value, snapshot in zip((x, data, guesses), snapshots): + assert_array_equal(value, snapshot) + + +def _resolved_unit_voltage_fixture(family): + """One resolved 1.2 V peak and 0.4 V baseline, with 0.01 V read noise.""" + if family == "gauss": + return _resolved_gaussian_voltage_fixture() + x = np.linspace(500, 510, 201) # nm; 0.05 nm spacing resolves both widths. + row = [1.2, 505.1, 0.7] if family == "lorentz" else [1.2, 505.1, 0.55, 0.3] + truth = np.array(row + [0.4]) # height V, center/widths nm, baseline V. + guesses = np.array([[1.0, 505.0, 0.85]]) + if family == "voigt": + guesses = np.array([[1.0, 505.0, 0.65, 0.4]]) + noiseless = _model(x, truth[:-1].reshape(1, -1), family, truth[-1:]) + seed = 20261006 if family == "lorentz" else 20261007 + noise = np.random.default_rng(seed).normal(0, 0.01, len(x)) + return x, noiseless + noise, guesses, truth, noiseless, noise + + +def _unit_voltage_nm_jacobian(x, parameters, family): + """Independent model Jacobian in V/nm coordinates, never metre-sized steps.""" + if family == "gauss": + return _gaussian_background_physical_jacobian(x, parameters) + amplitude, center, width = parameters[:3] + if family == "lorentz": + z = (x - center) / width + profile = 1 / (1 + z * z) + return np.column_stack( + ( + profile, + 2 * amplitude * z * profile**2 / width, + 2 * amplitude * z * z * profile**2 / width, + np.ones_like(x), + ) + ) + # Five-point O(h**4) differences of the public Voigt equation. Steps use + # local height/width/background scales, not the 505 nm center magnitude. + scales = np.array([1.2, 0.55, 0.55, 0.3, 0.4]) + columns = [] + for index, scale in enumerate(scales): + h = np.finfo(float).eps ** 0.2 * scale + step = np.zeros_like(parameters) + step[index] = h + + def model_at(p): + return _model(x, p[:-1].reshape(1, -1), family, p[-1:]) + + columns.append( + ( + model_at(parameters - 2 * step) + - 8 * model_at(parameters - step) + + 8 * model_at(parameters + step) + - model_at(parameters + 2 * step) + ) + / (12 * h) + ) + return np.column_stack(columns) + + +@pytest.mark.parametrize("family", FAMILIES) +@pytest.mark.parametrize("optimizer_mode", ["default_lm", "real_custom_trf"]) +def test_s01_s02_s03_s09_s30_s31_resolved_voltage_nm_to_m(fit, family, optimizer_mode): + x_nm, data, guesses_nm, truth, noiseless, noise = _resolved_unit_voltage_fixture(family) + nparameter = len(truth) + truth_jacobian = _unit_voltage_nm_jacobian(x_nm, truth, family) + # Frozen noise-derived recovery bounds, computed before either product call. + # Six local standard-error scales are generous bounds, not coverage claims. + truth_scales = np.sqrt(np.diag(np.linalg.inv(truth_jacobian.T @ truth_jacobian))) * 0.01 + recovery = 6 * truth_scales + if family == "gauss": + recovery = np.array([0.020, 0.012, 0.012, 0.006]) # Existing R4 bounds. + covariance_tolerance = 0.01 if family == "voigt" else 0.002 + condition_limit = {"gauss": 4, "lorentz": 5, "voigt": 20}[family] + summaries = [] + for wavelength_unit in (1.0, 1e-9): # nm, then m; voltage samples unchanged. + units = np.ones(nparameter) + units[1:-1] = wavelength_unit + x = x_nm * wavelength_unit + guesses = guesses_nm * units[:-1] + snapshots = [value.copy() for value in (x, data, guesses)] + calls = [] + + def real_scaled_optimizer(residual, initial, *, bounds, max_nfev): + assert_array_equal(initial[:-1], guesses.ravel()) + lower = np.zeros(nparameter) + lower[[1, -1]] = -np.inf + assert_array_equal(bounds[0], lower) + assert_array_equal(bounds[1], np.full(nparameter, np.inf)) + assert max_nfev == 2000 + # This invertible affine change only conditions the adapter's local + # solver. All residual calls and returned values remain physical. + origin = np.zeros(nparameter) + origin[1] = x[0] + local_scale = np.array([1.2, 0.7, 0.7, 0.4]) + if family == "voigt": + local_scale = np.array([1.2, 0.55, 0.55, 0.3, 0.4]) + local_scale *= units + solution = least_squares( + lambda local: residual(origin + local_scale * local), + (initial - origin) / local_scale, + bounds=((bounds[0] - origin) / local_scale, (bounds[1] - origin) / local_scale), + jac="3-point", + method="trf", + ftol=1e-11, + xtol=1e-11, + gtol=1e-11, + max_nfev=max_nfev, + ) + assert solution.nfev <= max_nfev + calls.append((bool(solution.success), int(solution.status), int(solution.nfev))) + return SimpleNamespace( + x=origin + local_scale * solution.x, + success=bool(solution.success), + message=str(solution.message), + ) + + options = {} if optimizer_mode == "default_lm" else {"optimizer": real_scaled_optimizer} + result = _preserving_call( + fit, + data, + x, + peak_type=family, + guesses=guesses, + background="constant", + max_nfev=2000, + **options, + ) + if optimizer_mode == "real_custom_trf": + assert len(calls) == 1 and calls[0][0] and calls[0][1] > 0 + _check_result(result, x, data, family, 1, 1) + physical_nm = np.concatenate( + (result.peak_parameters.ravel(), result.background_parameters) + ) + physical_nm = physical_nm / units + fitted, residuals = np.asarray(result.fitted), np.asarray(result.residuals) + rss = residuals @ residuals # V**2. + rms = np.sqrt(rss / len(data)) # V. + jacobian = _unit_voltage_nm_jacobian(x_nm, physical_nm, family) + norms = np.linalg.norm(jacobian, axis=0) + condition = np.linalg.cond(jacobian / norms) + stationarity = np.max(np.abs(jacobian.T @ residuals) / (norms * np.linalg.norm(residuals))) + print( + f"Physical units {family}/{optimizer_mode}/{wavelength_unit:g}: " + f"RSS={rss:.9g} V**2; rms={rms:.9g} V; stationarity={stationarity:.9g}; " + f"condition={condition:.9g}; custom status/nfev={calls}" + ) + # Independent physical checks prevent two equally wrong unit results + # from validating one another. Ordinary resolved spectra must succeed. + assert np.all(np.abs(physical_nm - truth) <= recovery), "Physical parameter recovery" + assert np.sqrt(np.mean((fitted - noiseless) ** 2)) < 0.006 # V rms. + assert 0.006 < rms < 0.014 # V; retain the measured read noise. + assert rss <= (noise @ noise) * (1 + 1e-6), "Fit must improve feasible truth" + assert stationarity < 3e-4, "Unfinished least-squares objective" + assert condition < condition_limit + expected = np.linalg.inv(jacobian.T @ jacobian) * rss / (len(data) - nparameter) + scales = np.sqrt(np.diag(expected)) + assert np.all(np.isfinite(expected)) and np.all(scales > 0) + covariance_nm = np.asarray(result.covariance) / units[:, None] / units[None, :] + uncertainty_units = np.outer(scales, scales) + normalized = covariance_nm / uncertainty_units + assert_allclose(normalized, normalized.T, rtol=0, atol=1e-10) + assert np.min(np.linalg.eigvalsh(normalized)) >= -1e-10 + assert_allclose( + normalized, expected / uncertainty_units, rtol=0, atol=covariance_tolerance + ) + summaries.append( + (physical_nm.copy(), fitted.copy(), residuals.copy(), covariance_nm.copy()) + ) + for values in ( + result.peak_parameters, + result.background_parameters, + result.fitted, + result.residuals, + result.covariance, + ): + values = np.asarray(values) + if values.flags.writeable: + values.flat[0] += 1 + for value, snapshot in zip((x, data, guesses), snapshots): + assert_array_equal(value, snapshot) + nm, metres = summaries + assert np.all(np.abs(metres[0] - nm[0]) <= 0.05 * truth_scales) + assert_allclose(metres[1], nm[1], rtol=0, atol=2e-5) # V; 0.2% of read noise. + assert_allclose(metres[2], nm[2], rtol=0, atol=2e-5) # Same measured voltage. + # C_m = U C_nm U.T, including every peak/background correlation. Conversion + # above avoids tiny absolute tolerances that could accept wrong m variances. + comparison_units = np.outer(truth_scales, truth_scales) + assert_allclose( + metres[3] / comparison_units, + nm[3] / comparison_units, + rtol=0, + atol=2.5 * covariance_tolerance, + ) + + +def _linear_unit_voltage_nm_jacobian(x, parameters, family, voigt_step=1.0): + """Independent peak derivatives and exact line columns 1 and x-x[0].""" + size = ROW_SIZE[family] + if family != "voigt": + peaks = _unit_voltage_nm_jacobian(x, parameters[:-1], family)[:, :size] + else: + # Center the independent equation before differences. Steps depend on + # resolved local widths, never the wavelength origin or metre units. + local_x = x - x[0] + row = parameters[:size].copy() + row[1] -= x[0] + unit_row = row.copy() + unit_row[0] = 1 + columns = [_model(local_x, unit_row.reshape(1, -1), family)] + for index, scale in ((1, 0.55), (2, 0.55), (3, 0.3)): + h = voigt_step * np.finfo(float).eps ** 0.2 * scale + step = np.zeros(size) + step[index] = h + columns.append( + ( + _model(local_x, (row - 2 * step).reshape(1, -1), family) + - 8 * _model(local_x, (row - step).reshape(1, -1), family) + + 8 * _model(local_x, (row + step).reshape(1, -1), family) + - _model(local_x, (row + 2 * step).reshape(1, -1), family) + ) + / (12 * h) + ) + peaks = np.column_stack(columns) + return np.column_stack((peaks, np.ones_like(x), x - x[0])) + + +@pytest.mark.parametrize("family", FAMILIES) +def test_s01_s02_s03_s07_s09_s30_resolved_linear_voltage_nm_to_m(fit, family): + # A15 fills only the R4 inverse-coordinate slope gap. The 293-case prefix, + # including actual-LM observation and real custom controls, stays intact. + x_nm, constant_data, guesses_nm, constant_truth, constant_noiseless, noise = ( + _resolved_unit_voltage_fixture(family) + ) + slope = 0.015 # V/nm; 0.15 V change over 10 nm, or 1.5e7 V/m. + line_change = slope * (x_nm - x_nm[0]) + data = constant_data + line_change # Same measured voltages in both calls. + noiseless = constant_noiseless + line_change + truth = np.append(constant_truth, slope) + nparameter = len(truth) + truth_jacobian = _linear_unit_voltage_nm_jacobian(x_nm, truth, family) + truth_scales = 0.01 * np.sqrt(np.diag(np.linalg.inv(truth_jacobian.T @ truth_jacobian))) + # ALL bounds below are frozen before product calls. Six local read-noise + # standard-error scales require meaningful recovery, not interval coverage. + recovery = 6 * truth_scales + condition_limit = 20 if family == "voigt" else 6 + covariance_tolerance = 0.012 if family == "voigt" else 0.003 + derivative_control = 1e-7 + stationarity_bound = 3e-4 + model_rms_bound = 0.006 # V; 60% of nominal read-noise rms. + residual_rms_bounds = (0.006, 0.014) # V; ordinary read noise remains. + feasible_objective_slack = 1e-6 + invariance_parameter_bounds = 0.05 * truth_scales + invariance_voltage_bound = 2e-5 # V; 0.2% of the 0.01 V read noise. + invariance_covariance_bound = 2.5 * covariance_tolerance + summaries = [] + if family == "voigt": + half_step = _linear_unit_voltage_nm_jacobian(x_nm, truth, family, voigt_step=0.5) + assert ( + np.max( + np.linalg.norm(half_step - truth_jacobian, axis=0) + / np.linalg.norm(truth_jacobian, axis=0) + ) + < derivative_control + ) + + for wavelength_unit in (1.0, 1e-9): # nm, m; V observations unchanged. + units = np.ones(nparameter) + units[1 : ROW_SIZE[family]] = wavelength_unit # Center and widths. + units[-1] = 1 / wavelength_unit # Inverse-coordinate background slope. + x = x_nm * wavelength_unit + guesses = guesses_nm * units[: ROW_SIZE[family]] + snapshots = [value.copy() for value in (x, data, guesses)] + # Omit optimizer: exercise the real default LM, with no custom adapter. + # RuntimeError fails; representative fits must succeed in both units. + result = _preserving_call( + fit, + data, + x, + peak_type=family, + guesses=guesses, + background="linear", + max_nfev=2000, + ) + _check_result(result, x, data, family, 1, 2) + physical_nm = ( + np.concatenate((result.peak_parameters.ravel(), result.background_parameters)) / units + ) + fitted = np.asarray(result.fitted) + residuals = np.asarray(result.residuals) + jacobian = _linear_unit_voltage_nm_jacobian(x_nm, physical_nm, family) + if family == "voigt": + half_step = _linear_unit_voltage_nm_jacobian(x_nm, physical_nm, family, voigt_step=0.5) + assert ( + np.max( + np.linalg.norm(half_step - jacobian, axis=0) / np.linalg.norm(jacobian, axis=0) + ) + < derivative_control + ) + norms = np.linalg.norm(jacobian, axis=0) + condition = np.linalg.cond(jacobian / norms) + rss = residuals @ residuals # V**2. + rms = np.sqrt(rss / len(data)) # V. + stationarity = np.max(np.abs(jacobian.T @ residuals) / (norms * np.sqrt(rss))) + print( + f"Linear physical units {family}/default_lm/{wavelength_unit:g}: " + f"RSS={rss:.9g} V**2; rms={rms:.9g} V; " + f"stationarity={stationarity:.9g}; condition={condition:.9g}" + ) + # Each unit result must satisfy independent truth/objective checks. + # Two wrong fits cannot validate one another by conversion alone. + assert np.all(np.abs(physical_nm - truth) <= recovery), "Physical recovery" + assert np.sqrt(np.mean((fitted - noiseless) ** 2)) < model_rms_bound + assert residual_rms_bounds[0] < rms < residual_rms_bounds[1] + assert rss <= (noise @ noise) * (1 + feasible_objective_slack) + assert stationarity < stationarity_bound, "Unfinished least-squares objective" + assert condition < condition_limit + # Full physical covariance: every peak/line entry, including b0/b1. + # Five parameters for Gauss/Lorentz; six for Voigt; dof 196 or 195. + expected = np.linalg.inv(jacobian.T @ jacobian) * rss / (len(data) - nparameter) + scales = np.sqrt(np.diag(expected)) + assert np.all(np.isfinite(expected)) and np.all(scales > 0) + covariance_nm = np.asarray(result.covariance) / units[:, None] / units[None, :] + uncertainty_units = np.outer(scales, scales) + normalized = covariance_nm / uncertainty_units + assert_allclose(normalized, normalized.T, rtol=0, atol=1e-10) + assert np.min(np.linalg.eigvalsh(normalized)) >= -1e-10 + assert_allclose( + normalized, expected / uncertainty_units, rtol=0, atol=covariance_tolerance + ) + summaries.append( + (physical_nm.copy(), fitted.copy(), residuals.copy(), covariance_nm.copy()) + ) + for values in ( + result.peak_parameters, + result.background_parameters, + result.fitted, + result.residuals, + result.covariance, + ): + values = np.asarray(values) + if values.flags.writeable: + values.flat[0] += 1 + for value, snapshot in zip((x, data, guesses), snapshots): + assert_array_equal(value, snapshot) + + nm, metres = summaries + assert np.all(np.abs(metres[0] - nm[0]) <= invariance_parameter_bounds) + assert_allclose(metres[1], nm[1], rtol=0, atol=invariance_voltage_bound) + assert_allclose(metres[2], nm[2], rtol=0, atol=invariance_voltage_bound) + # C_m = U*C_nm*U.T, with 1e9 for b1 and 1e-9 for center/widths. + # Convert to nm uncertainty units so tiny SI entries cannot hide defects. + comparison_units = np.outer(truth_scales, truth_scales) + assert_allclose( + metres[3] / comparison_units, + nm[3] / comparison_units, + rtol=0, + atol=invariance_covariance_bound, + ) + + +def _check_s29_s31_caller_warning_preserved(diagnostics, caller_warning): + """Check known caller provenance; do not classify arbitrary English text.""" + assert any( + item.category is type(caller_warning) and str(item.message) == str(caller_warning) + for item in diagnostics + ), "The caller's applicable numerical warning must be preserved" + + +def _check_s29_s31_runtime_error_minimum(failure): + """Only a minimum; B/D assess the actual error's numerical relevance.""" + assert isinstance(failure, RuntimeError) + assert str(failure).strip(), "Numerical inability needs nonempty RuntimeError text" + + +def _check_s29_s31_smallest_width_success(result, x, data, diagnostics, caller_warning): + """R4 sampled-model/uncertainty oracle; no width-recovery requirement.""" + assert result is not None, "A numerical failure must not silently return None" + peaks = np.asarray(result.peak_parameters) + background = np.asarray(result.background_parameters) + fitted, residuals = np.asarray(result.fitted), np.asarray(result.residuals) + covariance = np.asarray(result.covariance) + assert peaks.shape == (1, 3) and background.shape == (1,) + assert fitted.shape == residuals.shape == data.shape + for values in (peaks, background, fitted, residuals): + assert np.isrealobj(values) and np.all(np.isfinite(values)) + assert np.all(peaks[:, [0, 2]] > 0) + model, _ = _stable_narrow_component(x, peaks[0], "gauss") + assert_allclose(fitted, model + background[0], rtol=2e-10, atol=2e-10) + assert_allclose(fitted, data, rtol=0, atol=MODEL_ATOL) + assert_allclose(residuals, data - fitted, rtol=2e-12, atol=2e-12) + assert covariance.shape == (4, 4) and np.isrealobj(covariance) + assert_allclose(covariance, covariance.T, rtol=2e-10, atol=2e-12, equal_nan=True) + diagonal = np.diag(covariance) + # Variances are squared uncertainties: -inf is invalid, as is any negative. + assert np.all(np.isnan(diagonal) | (diagonal >= 0)) + unavailable = diagonal[[1, 2]] + assert np.all(np.isnan(unavailable) | np.isposinf(unavailable)) + _check_s29_s31_caller_warning_preserved(diagnostics, caller_warning) + return peaks, background, fitted, residuals, covariance + + +def test_s29_s31_smallest_positive_gaussian_width_real_custom_lm(fit): + from scipy.optimize import OptimizeWarning + + # R4 numerical robustness only. This boundary is not a resolved physical fit. + x = np.linspace(-1.0, 1.0, 81) + width = np.nextafter(0.0, 1.0) + guesses = np.array([[1.0, 0.0, width]]) + data = np.full(x.size, 0.5) + data[x == 0.0] += 1.0 + initial_model, sensitivity = _stable_narrow_component(x, guesses[0], "gauss") + assert_array_equal(initial_model + 0.5, data) + wider = guesses[0].copy() + wider[2] *= 2 + assert_array_equal(_stable_narrow_component(x, wider, "gauss")[0] + 0.5, data) + assert_array_equal(sensitivity[:, 1:], np.zeros((len(x), 2))) + assert np.linalg.matrix_rank(np.column_stack((sensitivity, np.ones(len(x))))) == 2 + assert len(x) - 4 == 77 + snapshots = [values.copy() for values in (data, x, guesses)] + roundoff = 64 * np.finfo(float).eps * max(1.0, np.max(np.abs(data))) + calls = [] + caller_warning = OptimizeWarning( + "Caller LM solve reached the sampled minimum; center/width uncertainty is unavailable." + ) + + def real_optimizer(residual, initial, *, bounds, max_nfev): + initial = np.asarray(initial) + assert initial.shape == (4,) and np.isrealobj(initial) + assert np.all(np.isfinite(initial)) + assert_array_equal(initial[:3], guesses.ravel()) + assert_array_equal(bounds[0], [0, -np.inf, 0, -np.inf]) + assert_array_equal(bounds[1], [np.inf, np.inf, np.inf, np.inf]) + assert max_nfev == 1000 + positive = np.isfinite(bounds[0]) + evaluations = [] + + def physical(transformed): + parameters = np.array(transformed, copy=True) + parameters[positive] = np.exp(parameters[positive]) + return parameters + + def evaluate(parameters): + assert parameters.shape == (4,) and np.isrealobj(parameters) + assert np.all(np.isfinite(parameters)) + assert np.all(parameters[positive] > 0) + observed = np.asarray(residual(parameters)) + assert observed.shape == data.shape and np.isrealobj(observed) + assert np.all(np.isfinite(observed)) + model, _ = _stable_narrow_component(x, parameters[:3], "gauss") + assert_allclose(observed, data - (model + parameters[3]), rtol=0, atol=roundoff) + evaluations.append(parameters.copy()) + return observed + + before = evaluate(initial) + transformed_start = initial.copy() + transformed_start[positive] = np.log(transformed_start[positive]) + # LM minimizes the supplied residual in the adapter's own coordinates. + solved = least_squares( + lambda transformed: evaluate(physical(transformed)), + transformed_start, + method="lm", + max_nfev=max_nfev, + ) + final = physical(solved.x) + after = evaluate(final) + initial_rss, final_rss = float(before @ before), float(after @ after) + assert bool(solved.success), "The real boundary-witness optimizer must converge" + assert final_rss <= initial_rss + len(data) * roundoff**2 + calls.append( + dict( + method="lm", + success=bool(solved.success), + nfev=int(solved.nfev), + physical_evaluations=len(evaluations), + all_domain_valid=all( + np.all(np.isfinite(values)) and np.all(values[positive] > 0) + for values in evaluations + ), + initial_rss=initial_rss, + final_rss=final_rss, + final=final.tolist(), + ) + ) + # The independent sampled-model proof establishes this caller diagnosis. + warnings.warn(caller_warning) + # Return actual solver output in physical units, never the fixture truth. + return SimpleNamespace(x=final, success=bool(solved.success), message=solved.message) + + result = None + with warnings.catch_warnings(record=True) as diagnostics: + warnings.simplefilter("always") + try: + result = _preserving_call( + fit, + data, + x, + peak_type="gauss", + guesses=guesses, + background="constant", + optimizer=real_optimizer, + max_nfev=1000, + ) + except RuntimeError as failure: + _check_s29_s31_runtime_error_minimum(failure) + print(f"Smallest-width real custom LM: RuntimeError: {failure}") + else: + assert result is not None, "A numerical failure must not silently return None" + finally: + for diagnostic in diagnostics: + print( + warnings.formatwarning( + diagnostic.message, + diagnostic.category, + diagnostic.filename, + diagnostic.lineno, + line="", + ), + file=sys.stderr, + end="", + ) + print("Smallest-width real SciPy solve:", json.dumps(calls, sort_keys=True)) + assert calls, "The actual configured SciPy LM solver must run" + _check_s29_s31_caller_warning_preserved(diagnostics, caller_warning) + if result is None: + return + arrays = _check_s29_s31_smallest_width_success(result, x, data, diagnostics, caller_warning) + assert_array_equal(arrays[0].ravel(), calls[0]["final"][:3]) + assert_array_equal(arrays[1], calls[0]["final"][3:]) + print("Smallest-width real custom LM: verified fit with unavailable uncertainty") + for values in arrays: + if values.flags.writeable: + values.flat[0] = 0 + for values, snapshot in zip((data, x, guesses), snapshots): + assert_array_equal(values, snapshot)