diff --git a/ref/research/1sar_rerun_2026_11_06_preregistration.md b/ref/research/1sar_rerun_2026_11_06_preregistration.md new file mode 100644 index 0000000..4f491a5 --- /dev/null +++ b/ref/research/1sar_rerun_2026_11_06_preregistration.md @@ -0,0 +1,168 @@ +# 1SAR Trajectory Remeasurement — Preregistration (`2026-11-06`, No Results) + +Registered **before any scientific tool execution** on branch `analysis/1sar-rerun-2026-11-06` (based on `main` at `c2b890610a2eb08cdc515a20b41ec2f183f89d81`), in a commit containing **no new results** (`NO-RESULTS`). Every prediction, selection rule, canary acceptance check, and admissibility boundary below is fixed prior to invoking PHENIX, CCP4, Gemmi, Reduce, or Probe. + +--- + +## 1. Scientific Question and Role of Historical Decks + +The objective of this study is to independently remeasure the retained four-model `1SAR` (ribonuclease Sa from *Streptomyces aureofaciens*) refinement trajectory — from the actual archived starting model (`data/1sar.pdb`) through the packaged round-4 model (`data/1sar_final.pdb`) and the retained round-5 (`data/1sar_round5.pdb`) and round-6 (`data/1sar_round6.pdb`) intermediate models — against the shared archived reflection dataset (`data/1sar.mtz`), preserving exact atom and reflection selections, raw logs, model/map arrays, and build identities so what independent tools report can be compared transparently. + +The historical slide decks (`data/coscientists/openscientist/slides_2026-05-04/build_slides.py` and `data/coscientists/openscientist/slides_2026-05-05/build_slides.py`) serve **solely as an observable menu** identifying the task categories originally plotted (`05_t01_rmsd` through `13_t13`). They are not current scientific evidence or authority; where historical claims lacked retained raw outputs or mixed incompatible selections (`data/coscientists/openscientist/EVAL_1sar_cdba2c07_2026-09-23.md`), this rerun collects fresh, provenance-bound observations without modifying any frozen historical `EVAL_*` or `QDS_*` record. + +--- + +## 2. Exact Subjects, Shared Reflection Dataset, and Reference Boundaries + +All inputs come from the committed co-scientist archive `data/coscientists/openscientist/cdba2c07-daff-4f60-ae96-12452b3a5fbb_artifacts.zip` and the separate deposited PDB reference `data/pdb_mtz/1sar_deposited.pdb`. No coordinate refinement, external data download, or web-service call will be performed. + +| Role | Path / Archive Member | Canonical `subject_ref` | Stage | SHA-256 Digest | Retained Input Composition (Pre-Execution Metadata) | +|---|---|---|---|---|---| +| **Archive container** | `data/coscientists/openscientist/cdba2c07-daff-4f60-ae96-12452b3a5fbb_artifacts.zip` | — | — | `9fc5352284b7ffa7763c633a3eb4d3d0db96b8c25a1be9bcea361722d2c53054` | `241` total archive members, `12` PDB coordinate members; exhaustive inspection confirms zero round-7 coordinate files exist in the archive. | +| **Trajectory start (Round 0)** | `archive#data/1sar.pdb` | `artifact:cdba2c07-daff-4f60-ae96-12452b3a5fbb#data/1sar.pdb` | `start` | `6b34c5d0dec6647df20eae9021ff7d511d6dedc03b4349c0b634d8e5b1f869e4` | `1,498` heavy atoms (`1,488` standard amino-acid atoms across `192` residues in chains A and B, `0` `HOH` waters, and two `SO4` residues `A:97:SO4` and `B:97:SO4` totaling `10` atoms stored on `ATOM` records). | +| **Packaged model (Round 4)** | `archive#data/1sar_final.pdb` | `artifact:cdba2c07-daff-4f60-ae96-12452b3a5fbb#data/1sar_final.pdb` | `final` | `8cbc1650f8be5a3833f98833fa514ec7d490c2c3bb9aa930413abac05e2a510b` | `1,641` heavy atoms (`1,488` protein atoms, `146` `HOH` waters, one `SO4` residue `A:97:SO4` (`5` atoms on `ATOM` records), `CA A 98` (`1` atom on `HETATM`), and `NA B 98` (`1` atom on `HETATM`)). | +| **Retained Round 5** | `archive#data/1sar_round5.pdb` | `artifact:cdba2c07-daff-4f60-ae96-12452b3a5fbb#data/1sar_round5.pdb` | `intermediate` | `25c140b46e18a1d1ec211d985868894f227c827ea0c76f7187d41e1f75c03733` | `1,640` heavy atoms (`1,488` protein atoms, `145` `HOH` waters, one `SO4` residue `A:97:SO4` (`5` atoms on `ATOM` records), `CA A 98` (`1` atom), and `NA B 98` (`1` atom)). | +| **Retained Round 6** | `archive#data/1sar_round6.pdb` | `artifact:cdba2c07-daff-4f60-ae96-12452b3a5fbb#data/1sar_round6.pdb` | `intermediate` | `5d01ce3645d1295a42f620987785fd8368441418e82a0b6d6992d7f79500c110` | `1,646` heavy atoms (`1,488` protein atoms, `151` `HOH` waters, one `SO4` residue `A:97:SO4` (`5` atoms on `ATOM` records), `CA A 98` (`1` atom), and `NA B 98` (`1` atom)). **Note:** `data/1sar_round6.pdb` does **not** contain `B:97:SO4`; it carries the same single `A:97:SO4` plus `CA` and `NA` hetero composition as R4 and R5. | +| **Reported Round 7 (absent)** | Unarchived (`/agent/job/1sar_round7_001.pdb` in `claude_iterations.log:510, :752`) | *None (log-only claim)* | — | *Unavailable* | Coordinates and raw tool outputs were never retained in the archive; omitted from all measured trajectory lines and kept strictly as a log-attested narrative boundary. | +| **Shared reflection dataset** | `archive#data/1sar.mtz` (copy at `data/coscientists/openscientist/retained_evidence_2026-09-23/data/1sar.mtz`) | `mtz:sha256:f36d5fe685a3e524e8809d17d19ce12073acdf339eba2cfd3b0347a818850ab2` | `all` | `f36d5fe685a3e524e8809d17d19ce12073acdf339eba2cfd3b0347a818850ab2` | Shared observed reflection data used for every subject: columns `F-obs`, `SIGF-obs`, and `R-free-flags` with explicit `1 = test` (`P 21 21 21`, cell `64.897, 78.323, 38.792 Å, 90°, 90°, 90°`, `49.972–2.500 Å`). | +| **Deposited native reference** | `data/pdb_mtz/1sar_deposited.pdb` | `pdb:1sar_deposited` | — | `e60e5db6c7eb554a3ac4e184bdf7cbb7d69b682e287579bdcf5f8bf279d51f86` | External `1.80 Å` deposited reference only (`REMARK 3 R VALUE = 0.172`, `FREE R VALUE : NULL`); **never** treated as the agent's starting model or as a trajectory point. | + +All four trajectory coordinate files have zero hydrogen/deuterium atoms, zero alternate-location indicators (`altloc == " "`), and zero `ANISOU` records. Every retained atom record is preserved as-is, classifying chemical components by full residue identity rather than `ATOM` versus `HETATM` record prefix (because `SO4` is stored on `ATOM` records, and the `CA` ion residue must be distinguished from protein `CA` backbone atoms). + +The same original `data/1sar.mtz` bytes and observed columns (`F-obs`, `SIGF-obs`, `R-free-flags` with `1 = test`) are supplied to every reciprocal-space and map calculation. If a derived observation-only MTZ projection is written for a tool, it retains only `H`, `K`, `L`, `F-obs`, `SIGF-obs`, `R-free-flags`, and symmetry/cell headers — never importing the archived `F-model` or map-coefficient columns (`2FOFCWT`, `FOFCWT`) — and records the exact column projection and input-domain ledger. Input-file row counts are metadata only and are never assumed to be effective tool denominators. + +--- + +## 3. Preregistered Measurement Menu and Tool Pairings + +All runtime executables are resolved through `scripts/toolchain.py` (`phenix`, `run_logged`, `phenix_build_evidence`, `run_refmac`, `ccp4_environment`, `gemmi_executable`) without adding per-runner path constants. This rerun does not expand to `T15`, `T16`, `TM-align`, or new coordinate-refinement controls. + +| Family | Planned Local Paths (`scripts/toolchain.py`) | Matched Selection and Retained Evidence | Methodological Boundaries and Planned Serial Runtime | +|---|---|---|---| +| **T01 Cα displacement** | 1. `cctbx` least-squares superposition API via configured `phenix.python` (`toolchain.phenix("phenix.python")`).
2. Independent `gemmi` superposition.
3. `scripts/assess_refinement.py::compare_pdb_pair` / `kabsch_rmsd` (`NumPy` Kabsch). | Exact intersection of protein `CA` atom keys `(chain, residue number, insertion code, residue name, atom name, altloc)`, with actual start (`data/1sar.pdb`) fixed and no trimming, morphing, or sequence rematching. Retain ordered key and coordinate arrays, rigid-body rotation/translation transform, `n`, per-residue Cα displacements, and overall RMSD in Å. Separate comparisons against `data/pdb_mtz/1sar_deposited.pdb` are explicitly labelled as deposited-reference comparisons. | Scoring identical key sets tests implementation agreement only, not structural improvement. `TM-align` is absent at its configured path (`scripts/toolchain.py::TMALIGN`) and will not be installed. Estimated runtime: `< 2 min` total. | +| **T03 native R-work / R-free / gap** (`T03_r-work`, `T03_r-free`, `T03_r-free_r-work_gap`) | 1. `phenix.model_vs_data` first-block public API adapter (`mmtbx/model_vs_data.py` via `toolchain.phenix("phenix.python")` and `refinement_cache.cached_phenix_operation`).
2. `REFMAC5` (`toolchain.run_refmac`) with explicit `LABIN FP=F-obs SIGFP=SIGF-obs FREE=R-free-flags`, `FREE 1`, `REFI BREF MIXED`, `NCYC 0`. | Shared observed `data/1sar.mtz`; retain raw input, input-domain, extracted, and final effective HKL and flag ledgers, raw logs, full-precision PHENIX arrays (`fmodel.r_work()`, `fmodel.r_free()`, `fmodel.f_obs_work()`, `fmodel.f_obs_free()`, `f_model_scaled_with_k1_w()`, `f_model_scaled_with_k1_t()`), native REFMAC printed precision (plus `XMLOUT` if emitted), per-tool model/scaling/output files, and R numerator/denominator sums. | PHENIX data/model-dependent outlier rejection and separate work/free `k1` scaling (`scale_k1_w()`, `scale_k1_t()`) are preserved; REFMAC output `FP` is rescaled and map-filled HKLs are not an observed mask. Unequal native HKL sets are labelled `non-comparable`; never average or transfer the §3 direct-sum `gemmi sfcalc` `R-work` band to `REFMAC5`. Estimated runtime: `2–15 min` canary; `10–40 min` across four subjects. | +| **T05 bond/angle RMSD and RMSZ** | 1. `phenix.model_statistics` with `pdb_interpretation.restraints_library.cdl=True` and `pdb_interpretation.restraints_library.cdl=False` (`Engh & Huber`; `scripts/bench_t05_restraint_library.py`).
2. Independent `gemmi rmsz` via `toolchain.gemmi_executable` using the configured CCP4 monomer library (`toolchain.ccp4_environment`). | Full exact heavy-atom model for each subject; retain restraint-library identities, complete raw logs, and restrained bond/angle counts; where available export restraint membership and raw distances, angles, targets, and sigmas. | Keep dimensional RMSD (`Å`, `°`) distinct from dimensionless RMSZ (current emitter routing governs QDS fields; unregistered RMSZ values remain raw structured evidence rather than catalog metric IDs or direct QDS field injections). Label cross-library and population differences; never call a difference a pure library effect unless proxy membership matches. Estimated runtime: `5–15 min`. | +| **T05 Ramachandran and rotamer classifications** | `phenix.ramalyze` and `phenix.rotalyze` with full residue output (`refinement_cache.cached_phenix_operation`). | Retain eligible residue IDs `(chain, resseq, icode, resname)`, per-residue class/score, and exact denominator separately for each classifier (`OUTLIER` vs `Allowed`/`Favored`). | Standalone `reduce`/`probe` do not supply independent Ramachandran or rotamer classifiers. Retained as `cctbx`-only informational evidence with no aggregate quality verdict and no disguised independent oracle. Estimated runtime: `2–8 min`. | +| **T05 clashes and H convention** | 1. `phenix.clashscore` (`mmtbx/validation/clashscore.py:180–187, 395–398, 415–424` derives `occ_max` from the model, chooses `min(10, occ_max)`, and formats Probe selectors; for these `max_occ = 1.0` inputs the static source-derived expected native Probe selector is `ogt1` with a Probe-selected denominator, to be verified against actual executed `argv` and atom membership).
2. Standalone dictionary-explicit `reduce` electron-cloud H build (`scripts/standalone_reduce.py::build_hydrogens`) + `probe` (`scripts/bench_t05_clashscore_h.py::run_probe_clashscore`, line `112`: `"ogt33 not water"` / `"ogt33"` contact filter; lines `69–79`: `count_atoms` counts all non-water PDB `ATOM`/`HETATM` records without that `ogt33` occupancy filter). | Retain original and H-built PDB hashes, `reduce` executable and dictionary identities (`REDUCE_HET_DICT`), and per-producer actual Probe `argv`, selected atom keys, contact-pair definition, raw contact lines, and numerator and denominator populations. Preserve the standalone helper's historical native definition (`ogt33` contact filter over unfiltered non-water `count_atoms`) separately from a Probe-selected atom population rather than relabelling the two paths as matched; in `start` (`data/1sar.pdb`), `CYS B 72 SG` has occupancy `0.30`, making the `ogt1` vs `ogt33` occupancy mask directly relevant. | Both paths share MolProbity lineage and differ in both H-placement/flip policy and occupancy-mask/denominator policy (`phenix.clashscore` expected `ogt1` Probe-selected denominator vs standalone `ogt33 not water` / `ogt33` contacts divided by unfiltered non-water `count_atoms`). Matching H coordinates alone is insufficient to isolate clash counting; treat occupancy-mask/denominator differences and H-placement/flip choices separately, with no algorithm changes, no forced matched masks, no threshold changes, and no new grade. Estimated runtime: `5–15 min`. | +| **T10 / T06 density support and waters** | 1. Write a fresh single-complex-pair projection MTZ (`PHX_OBS_PAIR_ONLY.mtz`) containing only `H, K, L` and `2FOFCWT, PH2FOFCWT` from the subject's first-block `fmodel`, and run `phenix.real_space_correlation` in coefficient mode with explicit `map_coefficients_label=2FOFCWT,PH2FOFCWT`, `detail=residue`, `atom_radius=2.0`, `use_hydrogens=False`, `hydrogen_atom_radius=1.0`, `resolution_factor=0.25`, `scattering_table=n_gaussian` (verifying projection provenance and exact complex-array/HKL equality).
2. Independent CCP4 `EDSTATS` via the explicit subprocess `argv`/stdin pipeline (`mtzinfo` → `mtzfix FLABEL F-obs HKLIN ... HKLOUT fixed.mtz` → two `fft` runs for `FWT/PHWT` and `DELFWT/PHDELWT` with `XYZLIM ASU` and `GRID SAMPLE 4` → `edstats MAPIN1 fo.map MAPIN2 df.map PDBIN REFMAC_OUT.pdb OUT edstats.out QQDOUT edstats.qqd` with stdin `RESL=,RESH=,RESC=CHAIN,USEA=t`) under `toolchain.ccp4_environment()` on freshly normalized `REFMAC5 NCYC 0` maps, avoiding Perl wrapper shell interpolation. | Use full `(model, chain, resseq, icode, resname, altloc, atom_name)` keys; report protein, non-water hetero (`SO4`, `CA`, `NA`), and `HOH` water populations separately. Retain expected/returned/finite residue counts, atom counts per row, map coefficients, map grid/extrema, limiting radius/mask conventions, RSCC (`CCSa` in EDSTATS), and raw `RSZO`/`RSZD-`/`RSZD+`/`RSZD` tables. Starting-model water `n = 0` means water RSCC is unavailable (never `0.0`). | Native PHENIX (`Fc`, fixed `2.0 Å` atom spheres) and EDSTATS (`DFc` reconstructed as observed-minus-difference via `mtzfix`, adaptive `atomsf.lib` 95%-integral radii, `RESC=CHAIN`) use different maps and masks and are labelled `not_comparable_mask_and_map`. Single-path availability is an acceptable explicit gap. No fixed RSCC cutoff, no arbitrary RSCC outlier-fraction cutoff, no metal-identity verdict, and no web services. Estimated runtime: `10–30 min`. | +| **Inventory and B-factor context** | `iotbx`/`cctbx` and independent `gemmi` plus `scripts/assess_refinement.py::summarize_pdb_inventory` and `scripts/audit_1sar_retained_evidence.py`. | Exact protein (`1,488`), water (`0`, `146`, `145`, `151`), and hetero (`SO4`, `CA`, `NA`) atom keys, unweighted atom B distribution in `Ų` (`mean`, `SD`, `min`, `max`), per-water B, and global protein comparator explicitly labelled as global (not a local shell). | Deterministic coordinate parsing of identical input bytes; water occupancy or B-factor ratios do not prove chemical identity or correctness. Estimated runtime: `< 2 min`. | +| **T13 shared dataset** | `phenix.xtriage` and `scripts/t13_data_quality.py` (`aimless` → `ctruncate`). | Executed **once** for the shared original `data/1sar.mtz` dataset (`F-obs,SIGF-obs`); retain full logs, output MTZ, dataset SHA-256 digest, named columns, parsed diagnostics, and unavailable fields. | Merged amplitudes cannot supply unmerged `CC½`, `⟨I/σ⟩` outer, `R-merge`, `R-meas`, or `R-pim`; retain the expected `aimless` failure (`hkl_unmerge_list::prepare - EMPTY`) and mark unmerged metrics unavailable without substitution. Estimated runtime: `5–15 min`. | +| **Difference peaks / optional original-menu observables** | `phenix.find_peaks_holes` / `mmtbx.find_peaks.manager` on the exact first-block `mFo-DFc` difference map (`map_cutoff = ±4.0`, `min_cross_distance = 1.8 Å`, `1.8–3.2 Å` model shell); independent `gemmi` map peak inventory only if a documented matched local path is available. | Fresh per-subject difference maps, explicit `±4.0σ` threshold (overriding installed `3.0σ` default), exclusion/merge radii, pre-filter and post-filter peak counts, and all peak coordinates, heights, and nearest-atom distances. | Because Gemmi's `find_blobs_by_flood_fill` uses 6-neighbor flood-fill components without PHENIX's interpolated `1.8 Å` cluster algorithm, retain a single-producer PHENIX peak inventory and document the independence gap unless a matched local path is established. No Round-7 comparison and no chemical/metal identity inferred from nearest atom. Estimated runtime: `4–12 min`. | + +--- + +## 4. Explicit Methodological Protocols and Non-Comparability Rules + +### 4.1 PHENIX First-Block `model_vs_data` Public API Adapter and HKL Ledgers + +High-level `mmtbx.model_vs_data.run()` reads `REMARK 3` resolution limits from the input PDB header and may rebuild and rescale a second `fmodel` before returning, and its CLI exposes neither an ignore-header switch nor an explicit free-test-value parameter. To guarantee that the first-block calculation, exported `fmodel` arrays, and map coefficients all describe the exact same untouched PDB input and full MTZ range: + +1. Construct the reflection-file server from original `data/1sar.mtz`, extract `extract_xtal_data.data_and_flags_master_params().extract()`, set observation labels `F-obs,SIGF-obs`, flag label `R-free-flags`, and `parameters.r_free_flags.test_flag_value = 1`, require flags, and disable flag generation. +2. Call `extract_xtal_data.run(...)` and fail closed if any extraction errors accumulate (`iotbx/extract_xtal_data.py` accumulates errors under `keep_going=True` in the vendor CLI). Require a non-empty retained flag array with disjoint, non-empty work and free sets (`1 = test`) so `mmtbx.f_model`'s all-work fallback can never masquerade as `R-free`. +3. Construct `iotbx.pdb.input` and `mmtbx.model.manager` from the original PDB bytes using the default `n_gaussian` scattering table, build `mmtbx.f_model.manager(xray_structure=..., f_obs=..., r_free_flags=..., twin_law=None)`, call `update_all_scales(update_f_part1=True)`, and snapshot scatterer coordinates, occupancies, and ADPs before and after scaling (`apply_back_trace=False`). +4. Because `cctbx.miller` regularization and `update_all_scales` (`remove_outliers=True`, low-resolution bin filtering, `apply_scale_k1_to_f_obs=True`) can filter and rescale observations in a model-dependent way, export four separate HKL ledgers: + - original raw MTZ rows, + - repository input-policy eligible rows (finite `F`, positive finite `SIGF`, valid `{0, 1}` flag), + - extracted pre-scaling observations, and + - final effective work and free sets (`fmodel.f_obs_work()`, `fmodel.f_obs_free()`), preserving original `F-obs/SIGF-obs` alongside transformed `f_obs`. +5. Export full-precision `fmodel.r_work()` and `fmodel.r_free()`, complex model arrays `f_model_scaled_with_k1_w()` and `f_model_scaled_with_k1_t()`, per-set counts, numerator `Σ|F_obs − |F_model_scaled||`, denominator `Σ F_obs`, resolution extrema, and separate work/free scale factors `scale_k1_w()` and `scale_k1_t()`. Because PHENIX fits `scale_k1_t()` on the free set, a single globally scaled `Fc` array cannot replay both native R values; retain the exact per-set scaled arrays and validate direct arithmetic replay against `fmodel.r_work()` and `fmodel.r_free()`. +6. Export `2mFo-DFc` and `mFo-DFc` coefficients from the same first-block `fmodel` (`fill_missing=False`, `exclude_free_r_reflections=False`, `isotropize=True`), recording their HKL sets separately from the R-factor denominator ledger. Never infer observed R-factor HKL membership from map coefficient arrays. + +### 4.2 REFMAC5 Fixed-Model Protocol and Fail-Closed Effective-Selection Audit + +1. Invoke `REFMAC5` via `scripts/toolchain.py::run_refmac` with explicit stdin keywords: + ```text + LABIN FP=F-obs SIGFP=SIGF-obs FREE=R-free-flags + FREE 1 + REFI BREF MIXED + NCYC 0 + END + ``` + plus explicit `LABOUT` labels for calculated structure factors and map coefficients (`FWT`, `PHWT`, `DELFWT`, `PHDELWT`). `FREE 1` is mandatory because REFMAC defaults to `FREE 0`; `REFI BREF MIXED` preserves the input PDB's isotropic-only ADP membership (whereas `BREF ANIS` converts every atom to anisotropic); and `NCYC 0` performs zero coordinate cycles. +2. Retain the complete keyword file, stdout/stderr logs, `XMLOUT` (if produced), output MTZ, and output PDB. Verify that output coordinates and occupancies match the input PDB, and audit any overall-B shift written to output ADPs. Never pass REFMAC's output PDB forward as the untouched input model, and only pass it as `PDBIN` to the explicit `edstats` subprocess pipeline (`mtzinfo` → `mtzfix` → `fft` → `edstats`) after proving exact atom mapping and unchanged coordinates/occupancies while disclosing any overall-B offset. +3. **Fail-closed effective-selection and reconstruction reconciliation:** In REFMAC output MTZs, `FP/SIGFP` are rescaled, output `FC` omits `FPART` bulk-solvent contributions, and missing-`FP` rows can still receive finite `FWT = dFC` and `DELFWT = 0`. Raw output row counts or finite map-coefficient counts therefore never establish effective R-factor denominators. Before accepting a REFMAC measurement, reconcile all six checks: + - one-to-one input/output HKL mapping with unchanged symmetry and indexing, + - preserved original free-flag membership (`1 = test`), + - no newly finite observations or unexplained missing observations, + - final observed work and free cardinalities equal to the summed final log-bin `NR_used` and `NR_free`, + - per-bin membership/counts and resolution extrema consistent with log precision, and + - R-work and R-free reconstructed from the appropriate total scaled model-amplitude output agreeing with native printed R values at stated log precision (reporting native printed and MTZ-reconstructed values distinctly). + If any check fails to reconcile, retain all raw outputs, record `effective_selection_unproven`, and do **not** publish a matched or inferred denominator. + +### 4.3 Non-Comparability and Threshold Governance + +- Compare exact effective work and free HKL sets across `PHENIX` and `REFMAC5` and across `start`, `R4`, `R5`, and `R6`. Even within `PHENIX`, model-based outlier rejection can make `start` and `R4` use slightly different effective HKL sets. +- Whenever native effective selections, map definitions, or mask radii differ, label the pair `non-comparable` (`not_comparable_mask_and_map` for native PHENIX vs EDSTATS density). Never average across tools or force equal denominators by disabling vendor outlier rejection on the primary native path (any common-intersection calculation is a separate descriptive sensitivity check, not a native refit). +- Do **not** transfer the registry §3 `gemmi sfcalc` direct-summation `R-work` offset band (`|ΔR_work| ≤ 0.02`, `ref/thresholds_and_standards.md`) to the `PHENIX` vs `REFMAC5` comparison. No registry threshold is modified and no pass/fail model-quality verdict is issued. + +--- + +## 5. Single-Family `R4` Native R-Factor Canary and Contract-4 Partial QDS + +Before running the multi-subject or multi-family batch, execute and validate a **single-family canary** restricted to packaged `R4` (`data/1sar_final.pdb`) and `data/1sar.mtz` across the two native R-factor producers (`phenix.model_vs_data` first-block adapter and `REFMAC5` `NCYC 0` / `FREE 1` / `REFI BREF MIXED`) using the exact same subject/family runner and cache adapters that the batch will invoke. No extra retention-family or third-producer (`gemmi`) measurement is included in the canary. + +1. **Build and Cache Admission (`#957`):** + - Resolve `PHENIX` via `scripts/refinement_cache.py::cached_phenix_operation` and `scripts/toolchain.py` with **no `PROTSTRUCT_*` overrides**, recording complete vendor-setup build evidence from `scripts/toolchain.py::phenix_build_evidence` together with helper and installed vendor-source hashes. An unknown or mismatched PHENIX build identity fails canary acceptance. + - Resolve `REFMAC5` via `scripts/toolchain.py::run_refmac` and `scripts/toolchain.py::ccp4_environment`, recording separate CCP4 executable and environment evidence; never mislabel REFMAC's build identity as PHENIX to reuse the PHENIX-specific cache gate. +2. **Six-Row Informational `EVAL` and Contract-4 Partial `QDS`:** + - From the verified canary execution manifest, author a canonical date-matched `EVAL_1sar_cdba2c07_.yaml` containing **six informational `T03` rows** (`T03_r-work`, `T03_r-free`, and `T03_r-free_r-work_gap` for `phenix.model_vs_data` (`cctbx`) and `REFMAC5` (`non_cctbx`)), each with `subject_ref: "artifact:cdba2c07-daff-4f60-ae96-12452b3a5fbb#data/1sar_final.pdb"`, `stage: final`, `scope: complex`, `pass_status: informational`, actual effective `n_work` and `n_free` in `TypedMeasurementValue.count` on the R-work and R-free rows (and both component counts recorded in the manifest for the gap row), and coherent `bundle_ref` and `evidence_refs`. + - Because `ref/structural_criteria.yaml` currently has `pass_criterion_bindings: []`, omit all criterion fields (`pass_criterion`, `pass_criterion_ref`, `criterion_preconditions`) and never assign `criterion_inapplicable` without an admitted binding. + - Select all issue-time applicable `tool_recommendations` (by `metric_definition_ref`) and `assumptions` (by `tool_ref`) plus their full supersession ancestry via `scripts/qds_emit.py::_active_registry_rows`, include all referenced canonical `Tool` rows from `ref/catalog.yaml` (`phenix.model_vs_data`, `REFMAC5`, `gemmi sfcalc`, `Servalcat`), emit the partial `QDS_1sar_cdba2c07_.yaml` (`coverage_scope: partial`) via `scripts/qds_emit.py::emit_qds` / `scripts/qds_emit_contract_v4.py::prepare_projection`, and attach the exact `QdsReplayPin` computed with `scripts/check_qds_trust_invariant.py::_canonical_qds_text` and `_snapshot_digest`. +3. **On-Disk Canary Acceptance Gate:** + - Canary acceptance requires verified input/output SHA-256 digests, recorded `argv` and REFMAC stdin keywords, measured build evidence, complete raw logs and exported HKL/model arrays, proven effective HKL selections and denominators (any `effective_selection_unproven` blocks acceptance), exact numerical R replay, `protstruct_review.models.Container.model_validate`, LinkML schema validation, `scripts/check_referential_integrity.py`, `scripts/check_pass_status.py`, `scripts/check_qds_trust_invariant.py`, and a green run of `uv run --locked --extra benchmark -- bash scripts/validate.sh`. + - If the canary fails any check, preserve the failed attempt on disk, fix the runner/adapter, and repeat **only** this single-family canary until accepted and committed before starting batch execution. + +--- + +## 6. Historically Informed Directional Predictions and Exploratory Questions + +Because `1SAR` has been examined in prior repository audits (`data/coscientists/openscientist/EVAL_1sar_cdba2c07_2026-09-07.md`, `EVAL_1sar_cdba2c07_2026-09-23.md`), `P1`, `P2`, and `P4` are **historically informed reproduction predictions** (directional expectations, not point intervals and not blind discovery), and `E3` is an **exploratory question**: + +- **P1 (Trajectory R-free reduction relative to actual start):** Native `R-free` on packaged `R4` (`data/1sar_final.pdb`), retained `R5` (`data/1sar_round5.pdb`), and retained `R6` (`data/1sar_round6.pdb`) will be lower than native `R-free` on the actual starting model (`data/1sar.pdb`) under both the `PHENIX` first-block adapter and `REFMAC5` (`NCYC 0`, `FREE 1`, `REFI BREF MIXED`). +- **P2 (Late-round R-free ordering `R6 < R5`):** Native `R-free` on retained `R6` (`data/1sar_round6.pdb`) will be lower than native `R-free` on retained `R5` (`data/1sar_round5.pdb`) under both implementations. +- **E3 (Exploratory question — restraint-library and geometry non-uniformity across rounds and libraries):** How do covalent bond/angle RMSD and RMSZ behave across `start → R4 → R5 → R6` and across `phenix.model_statistics` (`pdb_interpretation.restraints_library.cdl=True` vs `pdb_interpretation.restraints_library.cdl=False`) and `gemmi rmsz` (`CCP4 monomer library`), given their differing restraint targets, sigmas, and restrained-primitive denominators? +- **P4 (Exact agreement of deterministic coordinate inventory, B-factor, and fixed-key Cα RMSD implementations):** For identical coordinate bytes and identical 192-residue `(chain, resseq, icode, resname, CA, altloc)` key intersections, `iotbx`/`cctbx`, `gemmi`, and `scripts/assess_refinement.py` (`NumPy` Kabsch and fixed-column inventory) will agree on atom/water/hetero counts, unweighted B-factor distributions, and least-squares Cα RMSDs to reported numerical precision. Any divergence is treated as an implementation or selection bug to diagnose, never averaged away. + +Any prediction or exploratory question that is falsified by the executed data, or rendered unanswerable by an unproven effective selection (`effective_selection_unproven`), tool failure, mismatched map/mask convention (`not_comparable_mask_and_map`), or single-lineage coverage gap, remains recorded as such without post-hoc modification. + +--- + +## 7. Sample Size, Inference Scope, Limitations, and Runtime Budget + +1. **Sample Size and Inference Scope (`N = 1` Trajectory):** + - The four coordinate subjects (`start`, `R4`, `R5`, `R6`) are serial steps of one refinement trajectory on a single crystal structure and reflection file. They do **not** constitute four independent proteins or a statistical cohort. + - Accordingly, this one-trajectory design **does not support population, causal, or threshold-calibration inference** (nor `p`-values or bootstrap pseudo-replication across correlated residues or reflections) for `ref/thresholds_and_standards.md`. Uncertainty reporting is strictly limited to observed cross-implementation and selection sensitivity on these retained files. +2. **Conditions Leaving Questions Unanswered:** + - Unretained Round-7 coordinates (`claude_iterations.log:510, :752`) cannot be measured or adjudicated. + - The deposition-relative `T03` `R-free` clause remains unevaluable because `data/pdb_mtz/1sar_deposited.pdb` has `FREE R VALUE : NULL`. + - Unmerged-intensity `T13` statistics (`CC½`, `⟨I/σ⟩` outer, `R-merge`, `R-meas`, `R-pim`) remain unavailable because `data/1sar.mtz` contains merged amplitudes only. + - Chemical identity and occupancy of `CA A 98` and `NA B 98` cannot be established from `RSCC` or B-factors without anomalous data or coordinate/occupancy refinement. + - Missing independent local oracles (`TM-align`, non-`cctbx` Top8000 Ramachandran/rotamer classifiers, matched-radius independent peak/density counters) or an `effective_selection_unproven` outcome leave the corresponding cross-tool comparison unanswered. +3. **Runtime Planning Estimates and Execution Discipline:** + - Initial planning estimate: **2–15 minutes** for the single-family `R4` `T03` canary and **30–120 minutes** total sequential local scientific runtime for the full four-subject measurement matrix (excluding code authoring, QC, and review). + - All tool invocations run sequentially with bounded per-operation timeouts; measured canary wall-clock times will replace these planning estimates before batch execution begins. + +--- + +## 8. Record Architecture, `#1003` Scoping, and Two-PR Delivery Order + +1. **Canonical `EVAL` and `QDS` Organization:** + - Trajectory measurements use exact subject stages: `start` (`data/1sar.pdb`), `final` (`data/1sar_final.pdb`), and `intermediate` (`data/1sar_round5.pdb`, `data/1sar_round6.pdb`), while shared dataset diagnostics (`T13`) use `stage: all`, `scope: dataset`, and `subject_ref: mtz:sha256:f36d5fe685a3e524e8809d17d19ce12073acdf339eba2cfd3b0347a818850ab2`. + - Only the `R4` `T03` native R-factor canary emits a Contract-4 partial `QDS`. Supplemental `start`/`R5`/`R6` trajectory runs and additional task families may be committed as standalone canonical `EVAL_*.yaml` carriers without a QDS (`scripts/check_referential_integrity.py::check_corpus_refs` requires a Contract-4 context only when `corrections[]` are present). Stage labels and emitter scalar priorities are never altered to force non-final models or competing same-date estimators into a QDS scalar summary. + - Every new measurement row is `pass_status: informational` with no criterion fields. All committed historical `EVAL_*` and `QDS_*` files remain byte-for-byte unchanged. +2. **Scoping of `#1003` and Related Follow-Ups:** + - Issue `#1003` is scoped to the false Round-7 `"restored B:97:SO4"` narrative in the generator (`scripts/assess_refinement.py:1651`) and the old retained JSON (`ref/research/data/refinement_assessment_1sar_and_controls_2026-10-18.json`) — to be addressed in future generator and new dated assessment output updates without modifying frozen historical records — whereas the earlier case-study R5/R6 prose in `ref/research/refinement_assessment_case_study_2026-10-18.md` is already corrected on deck PR `#998` under `#1002`. Issues `#964` and `#987` remain separate follow-ups unless a bounded change is required by the runner. +3. **Two-PR Dependency and Merge Sequence:** + - The `1SAR` remeasurement analysis and retained evidence ship in a dedicated analysis PR from `main` (`analysis/1sar-rerun-2026-11-06`). + - Slide-deck PR `#998` (`2c8d8c6657a85fe4049123d659be7f9ae018a85e`) remains open. Once accepted analysis records are committed on the analysis branch, the analysis branch is merged normally into the deck branch (no force-push) and PR `#998` is retargeted to the analysis branch so its diff contains only the deck package and reads committed source records rather than orphan SHA pins. + - Upon later explicit user approval to merge: merge the analysis PR to `main` first, retarget deck PR `#998` to `main` before deleting its base branch, integrate `main` and pass fresh CI checks, and then merge the deck PR. + +--- + +## 9. Pre-Execution Review Log (No Results) + +Pre-registration draft review items resolved prior to any scientific tool execution or registration commit (no new scientific results): + +- **`#1004`:** Removed false identical-byte alias claims (`data/1sar_round1_input.pdb`, `data/1sar_round5_input.pdb`) from the §2 subject table; retained only verified archive/member digests. +- **`#1005`:** Removed unverified catalog/schema metric ID parentheticals from §3 (retaining only the three verified `T03` canary IDs `T03_r-work`, `T03_r-free`, and `T03_r-free_r-work_gap`) and verified repository function, path, and CLI argument references (`toolchain.gemmi_executable`, `toolchain.TMALIGN`, `pdb_interpretation.restraints_library.cdl=True/False`, `scripts/standalone_reduce.py::build_hydrogens`). +- **`#1006`:** Distinguished the clashscore occupancy-mask and denominator policies in §3 between installed `phenix.clashscore` (`mmtbx/validation/clashscore.py:180–187, 395–398, 415–424`, static source-derived expected `ogt1` Probe selector and Probe-selected denominator for `max_occ = 1.0` inputs) and standalone `scripts/bench_t05_clashscore_h.py` (line `112`: `"ogt33 not water"` / `"ogt33"` contact filter; lines `69–79`: unfiltered non-water `count_atoms` denominator; relevant to `start` `CYS B 72 SG` occupancy `0.30`), requiring per-producer retention of actual Probe `argv`, selected atom keys, contact-pair definitions, raw contact lines, and numerator/denominator populations without conflating occupancy-mask/denominator differences with H-placement/flip choices. diff --git a/scripts/onesar_phenix_measure.py b/scripts/onesar_phenix_measure.py new file mode 100644 index 0000000..a8afed6 --- /dev/null +++ b/scripts/onesar_phenix_measure.py @@ -0,0 +1,1795 @@ +#!/usr/bin/env python3 +"""Vendor Python 3.9 helper for PHENIX 2.0-5936 first-block R-factor and map export. + +This script runs under ``phenix.python`` via :mod:`scripts.onesar_rerun`. +All ``cctbx`` / ``iotbx`` / ``mmtbx`` imports are deferred inside +:func:`measure_first_block_rfactors` so the module can be imported and its pure +math/snapshot helpers unit-tested under the repository Python 3.12 environment +without vendor libraries installed. + +Scientific contract (Task 082 / 079 preregistration / 078 native design): +- Uses the public first-block ``mmtbx.model_vs_data`` sequence: + - ``iotbx.pdb.input`` on the untouched model PDB. + - ``iotbx.reflection_file_reader.any_reflection_file`` on the projected + 6-column observation MTZ. + - ``cctbx.crystal.select_crystal_symmetry`` with ``enforce_similarity=True``. + - ``mmtbx.model_vs_data.reflection_file_server``. + - ``iotbx.extract_xtal_data.data_and_flags_master_params().extract()`` with + ``labels=["F-obs", "SIGF-obs"]``, ``r_free_flags.label="R-free-flags"``, + ``r_free_flags.test_flag_value=1``, ``r_free_flags.required=True``, + ``r_free_flags.generate=False``, ``r_free_flags.ignore_r_free_flags=False``, + and ``keep_going=False``. + - ``mmtbx.model.manager`` + ``setup_scattering_dictionaries(scattering_table="n_gaussian", d_min=f_obs.d_min())``. + - ``mmtbx.f_model.manager(xray_structure=..., r_free_flags=..., f_obs=..., twin_law=None)`` + followed by ``fmodel.update_all_scales(update_f_part1=True)``. +- Does NOT run the second ``mmtbx.model_vs_data`` block (lines 176-225) that + re-applies PDB ``REMARK 3`` resolution cutoffs or rebuilds ``fmodel``. +- Exports full-precision extracted pre-``fmodel``, final effective, actual work, + and actual free arrays, work/free-specific scaled model structure factors + (``f_model_scaled_with_k1_w`` and ``f_model_scaled_with_k1_t``), scatterer + invariance snapshots, and fresh ``2mFo-DFc`` / ``mFo-DFc`` complex map + coefficients alongside float32 MTZ supplements. +""" + +from __future__ import annotations + +import argparse +import copy +import errno +import hashlib +import json +import math +import os +import stat +import struct +import sys +from io import StringIO +from pathlib import Path +from typing import Any, Mapping, Sequence + + +EXPECTED_PARAMETERS = { + "algorithm": "mmtbx.model_vs_data.first_block_public_api", + "labels": ["F-obs", "SIGF-obs"], + "r_free_flags_label": "R-free-flags", + "test_flag_value": 1, + "r_free_flags_generate": False, + "ignore_r_free_flags": False, + "keep_going": False, + "scattering_table": "n_gaussian", + "twin_law": None, + "update_f_part1": True, + "apply_pdb_header_resolution_cut": False, +} + +EXPECTED_MAP_PARAMETERS = { + "acentrics_scale": 2.0, + "centrics_pre_scale": 1.0, + "exclude_free_r_reflections": False, + "fill_missing": False, + "isotropize": True, + "sharp": False, + "phases_prefix": "PH", +} + + +def _sha256_file_py39(path: Path) -> str: + st = os.lstat(path) + if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode): + raise ValueError(f"Regular non-symlink file required: {path}") + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def validate_parameters_contract(params: Mapping[str, Any]) -> None: + """Verify that the operation parameter JSON matches the preregistered contract.""" + if not isinstance(params, Mapping): + raise ValueError("Operation parameters must be a JSON object") + for key, expected in EXPECTED_PARAMETERS.items(): + actual = params.get(key) + if actual != expected or type(actual) is not type(expected): + raise ValueError( + f"Parameter {key!r} mismatch: expected {expected!r}, got {actual!r}" + ) + expected_prefix = params.get("expected_resolved_prefix") + if ( + not isinstance(expected_prefix, str) + or not expected_prefix + or not Path(expected_prefix).is_absolute() + ): + raise ValueError( + "Operation parameters must include an absolute " + f"expected_resolved_prefix; got {expected_prefix!r}" + ) + expected_version = params.get("expected_phenix_version") + if not isinstance(expected_version, str) or not expected_version: + raise ValueError( + "Operation parameters must include a non-empty " + f"expected_phenix_version; got {expected_version!r}" + ) + map_params = params.get("map_coefficients") + if not isinstance(map_params, Mapping): + raise ValueError("Operation parameters missing map_coefficients object") + for key, expected in EXPECTED_MAP_PARAMETERS.items(): + actual = map_params.get(key) + if actual != expected or type(actual) is not type(expected): + raise ValueError( + f"map_coefficients.{key} mismatch: expected {expected!r}, got {actual!r}" + ) + + +def compute_rfactor_from_arrays( + f_obs: Sequence[float], + f_model_scaled_amplitudes: Sequence[float], +) -> dict[str, float]: + """Compute exact double-precision crystallographic R-factor numerator, denominator, and ratio.""" + if len(f_obs) != len(f_model_scaled_amplitudes): + raise ValueError( + f"Length mismatch in R-factor arrays: {len(f_obs)} vs " + f"{len(f_model_scaled_amplitudes)}" + ) + if not f_obs: + raise ValueError("Cannot compute R-factor on empty reflection array") + numerator = 0.0 + denominator = 0.0 + for idx, (fo, fm) in enumerate(zip(f_obs, f_model_scaled_amplitudes)): + fo_val = float(fo) + fm_val = float(fm) + if not math.isfinite(fo_val) or not math.isfinite(fm_val): + raise ValueError(f"Non-finite amplitude at index {idx}: fo={fo}, fm={fm}") + if fo_val <= 0.0 or fm_val < 0.0: + raise ValueError( + f"Invalid amplitude sign at index {idx}: fo={fo_val}, fm={fm_val}" + ) + numerator += abs(fo_val - fm_val) + denominator += fo_val + if denominator <= 0.0: + raise ValueError(f"Non-positive R-factor denominator: {denominator}") + return { + "numerator": numerator, + "denominator": denominator, + "r_factor": numerator / denominator, + } + + +def build_raw_scatterer_snapshot( + records: Sequence[Mapping[str, Any]], +) -> dict[str, Any]: + """Capture raw scatterer count and records without finite/non-empty digest validation.""" + raw_records: list[dict[str, Any]] = [] + for idx, rec in enumerate(records): + raw_records.append( + { + "index": idx, + "label": str(rec["label"]), + "scattering_type": str(rec["scattering_type"]), + "site_frac": [float(x) for x in rec["site_frac"]], + "site_cart": [float(x) for x in rec["site_cart"]], + "occupancy": float(rec["occupancy"]), + "u_iso": float(rec["u_iso"]), + "u_star": [float(x) for x in rec["u_star"]], + "use_u_iso": bool(rec["use_u_iso"]), + "use_u_aniso": bool(rec["use_u_aniso"]), + } + ) + return { + "n_scatterers": len(raw_records), + "sha256": None, + "records": raw_records, + } + + +def snapshot_scatterer_records( + records: Sequence[Mapping[str, Any]], +) -> dict[str, Any]: + """Compute a canonical digest and normalized representation of scatterer state.""" + if not records: + raise ValueError("Scatterer snapshot cannot be empty") + normalized: list[dict[str, Any]] = [] + for idx, rec in enumerate(records): + site_frac = [float(x) for x in rec["site_frac"]] + site_cart = [float(x) for x in rec["site_cart"]] + u_star = [float(x) for x in rec["u_star"]] + occ = float(rec["occupancy"]) + u_iso = float(rec["u_iso"]) + if ( + len(site_frac) != 3 + or len(site_cart) != 3 + or len(u_star) != 6 + or not all(math.isfinite(v) for v in site_frac + site_cart + u_star) + or not math.isfinite(occ) + or not math.isfinite(u_iso) + ): + raise ValueError(f"Invalid scatterer record at index {idx}: {rec!r}") + normalized.append( + { + "index": idx, + "label": str(rec["label"]), + "scattering_type": str(rec["scattering_type"]), + "site_frac": site_frac, + "site_cart": site_cart, + "occupancy": occ, + "u_iso": u_iso, + "u_star": u_star, + "use_u_iso": bool(rec["use_u_iso"]), + "use_u_aniso": bool(rec["use_u_aniso"]), + } + ) + canonical_bytes = json.dumps( + normalized, sort_keys=True, separators=(",", ":") + ).encode("utf-8") + return { + "n_scatterers": len(normalized), + "sha256": hashlib.sha256(canonical_bytes).hexdigest(), + "records": normalized, + } + + +def compare_scatterer_snapshots( + before: Mapping[str, Any], + after_model: Mapping[str, Any], + after_fmodel: Mapping[str, Any], +) -> dict[str, Any]: + """Compare scatterer snapshots before and after scaling and fail on any mutation.""" + before_recs = before.get("records") + after_model_recs = after_model.get("records") + after_fmodel_recs = after_fmodel.get("records") + if ( + not isinstance(before_recs, list) + or not isinstance(after_model_recs, list) + or not isinstance(after_fmodel_recs, list) + or not before_recs + or len(before_recs) != len(after_model_recs) + or len(before_recs) != len(after_fmodel_recs) + ): + raise ValueError("Scatterer count changed across f_model scaling") + + recomputed_before = snapshot_scatterer_records(before_recs) + recomputed_after_model = snapshot_scatterer_records(after_model_recs) + recomputed_after_fmodel = snapshot_scatterer_records(after_fmodel_recs) + for stage_name, raw_snap, recomputed_snap in ( + ("before", before, recomputed_before), + ("after_model", after_model, recomputed_after_model), + ("after_fmodel", after_fmodel, recomputed_after_fmodel), + ): + if raw_snap.get("n_scatterers") != recomputed_snap["n_scatterers"]: + raise ValueError( + f"{stage_name} snapshot n_scatterers mismatch with its records" + ) + if raw_snap.get("sha256") != recomputed_snap["sha256"]: + raise ValueError( + f"{stage_name} snapshot sha256 mismatch with its records" + ) + + max_site_cart_diff = 0.0 + max_occ_diff = 0.0 + max_u_iso_diff = 0.0 + max_u_star_diff = 0.0 + + for idx, base in enumerate(recomputed_before["records"]): + for other_label, other in ( + ("after_model", recomputed_after_model["records"][idx]), + ("after_fmodel", recomputed_after_fmodel["records"][idx]), + ): + if ( + base["label"] != other["label"] + or base["scattering_type"] != other["scattering_type"] + or base["use_u_iso"] != other["use_u_iso"] + or base["use_u_aniso"] != other["use_u_aniso"] + ): + raise ValueError( + f"Scatterer metadata mutated at index {idx} ({other_label}): " + f"{base!r} vs {other!r}" + ) + for a, b in zip(base["site_cart"], other["site_cart"]): + max_site_cart_diff = max(max_site_cart_diff, abs(float(a) - float(b))) + max_occ_diff = max( + max_occ_diff, abs(float(base["occupancy"]) - float(other["occupancy"])) + ) + max_u_iso_diff = max( + max_u_iso_diff, abs(float(base["u_iso"]) - float(other["u_iso"])) + ) + for a, b in zip(base["u_star"], other["u_star"]): + max_u_star_diff = max(max_u_star_diff, abs(float(a) - float(b))) + + summary = { + "n_scatterers": len(before_recs), + "before_sha256": recomputed_before["sha256"], + "after_model_sha256": recomputed_after_model["sha256"], + "after_fmodel_sha256": recomputed_after_fmodel["sha256"], + "identical_before_vs_after_model": ( + recomputed_before["sha256"] == recomputed_after_model["sha256"] + ), + "identical_before_vs_after_fmodel": ( + recomputed_before["sha256"] == recomputed_after_fmodel["sha256"] + ), + "max_abs_site_cart_diff": max_site_cart_diff, + "max_abs_occupancy_diff": max_occ_diff, + "max_abs_u_iso_diff": max_u_iso_diff, + "max_abs_u_star_diff": max_u_star_diff, + "snapshots": { + "before": recomputed_before, + "after_model": recomputed_after_model, + "after_fmodel": recomputed_after_fmodel, + }, + } + if ( + summary["before_sha256"] != summary["after_model_sha256"] + or summary["before_sha256"] != summary["after_fmodel_sha256"] + or max_site_cart_diff != 0.0 + or max_occ_diff != 0.0 + or max_u_iso_diff != 0.0 + or max_u_star_diff != 0.0 + ): + raise ValueError(f"Scatterer snapshot mutated during scaling: {summary}") + return summary + + +def _capture_xrs_snapshot( + xrs: Any, + *, + on_raw_capture: Any = None, +) -> dict[str, Any]: + sites_cart = list(xrs.sites_cart()) + scatterers = list(xrs.scatterers()) + if len(sites_cart) != len(scatterers): + raise ValueError("xray_structure sites_cart and scatterers size mismatch") + records: list[dict[str, Any]] = [] + for idx, sc in enumerate(scatterers): + records.append( + { + "label": str(sc.label), + "scattering_type": str(sc.scattering_type), + "site_frac": [float(v) for v in sc.site], + "site_cart": [float(v) for v in sites_cart[idx]], + "occupancy": float(sc.occupancy), + "u_iso": float(sc.u_iso), + "u_star": [float(v) for v in sc.u_star], + "use_u_iso": bool(sc.flags.use_u_iso()), + "use_u_aniso": bool(sc.flags.use_u_aniso()), + } + ) + if on_raw_capture is not None: + on_raw_capture(build_raw_scatterer_snapshot(records)) + return snapshot_scatterer_records(records) + + +def _extract_runtime_crystal_symmetry_record( + crystal_symmetry: Any, + *, + label: str = "crystal_symmetry", +) -> dict[str, Any]: + """Extract unit cell, space-group number, and XYZ symmetry operations from cctbx.crystal.symmetry.""" + if ( + crystal_symmetry is None + or crystal_symmetry.unit_cell() is None + or crystal_symmetry.space_group_info() is None + ): + raise RuntimeError(f"{label} is missing complete crystal_symmetry") + unit_cell_obj = crystal_symmetry.unit_cell() + sgi = crystal_symmetry.space_group_info() + sg_fn = getattr(crystal_symmetry, "space_group", None) + sg = sg_fn() if callable(sg_fn) else None + sgi_group_fn = getattr(sgi, "group", None) + sgi_group = sgi_group_fn() if callable(sgi_group_fn) else None + if sg is None: + sg = sgi_group + if sg is None: + raise RuntimeError(f"{label} is missing space_group()") + + sg_type_fn = getattr(sg, "type", None) + sg_type = sg_type_fn() if callable(sg_type_fn) else None + sg_num_fn = getattr(sg_type, "number", None) if sg_type is not None else None + if not callable(sg_num_fn): + raise RuntimeError( + f"{label}.space_group().type().number() is unavailable" + ) + raw_sg_number = sg_num_fn() + if ( + type(raw_sg_number) is bool + or not isinstance(raw_sg_number, int) + or not (1 <= int(raw_sg_number) <= 230) + ): + raise RuntimeError( + f"{label} space_group().type().number() must be an integer in 1..230, " + f"got {raw_sg_number!r}" + ) + space_group_number = int(raw_sg_number) + + sgi_type_fn = getattr(sgi, "type", None) + sgi_type = sgi_type_fn() if callable(sgi_type_fn) else None + if sgi_type is not None: + sgi_num_fn = getattr(sgi_type, "number", None) + if not callable(sgi_num_fn): + raise RuntimeError( + f"{label}.space_group_info().type().number() is unavailable" + ) + sgi_num = sgi_num_fn() + if ( + type(sgi_num) is bool + or not isinstance(sgi_num, int) + or int(sgi_num) != space_group_number + ): + raise RuntimeError( + f"{label} space_group_info().type().number() {sgi_num!r} " + f"disagrees with space_group().type().number() {space_group_number!r}" + ) + + all_ops_fn = getattr(sg, "all_ops", None) + if not callable(all_ops_fn): + raise RuntimeError(f"{label}.space_group().all_ops() is unavailable") + raw_ops = list(all_ops_fn()) + if not raw_ops: + raise RuntimeError( + f"{label}.space_group().all_ops() returned no symmetry operations" + ) + operators_xyz: list[str] = [] + for op_idx, op in enumerate(raw_ops): + as_xyz_fn = getattr(op, "as_xyz", None) + if not callable(as_xyz_fn): + raise RuntimeError( + f"{label}.space_group().all_ops()[{op_idx}] is missing as_xyz()" + ) + op_xyz = as_xyz_fn() + if not isinstance(op_xyz, str) or not op_xyz.strip(): + raise RuntimeError( + f"{label}.space_group().all_ops()[{op_idx}].as_xyz() returned " + f"invalid operator string {op_xyz!r}" + ) + operators_xyz.append(str(op_xyz)) + + if sgi_group is not None and sgi_group is not sg: + sgi_all_ops_fn = getattr(sgi_group, "all_ops", None) + if not callable(sgi_all_ops_fn): + raise RuntimeError( + f"{label}.space_group_info().group().all_ops() is unavailable" + ) + sgi_ops_xyz = [str(op.as_xyz()) for op in list(sgi_all_ops_fn())] + if sgi_ops_xyz != operators_xyz: + raise RuntimeError( + f"{label} space_group_info().group().all_ops() {sgi_ops_xyz!r} " + f"disagrees with space_group().all_ops() {operators_xyz!r}" + ) + + space_group_symbol = str(sgi.symbol_and_number()) + if not space_group_symbol.strip(): + raise RuntimeError( + f"{label}.space_group_info().symbol_and_number() returned empty string" + ) + return { + "unit_cell": [float(x) for x in unit_cell_obj.parameters()], + "space_group_symbol": space_group_symbol, + "space_group_number": space_group_number, + "space_group_operators_xyz": operators_xyz, + "unit_cell_precision_note": ( + "Effective runtime unit cell selected by cctbx.crystal.select_crystal_symmetry " + "from reflection Miller arrays (CCP4/iotbx float32 CELL/DCELL promoted to float64)." + ), + } + + +def _inspect_vendor_modules( + module_map: Mapping[str, Any], + *, + expected_resolved_prefix: str | Path | None = None, +) -> dict[str, Any]: + resolved_prefix = Path(sys.prefix).resolve(strict=True) + resolved_exec = Path(sys.executable).resolve(strict=True) + if not resolved_exec.is_relative_to(resolved_prefix): + raise ValueError( + f"sys.executable {resolved_exec} escapes sys.prefix {resolved_prefix}" + ) + expected_prefix_path: Path | None = None + if expected_resolved_prefix is not None: + expected_prefix_path = Path(expected_resolved_prefix).resolve(strict=True) + if not resolved_prefix.is_relative_to(expected_prefix_path): + raise ValueError( + f"Runtime resolved_sys_prefix {resolved_prefix} does not belong " + f"to expected_resolved_prefix {expected_prefix_path}" + ) + if not resolved_exec.is_relative_to(expected_prefix_path): + raise ValueError( + f"Runtime resolved_sys_executable {resolved_exec} escapes " + f"expected_resolved_prefix {expected_prefix_path}" + ) + vendor_modules: dict[str, dict[str, str]] = {} + for key, mod in module_map.items(): + mod_file = getattr(mod, "__file__", None) + if not isinstance(mod_file, str) or not mod_file: + raise ValueError(f"Loaded module {key} has no __file__ attribute") + mod_path = Path(mod_file).resolve(strict=True) + if not mod_path.is_relative_to(resolved_prefix): + raise ValueError( + f"Loaded module {key} at {mod_path} escapes sys.prefix {resolved_prefix}" + ) + if expected_prefix_path is not None and not mod_path.is_relative_to( + expected_prefix_path + ): + raise ValueError( + f"Loaded module {key} at {mod_path} escapes " + f"expected_resolved_prefix {expected_prefix_path}" + ) + vendor_modules[key] = { + "module_name": str(getattr(mod, "__name__", key)), + "configured_path": mod_file, + "resolved_path": str(mod_path), + "sha256": _sha256_file_py39(mod_path), + } + return { + "python_version": sys.version, + "sys_executable": sys.executable, + "resolved_sys_executable": str(resolved_exec), + "sys_prefix": sys.prefix, + "resolved_sys_prefix": str(resolved_prefix), + "vendor_modules": vendor_modules, + } + + +def _export_map_records(miller_array: Any) -> dict[str, Any]: + indices = list(miller_array.indices()) + data = list(miller_array.data()) + centric_flags = list(miller_array.centric_flags().data()) + if len(indices) != len(data) or len(indices) != len(centric_flags): + raise ValueError("Map coefficient array index/data/centric_flags length mismatch") + records: list[dict[str, Any]] = [] + centric_count = 0 + acentric_count = 0 + for hkl, coeff, is_centric in zip(indices, data, centric_flags): + re_val = float(coeff.real) + im_val = float(coeff.imag) + amp_val = float(abs(coeff)) + phase_deg = float(math.degrees(math.atan2(im_val, re_val)) % 360.0) + centric_bool = bool(is_centric) + if centric_bool: + centric_count += 1 + else: + acentric_count += 1 + records.append( + { + "hkl": [int(hkl[0]), int(hkl[1]), int(hkl[2])], + "real": re_val, + "imag": im_val, + "amplitude": amp_val, + "phase_deg": phase_deg, + "is_centric": centric_bool, + } + ) + return { + "n_reflections": len(records), + "centric_count": centric_count, + "acentric_count": acentric_count, + "records": records, + } + + +def _emit_stage_log(stage_label: str, text: str) -> None: + sys.stdout.write(f"=== [onesar_phenix_measure] {stage_label} ===\n") + if text: + sys.stdout.write(text) + if not text.endswith("\n"): + sys.stdout.write("\n") + sys.stdout.flush() + + +def encode_raw_evidence_nonfinite( + value: Any, + *, + field_path: str = "", + nonfinite_locations: list[dict[str, Any]] | None = None, +) -> Any: + """Recursively replace non-finite float values with strict-JSON IEEE-754 tags. + + Ordinary finite floats, ints, booleans, strings, lists, and mappings are + returned unchanged so valid finite measurements have identical JSON + representation. Non-finite floats (NaN, +Inf, -Inf) are replaced in-place + at their exact field location with a tagged object recording the IEEE-754 + binary64 bit pattern (``0x...``), sign bit, kind (``nan``, ``+inf``, + ``-inf``), string representation, and ``field_path``. + """ + if type(value) is float and not math.isfinite(value): + bits = struct.unpack(">Q", struct.pack(">d", value))[0] + kind = ( + "nan" + if math.isnan(value) + else ("+inf" if value > 0.0 else "-inf") + ) + tag = { + "__nonfinite_ieee754_float64__": True, + "kind": kind, + "repr": str(value), + "ieee754_binary64_hex": f"0x{bits:016x}", + "sign_bit": int((bits >> 63) & 1), + "field_path": field_path, + } + if nonfinite_locations is not None: + nonfinite_locations.append(dict(tag)) + return tag + if isinstance(value, Mapping): + if value.get("__nonfinite_ieee754_float64__") is True: + tag_copy = dict(value) + if nonfinite_locations is not None: + nonfinite_locations.append(tag_copy) + return tag_copy + out_dict: dict[str, Any] = {} + for k, v in value.items(): + k_str = str(k) + child_path = f"{field_path}.{k_str}" if field_path else k_str + out_dict[k_str] = encode_raw_evidence_nonfinite( + v, + field_path=child_path, + nonfinite_locations=nonfinite_locations, + ) + return out_dict + if isinstance(value, (list, tuple)): + return [ + encode_raw_evidence_nonfinite( + item, + field_path=f"{field_path}[{idx}]", + nonfinite_locations=nonfinite_locations, + ) + for idx, item in enumerate(value) + ] + return value + + +def _prepare_partial_evidence_for_checkpoint( + partial_evidence: dict[str, Any], +) -> dict[str, Any]: + """Sanitize non-finite float values in partial_evidence in-place and update nonfinite_raw_values.""" + locations: list[dict[str, Any]] = [] + for key in list(partial_evidence.keys()): + if key == "nonfinite_raw_values": + continue + partial_evidence[key] = encode_raw_evidence_nonfinite( + partial_evidence[key], + field_path=key, + nonfinite_locations=locations, + ) + partial_evidence["nonfinite_raw_values"] = locations + return partial_evidence + + +def _open_checkpoint_temp_file(tmp_path: Path) -> Any: + """Open the temporary checkpoint sibling file for writing.""" + return tmp_path.open("w", encoding="utf-8") + + +def _atomic_write_json_checkpoint( + payload: Mapping[str, Any], + target_path: Path | None, + *, + allow_nonfinite_tags: bool = False, +) -> None: + """Atomically write a complete JSON checkpoint via a temporary sibling file.""" + if target_path is None: + return + to_serialize = ( + encode_raw_evidence_nonfinite(payload) + if allow_nonfinite_tags + else payload + ) + serialized = ( + json.dumps(to_serialize, indent=2, sort_keys=True, allow_nan=False) + + "\n" + ) + tmp_path = target_path.parent / f".{target_path.name}.tmp.{os.getpid()}" + try: + with _open_checkpoint_temp_file(tmp_path) as fh: + fh.write(serialized) + fh.flush() + try: + os.fsync(fh.fileno()) + except OSError as exc: + if exc.errno not in (errno.EINVAL, errno.ENOTSUP): + raise + os.replace(tmp_path, target_path) + except BaseException: + try: + tmp_path.unlink(missing_ok=True) + except OSError: + pass + raise + + +def _checkpoint_diagnostics( + diagnostics: Mapping[str, Any], + out_diagnostics_json_path: Path | None, +) -> None: + """Persist the current diagnostics dictionary to disk via atomic replacement.""" + _atomic_write_json_checkpoint( + diagnostics, out_diagnostics_json_path, allow_nonfinite_tags=True + ) + + +class _PersistentTeeLogStream: + """Writable log stream that flushes vendor log output live to stdout and diagnostics.""" + + def __init__( + self, + stage_label: str, + diagnostics: dict[str, Any], + diagnostics_field: str, + diagnostics_json_path: Path | None = None, + log_file_path: Path | None = None, + ) -> None: + self._stage_label = stage_label + self._diagnostics = diagnostics + self._diagnostics_field = diagnostics_field + self._diagnostics_json_path = diagnostics_json_path + self._log_file_path = log_file_path + self._buffer = StringIO() + self._banner_emitted = False + self._ends_with_newline = True + if self._log_file_path is not None: + with self._log_file_path.open("w", encoding="utf-8") as fh: + fh.write("") + fh.flush() + + def _emit_banner_if_needed(self) -> None: + if not self._banner_emitted: + sys.stdout.write( + f"=== [onesar_phenix_measure] {self._stage_label} ===\n" + ) + sys.stdout.flush() + self._banner_emitted = True + self._ends_with_newline = True + + def _sync_persistent(self, chunk: str) -> None: + if self._log_file_path is not None and chunk: + with self._log_file_path.open("a", encoding="utf-8") as fh: + fh.write(chunk) + fh.flush() + try: + os.fsync(fh.fileno()) + except OSError: + pass + self._diagnostics[self._diagnostics_field] = self._buffer.getvalue() + _checkpoint_diagnostics(self._diagnostics, self._diagnostics_json_path) + + def write(self, text: Any) -> int: + chunk = str(text) + if not chunk: + return 0 + self._buffer.write(chunk) + self._emit_banner_if_needed() + sys.stdout.write(chunk) + sys.stdout.flush() + self._ends_with_newline = chunk.endswith("\n") + self._sync_persistent(chunk) + return len(chunk) + + def writelines(self, lines: Any) -> None: + for line in lines: + self.write(line) + + def flush(self) -> None: + sys.stdout.flush() + self._sync_persistent("") + + def finalize(self) -> str: + self._emit_banner_if_needed() + if not self._ends_with_newline: + sys.stdout.write("\n") + sys.stdout.flush() + self._ends_with_newline = True + text = self._buffer.getvalue() + self._diagnostics[self._diagnostics_field] = text + _checkpoint_diagnostics(self._diagnostics, self._diagnostics_json_path) + return text + + def getvalue(self) -> str: + return self._buffer.getvalue() + + +def measure_first_block_rfactors( + *, + pdb_path: Path, + mtz_path: Path, + params_path: Path, + out_json_path: Path, + out_maps_mtz_path: Path, + out_obs_pair_mtz_path: Path, + out_diagnostics_json_path: Path | None = None, + out_partial_json_path: Path | None = None, +) -> dict[str, Any]: + """Execute the first-block ``mmtbx.model_vs_data`` measurement inside ``phenix.python``.""" + effective_partial_json_path = ( + out_partial_json_path + if out_partial_json_path is not None + else out_json_path.with_name( + f"{out_json_path.stem}_partial_evidence.json" + ) + ) + if out_json_path.exists(): + raise FileExistsError( + f"Refusing to overwrite existing output JSON: {out_json_path}" + ) + if effective_partial_json_path.exists(): + raise FileExistsError( + "Refusing to overwrite existing partial evidence JSON: " + f"{effective_partial_json_path}" + ) + if ( + out_diagnostics_json_path is not None + and out_diagnostics_json_path.exists() + ): + raise FileExistsError( + "Refusing to overwrite existing diagnostics JSON: " + f"{out_diagnostics_json_path}" + ) + + diagnostics: dict[str, Any] = { + "schema_version": 1, + "status": "running", + "current_phase": "parameter_validation", + "phase_history": ["parameter_validation"], + "adapter_identity": ( + "mmtbx.model_vs_data.first_block_public_api " + "(public Python API adapter; not the phenix.model_vs_data CLI banner)" + ), + "extract_xtal_data_log": None, + "extract_xtal_data_constructor_log_note": ( + "iotbx.extract_xtal_data.run owns an internal self.log StringIO buffer " + "that becomes accessible only after __init__ returns; if __init__ raises " + "before returning, only the exception is accessible." + ), + "model_manager_log": "", + "f_model_manager_log_note": ( + "mmtbx.f_model.manager.__init__ accepts no log parameter in " + "PHENIX 2.0-5936." + ), + "fmodel_update_all_scales_log": "", + "partial_evidence_json_path": str(effective_partial_json_path), + "partial_evidence_status": "running_partial_unverified", + "completed_raw_stages": [], + "failure": None, + } + partial_evidence: dict[str, Any] = { + "schema_version": 1, + "artifact_role": "partial_unverified_phenix_evidence", + "reusable_cache_output": False, + "canonical_eval_qds_eligible": False, + "algorithm": "mmtbx.model_vs_data.first_block_public_api", + "status": "running_partial_unverified", + "current_phase": "parameter_validation", + "phase_history": ["parameter_validation"], + "completed_stages": [], + "rfactor_replay_verified": False, + "map_export_verified": False, + "expected_resolved_prefix": None, + "expected_phenix_version": None, + "runtime_environment": None, + "crystal_symmetry": None, + "extraction": None, + "extracted_pre_fmodel": None, + "xray_structure_snapshots_raw": { + "before": None, + "after_model": None, + "after_fmodel": None, + "invariance_verified": False, + }, + "xray_structure_invariance": None, + "final_effective": None, + "actual_work": None, + "actual_free": None, + "resolution_ranges": None, + "scales_and_rfactors": None, + "rfactor_replay_comparison": None, + "map_coefficients": None, + "nonfinite_raw_values": [], + "failure": None, + } + current_phase = "parameter_validation" + + def _enter_phase(phase: str) -> None: + nonlocal current_phase + current_phase = phase + diagnostics["current_phase"] = phase + partial_evidence["current_phase"] = phase + if ( + not diagnostics["phase_history"] + or diagnostics["phase_history"][-1] != phase + ): + diagnostics["phase_history"].append(phase) + partial_evidence["phase_history"] = list(diagnostics["phase_history"]) + _checkpoint_diagnostics(diagnostics, out_diagnostics_json_path) + + def _checkpoint_partial_stage(stage_name: str | None = None) -> None: + if stage_name is not None and ( + not partial_evidence["completed_stages"] + or partial_evidence["completed_stages"][-1] != stage_name + ): + partial_evidence["completed_stages"].append(stage_name) + partial_evidence["current_phase"] = current_phase + partial_evidence["phase_history"] = list(diagnostics["phase_history"]) + diagnostics["partial_evidence_status"] = partial_evidence["status"] + diagnostics["completed_raw_stages"] = list( + partial_evidence["completed_stages"] + ) + _prepare_partial_evidence_for_checkpoint(partial_evidence) + _atomic_write_json_checkpoint( + partial_evidence, + effective_partial_json_path, + allow_nonfinite_tags=True, + ) + _checkpoint_diagnostics(diagnostics, out_diagnostics_json_path) + + _checkpoint_diagnostics(diagnostics, out_diagnostics_json_path) + try: + params = json.loads(params_path.read_text(encoding="utf-8")) + validate_parameters_contract(params) + expected_resolved_prefix = str( + Path(params["expected_resolved_prefix"]).resolve(strict=True) + ) + expected_phenix_version = str(params["expected_phenix_version"]) + + _enter_phase("vendor_imports") + # Deferred vendor imports (executed only inside phenix.python). + from cctbx import crystal + from iotbx import extract_xtal_data + import iotbx.mtz + import iotbx.pdb + from iotbx import reflection_file_reader + from iotbx import reflection_file_utils + import mmtbx.f_model.f_model as mmtbx_f_model_mod + import mmtbx.map_tools + import mmtbx.model + import mmtbx.model_vs_data + + _enter_phase("runtime_environment_inspection") + runtime_env = _inspect_vendor_modules( + { + "vendor_mmtbx_model_vs_data": mmtbx.model_vs_data, + "vendor_iotbx_extract_xtal_data": extract_xtal_data, + "vendor_iotbx_reflection_file_utils": reflection_file_utils, + "vendor_mmtbx_f_model": mmtbx_f_model_mod, + "vendor_mmtbx_map_tools": mmtbx.map_tools, + "vendor_iotbx_mtz": iotbx.mtz, + }, + expected_resolved_prefix=expected_resolved_prefix, + ) + + _enter_phase("pdb_input") + pdb_inp = iotbx.pdb.input(file_name=str(pdb_path)) + pdb_symmetry = pdb_inp.crystal_symmetry() + if ( + pdb_symmetry is None + or pdb_symmetry.unit_cell() is None + or pdb_symmetry.space_group_info() is None + ): + raise RuntimeError("Model PDB is missing complete crystal_symmetry") + _extract_runtime_crystal_symmetry_record( + pdb_symmetry, label="Model PDB crystal_symmetry" + ) + + _enter_phase("reflection_file_input") + reflection_file = reflection_file_reader.any_reflection_file( + file_name=str(mtz_path) + ) + reflection_arrays = reflection_file.as_miller_arrays() + if not reflection_arrays: + raise RuntimeError( + "No reflection Miller arrays available in MTZ for symmetry selection" + ) + reflection_symmetries = [] + ref_cell_and_sg = None + for arr in reflection_arrays: + arr_sym = arr.crystal_symmetry() + if ( + arr_sym is None + or arr_sym.unit_cell() is None + or arr_sym.space_group_info() is None + ): + raise RuntimeError( + "Reflection Miller array is missing complete crystal_symmetry" + ) + arr_sym_record = _extract_runtime_crystal_symmetry_record( + arr_sym, label="Reflection Miller array crystal_symmetry" + ) + key = ( + tuple(round(float(x), 6) for x in arr_sym_record["unit_cell"]), + arr_sym_record["space_group_symbol"], + arr_sym_record["space_group_number"], + tuple(arr_sym_record["space_group_operators_xyz"]), + ) + if ref_cell_and_sg is None: + ref_cell_and_sg = key + elif key != ref_cell_and_sg: + raise RuntimeError( + f"Ambiguous reflection crystal_symmetry across Miller arrays: " + f"{ref_cell_and_sg} vs {key}" + ) + reflection_symmetries.append(arr_sym) + + _enter_phase("crystal_symmetry_consensus") + crystal_symmetry = crystal.select_crystal_symmetry( + from_command_line=None, + from_parameter_file=None, + from_coordinate_files=[pdb_symmetry], + from_reflection_files=[reflection_symmetries[0]], + enforce_similarity=True, + absolute_angle_tolerance=1.0e-2, + absolute_length_tolerance=1.0e-2, + ) + if ( + crystal_symmetry is None + or crystal_symmetry.unit_cell() is None + or crystal_symmetry.space_group_info() is None + ): + raise RuntimeError("Failed to determine consensus crystal_symmetry") + crystal_symmetry_record = _extract_runtime_crystal_symmetry_record( + crystal_symmetry, label="Consensus crystal_symmetry" + ) + + _enter_phase("extract_xtal_data") + rfs = mmtbx.model_vs_data.reflection_file_server( + crystal_symmetry=crystal_symmetry, + reflection_files=[reflection_file], + ) + extract_params = extract_xtal_data.data_and_flags_master_params().extract() + extract_params.labels = ["F-obs", "SIGF-obs"] + extract_params.r_free_flags.label = "R-free-flags" + extract_params.r_free_flags.test_flag_value = 1 + extract_params.r_free_flags.generate = False + extract_params.r_free_flags.ignore_r_free_flags = False + + df = extract_xtal_data.run( + reflection_file_server=rfs, + parameters=extract_params, + keep_going=False, + ) + # Retain df.log immediately after construction before checking errors or later work. + extract_log_text = ( + df.log.getvalue() if getattr(df, "log", None) is not None else "" + ) + diagnostics["extract_xtal_data_log"] = extract_log_text + _emit_stage_log("extract_xtal_data.run (df.log)", extract_log_text) + _checkpoint_diagnostics(diagnostics, out_diagnostics_json_path) + + err_list = [str(e) for e in getattr(df, "err", [])] + if err_list: + raise RuntimeError(f"extract_xtal_data reported errors: {err_list}") + if df.f_obs is None or df.r_free_flags is None: + raise RuntimeError("extract_xtal_data did not return f_obs and r_free_flags") + if df.f_obs.sigmas() is None: + raise RuntimeError("Extracted f_obs is missing sigmas") + if int(df.test_flag_value) != 1: + raise RuntimeError( + f"Unexpected extracted test_flag_value: {df.test_flag_value!r}" + ) + if not df.f_obs.indices().all_eq(df.r_free_flags.indices()): + raise RuntimeError("Extracted f_obs and r_free_flags indices are not aligned") + + extracted_indices = list(df.f_obs.indices()) + extracted_f = list(df.f_obs.data()) + extracted_sigf = list(df.f_obs.sigmas()) + extracted_flags = [bool(flag) for flag in list(df.r_free_flags.data())] + if not any(extracted_flags) or all(extracted_flags): + raise RuntimeError( + "Extracted r_free_flags must contain non-empty work and free sets" + ) + extracted_rows: list[dict[str, Any]] = [] + for hkl, fo, sigfo, is_free in zip( + extracted_indices, extracted_f, extracted_sigf, extracted_flags + ): + extracted_rows.append( + { + "hkl": [int(hkl[0]), int(hkl[1]), int(hkl[2])], + "f_obs": float(fo), + "sigf_obs": float(sigfo), + "is_free": bool(is_free), + } + ) + + extraction_record = { + "labels": ["F-obs", "SIGF-obs"], + "r_free_flags_label": "R-free-flags", + "test_flag_value": int(df.test_flag_value), + "r_free_flags_generate": False, + "ignore_r_free_flags": False, + "keep_going": False, + "err": err_list, + "log_text": extract_log_text, + "applied_pdb_header_resolution_cut": False, + "raw_data_info": ( + str(df.raw_data.info()) if df.raw_data is not None else None + ), + "raw_flags_info": ( + str(df.raw_flags.info()) if df.raw_flags is not None else None + ), + } + partial_evidence["expected_resolved_prefix"] = expected_resolved_prefix + partial_evidence["expected_phenix_version"] = expected_phenix_version + partial_evidence["runtime_environment"] = copy.deepcopy(runtime_env) + partial_evidence["crystal_symmetry"] = copy.deepcopy( + crystal_symmetry_record + ) + partial_evidence["extraction"] = copy.deepcopy(extraction_record) + partial_evidence["extracted_pre_fmodel"] = copy.deepcopy(extracted_rows) + if hasattr(df.f_obs, "d_max_min"): + ext_dmax_pre, ext_dmin_pre = df.f_obs.d_max_min() + partial_evidence["resolution_ranges"] = { + "extracted_pre_fmodel": { + "d_max": float(ext_dmax_pre), + "d_min": float(ext_dmin_pre), + } + } + _checkpoint_partial_stage("extract_xtal_data") + + def _record_raw_xrs_snapshot( + stage_key: str, raw_snapshot: Mapping[str, Any] + ) -> None: + partial_evidence["xray_structure_snapshots_raw"][stage_key] = ( + copy.deepcopy(dict(raw_snapshot)) + ) + _checkpoint_partial_stage() + + _enter_phase("model_manager") + model_log = _PersistentTeeLogStream( + "mmtbx.model.manager", + diagnostics, + "model_manager_log", + diagnostics_json_path=out_diagnostics_json_path, + ) + try: + model = mmtbx.model.manager( + model_input=pdb_inp, + crystal_symmetry=crystal_symmetry, + log=model_log, + ) + model.setup_scattering_dictionaries( + scattering_table="n_gaussian", + d_min=df.f_obs.d_min(), + ) + finally: + model_log.finalize() + + before_xrs_snapshot = _capture_xrs_snapshot( + model.get_xray_structure(), + on_raw_capture=lambda raw_snap: _record_raw_xrs_snapshot( + "before", raw_snap + ), + ) + partial_evidence["xray_structure_snapshots_raw"]["before"] = ( + copy.deepcopy(before_xrs_snapshot) + ) + _checkpoint_partial_stage("xray_structure_snapshot_before") + + _enter_phase("fmodel_manager_init") + fmodel = mmtbx.f_model.manager( + xray_structure=model.get_xray_structure(), + r_free_flags=df.r_free_flags, + f_obs=df.f_obs, + twin_law=None, + ) + + _enter_phase("update_all_scales") + fmodel_log = _PersistentTeeLogStream( + "fmodel.update_all_scales", + diagnostics, + "fmodel_update_all_scales_log", + diagnostics_json_path=out_diagnostics_json_path, + ) + try: + fmodel.update_all_scales(update_f_part1=True, log=fmodel_log) + finally: + fmodel_log.finalize() + + after_model_xrs_snapshot = _capture_xrs_snapshot( + model.get_xray_structure(), + on_raw_capture=lambda raw_snap: _record_raw_xrs_snapshot( + "after_model", raw_snap + ), + ) + partial_evidence["xray_structure_snapshots_raw"]["after_model"] = ( + copy.deepcopy(after_model_xrs_snapshot) + ) + _checkpoint_partial_stage("xray_structure_snapshot_after_model") + + after_fmodel_xrs_snapshot = _capture_xrs_snapshot( + fmodel.xray_structure, + on_raw_capture=lambda raw_snap: _record_raw_xrs_snapshot( + "after_fmodel", raw_snap + ), + ) + partial_evidence["xray_structure_snapshots_raw"]["after_fmodel"] = ( + copy.deepcopy(after_fmodel_xrs_snapshot) + ) + _checkpoint_partial_stage("xray_structure_snapshot_after_fmodel") + + xrs_invariance = compare_scatterer_snapshots( + before_xrs_snapshot, + after_model_xrs_snapshot, + after_fmodel_xrs_snapshot, + ) + partial_evidence["xray_structure_snapshots_raw"][ + "invariance_verified" + ] = True + partial_evidence["xray_structure_invariance"] = copy.deepcopy( + xrs_invariance + ) + _checkpoint_partial_stage("xray_structure_invariance") + + _enter_phase("rfactor_replay") + final_f_obs_arr = fmodel.f_obs() + final_flags_arr = fmodel.r_free_flags() + if final_f_obs_arr.sigmas() is None: + raise RuntimeError("fmodel.f_obs() sigmas are None") + if not final_f_obs_arr.indices().all_eq(final_flags_arr.indices()): + raise RuntimeError( + "fmodel.f_obs() and fmodel.r_free_flags() indices disagree" + ) + + final_rows: list[dict[str, Any]] = [] + sigmas_by_hkl: dict[tuple[int, int, int], float] = {} + for hkl, fo, sigfo, is_free in zip( + list(final_f_obs_arr.indices()), + list(final_f_obs_arr.data()), + list(final_f_obs_arr.sigmas()), + list(final_flags_arr.data()), + ): + hkl_tuple = (int(hkl[0]), int(hkl[1]), int(hkl[2])) + sigmas_by_hkl[hkl_tuple] = float(sigfo) + final_rows.append( + { + "hkl": [hkl_tuple[0], hkl_tuple[1], hkl_tuple[2]], + "f_obs": float(fo), + "sigf_obs": float(sigfo), + "is_free": bool(is_free), + } + ) + partial_evidence["final_effective"] = copy.deepcopy(final_rows) + if hasattr(final_f_obs_arr, "d_max_min"): + fin_dmax_pre, fin_dmin_pre = final_f_obs_arr.d_max_min() + if not isinstance(partial_evidence.get("resolution_ranges"), dict): + partial_evidence["resolution_ranges"] = {} + partial_evidence["resolution_ranges"]["final_effective"] = { + "d_max": float(fin_dmax_pre), + "d_min": float(fin_dmin_pre), + } + _checkpoint_partial_stage("final_effective_raw") + + f_obs_work_arr = fmodel.f_obs_work() + f_model_work_arr = fmodel.f_model_work() + f_model_scaled_w_arr = fmodel.f_model_scaled_with_k1_w() + if not ( + f_obs_work_arr.indices().all_eq(f_model_work_arr.indices()) + and f_obs_work_arr.indices().all_eq(f_model_scaled_w_arr.indices()) + ): + raise RuntimeError("Work reflection array indices are not aligned") + + work_sigmas = ( + list(f_obs_work_arr.sigmas()) + if f_obs_work_arr.sigmas() is not None + else [ + sigmas_by_hkl[(int(h[0]), int(h[1]), int(h[2]))] + for h in list(f_obs_work_arr.indices()) + ] + ) + actual_work_rows: list[dict[str, Any]] = [] + for hkl, fo, sigfo, fm_raw, fm_scaled in zip( + list(f_obs_work_arr.indices()), + list(f_obs_work_arr.data()), + work_sigmas, + list(f_model_work_arr.data()), + list(f_model_scaled_w_arr.data()), + ): + actual_work_rows.append( + { + "hkl": [int(hkl[0]), int(hkl[1]), int(hkl[2])], + "f_obs": float(fo), + "sigf_obs": float(sigfo), + "f_model_real": float(fm_raw.real), + "f_model_imag": float(fm_raw.imag), + "f_model_amplitude": float(abs(fm_raw)), + "f_model_scaled_with_k1_w_real": float(fm_scaled.real), + "f_model_scaled_with_k1_w_imag": float(fm_scaled.imag), + "f_model_scaled_with_k1_w_amplitude": float(abs(fm_scaled)), + } + ) + partial_evidence["actual_work"] = copy.deepcopy(actual_work_rows) + if hasattr(f_obs_work_arr, "d_max_min"): + work_dmax_pre, work_dmin_pre = f_obs_work_arr.d_max_min() + if not isinstance(partial_evidence.get("resolution_ranges"), dict): + partial_evidence["resolution_ranges"] = {} + partial_evidence["resolution_ranges"]["actual_work"] = { + "d_max": float(work_dmax_pre), + "d_min": float(work_dmin_pre), + } + _checkpoint_partial_stage("actual_work_raw") + + f_obs_free_arr = fmodel.f_obs_free() + f_model_free_arr = fmodel.f_model_free() + f_model_scaled_t_arr = fmodel.f_model_scaled_with_k1_t() + if not ( + f_obs_free_arr.indices().all_eq(f_model_free_arr.indices()) + and f_obs_free_arr.indices().all_eq(f_model_scaled_t_arr.indices()) + ): + raise RuntimeError("Free reflection array indices are not aligned") + + free_sigmas = ( + list(f_obs_free_arr.sigmas()) + if f_obs_free_arr.sigmas() is not None + else [ + sigmas_by_hkl[(int(h[0]), int(h[1]), int(h[2]))] + for h in list(f_obs_free_arr.indices()) + ] + ) + actual_free_rows: list[dict[str, Any]] = [] + for hkl, fo, sigfo, fm_raw, fm_scaled in zip( + list(f_obs_free_arr.indices()), + list(f_obs_free_arr.data()), + free_sigmas, + list(f_model_free_arr.data()), + list(f_model_scaled_t_arr.data()), + ): + actual_free_rows.append( + { + "hkl": [int(hkl[0]), int(hkl[1]), int(hkl[2])], + "f_obs": float(fo), + "sigf_obs": float(sigfo), + "f_model_real": float(fm_raw.real), + "f_model_imag": float(fm_raw.imag), + "f_model_amplitude": float(abs(fm_raw)), + "f_model_scaled_with_k1_t_real": float(fm_scaled.real), + "f_model_scaled_with_k1_t_imag": float(fm_scaled.imag), + "f_model_scaled_with_k1_t_amplitude": float(abs(fm_scaled)), + } + ) + partial_evidence["actual_free"] = copy.deepcopy(actual_free_rows) + if hasattr(f_obs_free_arr, "d_max_min"): + free_dmax_pre, free_dmin_pre = f_obs_free_arr.d_max_min() + if not isinstance(partial_evidence.get("resolution_ranges"), dict): + partial_evidence["resolution_ranges"] = {} + partial_evidence["resolution_ranges"]["actual_free"] = { + "d_max": float(free_dmax_pre), + "d_min": float(free_dmin_pre), + } + _checkpoint_partial_stage("actual_free_raw") + + scale_k1_w_val = float(fmodel.scale_k1_w()) + scale_k1_t_val = float(fmodel.scale_k1_t()) + scale_k1_val = float(fmodel.scale_k1()) + + work_replay = compute_rfactor_from_arrays( + [r["f_obs"] for r in actual_work_rows], + [r["f_model_scaled_with_k1_w_amplitude"] for r in actual_work_rows], + ) + free_replay = compute_rfactor_from_arrays( + [r["f_obs"] for r in actual_free_rows], + [r["f_model_scaled_with_k1_t_amplitude"] for r in actual_free_rows], + ) + all_rows = actual_work_rows + actual_free_rows + all_replay = compute_rfactor_from_arrays( + [r["f_obs"] for r in all_rows], + [scale_k1_val * r["f_model_amplitude"] for r in all_rows], + ) + native_r_work = float(fmodel.r_work()) + native_r_free = float(fmodel.r_free()) + native_r_all = float(fmodel.r_all()) + + ext_dmax, ext_dmin = df.f_obs.d_max_min() + fin_dmax, fin_dmin = final_f_obs_arr.d_max_min() + work_dmax, work_dmin = f_obs_work_arr.d_max_min() + free_dmax, free_dmin = f_obs_free_arr.d_max_min() + resolution_ranges_record = { + "extracted_pre_fmodel": { + "d_max": float(ext_dmax), + "d_min": float(ext_dmin), + }, + "final_effective": { + "d_max": float(fin_dmax), + "d_min": float(fin_dmin), + }, + "actual_work": { + "d_max": float(work_dmax), + "d_min": float(work_dmin), + }, + "actual_free": { + "d_max": float(free_dmax), + "d_min": float(free_dmin), + }, + } + scales_and_rfactors_record = { + "r_work": native_r_work, + "r_free": native_r_free, + "r_all": native_r_all, + "r_free_r_work_gap": native_r_free - native_r_work, + "scale_k1_w": scale_k1_w_val, + "scale_k1_t": scale_k1_t_val, + "scale_k1": scale_k1_val, + "work_numerator": work_replay["numerator"], + "work_denominator": work_replay["denominator"], + "free_numerator": free_replay["numerator"], + "free_denominator": free_replay["denominator"], + "all_numerator": all_replay["numerator"], + "all_denominator": all_replay["denominator"], + "work_count": len(actual_work_rows), + "free_count": len(actual_free_rows), + "final_effective_count": len(final_rows), + "extracted_pre_fmodel_count": len(extracted_rows), + } + + # Checkpoint completed raw R-factor arrays and both native/replayed scalars + # BEFORE scaled-complex and native-vs-replayed R-factor assertions (#1014). + partial_evidence["final_effective"] = copy.deepcopy(final_rows) + partial_evidence["actual_work"] = copy.deepcopy(actual_work_rows) + partial_evidence["actual_free"] = copy.deepcopy(actual_free_rows) + partial_evidence["resolution_ranges"] = copy.deepcopy( + resolution_ranges_record + ) + partial_evidence["scales_and_rfactors"] = { + **scales_and_rfactors_record, + "replayed_r_work": work_replay["r_factor"], + "replayed_r_free": free_replay["r_factor"], + "replayed_r_all": all_replay["r_factor"], + "replayed_r_free_r_work_gap": ( + free_replay["r_factor"] - work_replay["r_factor"] + ), + } + partial_evidence["rfactor_replay_comparison"] = { + "native_r_work": native_r_work, + "replayed_r_work": work_replay["r_factor"], + "r_work_abs_diff": abs(native_r_work - work_replay["r_factor"]), + "native_r_free": native_r_free, + "replayed_r_free": free_replay["r_factor"], + "r_free_abs_diff": abs(native_r_free - free_replay["r_factor"]), + "native_r_all": native_r_all, + "replayed_r_all": all_replay["r_factor"], + "r_all_abs_diff": abs(native_r_all - all_replay["r_factor"]), + "work_numerator": work_replay["numerator"], + "work_denominator": work_replay["denominator"], + "free_numerator": free_replay["numerator"], + "free_denominator": free_replay["denominator"], + "all_numerator": all_replay["numerator"], + "all_denominator": all_replay["denominator"], + "scale_k1_w": scale_k1_w_val, + "scale_k1_t": scale_k1_t_val, + "scale_k1": scale_k1_val, + } + _checkpoint_partial_stage("rfactor_raw_arrays_and_scalars") + + for row in actual_work_rows: + if not math.isclose( + row["f_model_scaled_with_k1_w_real"], + scale_k1_w_val * row["f_model_real"], + rel_tol=1e-12, + abs_tol=1e-12, + ) or not math.isclose( + row["f_model_scaled_with_k1_w_imag"], + scale_k1_w_val * row["f_model_imag"], + rel_tol=1e-12, + abs_tol=1e-12, + ): + raise RuntimeError( + f"Work reflection {row['hkl']} scaled complex coefficient " + f"({row['f_model_scaled_with_k1_w_real']}, " + f"{row['f_model_scaled_with_k1_w_imag']}) disagrees with " + f"scale_k1_w * f_model_work" + ) + for row in actual_free_rows: + if not math.isclose( + row["f_model_scaled_with_k1_t_real"], + scale_k1_t_val * row["f_model_real"], + rel_tol=1e-12, + abs_tol=1e-12, + ) or not math.isclose( + row["f_model_scaled_with_k1_t_imag"], + scale_k1_t_val * row["f_model_imag"], + rel_tol=1e-12, + abs_tol=1e-12, + ): + raise RuntimeError( + f"Free reflection {row['hkl']} scaled complex coefficient " + f"({row['f_model_scaled_with_k1_t_real']}, " + f"{row['f_model_scaled_with_k1_t_imag']}) disagrees with " + f"scale_k1_t * f_model_free" + ) + + if not math.isclose( + work_replay["r_factor"], native_r_work, rel_tol=1e-12, abs_tol=1e-12 + ): + raise RuntimeError( + f"Native fmodel.r_work() {native_r_work} disagrees with array replay " + f"{work_replay['r_factor']}" + ) + if not math.isclose( + free_replay["r_factor"], native_r_free, rel_tol=1e-12, abs_tol=1e-12 + ): + raise RuntimeError( + f"Native fmodel.r_free() {native_r_free} disagrees with array replay " + f"{free_replay['r_factor']}" + ) + if not math.isclose( + all_replay["r_factor"], native_r_all, rel_tol=1e-12, abs_tol=1e-12 + ): + raise RuntimeError( + f"Native fmodel.r_all() {native_r_all} disagrees with array replay " + f"{all_replay['r_factor']}" + ) + + partial_evidence["rfactor_replay_verified"] = True + _checkpoint_partial_stage("rfactor_replay_verified") + + _enter_phase("map_export") + map_params_record = { + "acentrics_scale": 2.0, + "centrics_pre_scale": 1.0, + "exclude_free_r_reflections": False, + "fill_missing": False, + "isotropize": True, + "sharp": False, + } + coeff_semantics = ( + "cctbx_map_tools_combine_already_incorporates_centric_and_acentric_scales_" + "do_not_double_acentrics_again" + ) + # Fresh complex map coefficients from the same first-block fmodel. + mc_2mfo_dfc = fmodel.map_coefficients( + map_type="2mFo-DFc", + acentrics_scale=2.0, + centrics_pre_scale=1.0, + exclude_free_r_reflections=False, + fill_missing=False, + isotropize=True, + sharp=False, + ) + if mc_2mfo_dfc is None: + raise RuntimeError("fmodel.map_coefficients returned None") + map_2mfo_payload = _export_map_records(mc_2mfo_dfc) + partial_evidence["map_coefficients"] = { + "2mFo-DFc": { + "map_type": "2mFo-DFc", + "amplitude_label": "2FOFCWT", + "phase_label": "PH2FOFCWT", + "parameters": dict(map_params_record), + "coefficient_semantics": coeff_semantics, + **copy.deepcopy(map_2mfo_payload), + }, + "mFo-DFc": None, + "mtz_supplements": None, + } + _checkpoint_partial_stage("map_coefficients_2mfo_dfc_raw") + + mc_mfo_dfc = fmodel.map_coefficients( + map_type="mFo-DFc", + acentrics_scale=2.0, + centrics_pre_scale=1.0, + exclude_free_r_reflections=False, + fill_missing=False, + isotropize=True, + sharp=False, + ) + if mc_mfo_dfc is None: + raise RuntimeError("fmodel.map_coefficients returned None") + map_mfo_payload = _export_map_records(mc_mfo_dfc) + partial_evidence["map_coefficients"]["mFo-DFc"] = { + "map_type": "mFo-DFc", + "amplitude_label": "FOFCWT", + "phase_label": "PHFOFCWT", + "parameters": dict(map_params_record), + "coefficient_semantics": coeff_semantics, + **copy.deepcopy(map_mfo_payload), + } + _checkpoint_partial_stage("map_coefficients_raw") + + decorator = iotbx.mtz.label_decorator(phases_prefix="PH") + maps_dataset = mc_2mfo_dfc.as_mtz_dataset( + column_root_label="2FOFCWT", + label_decorator=decorator, + ) + maps_dataset.add_miller_array( + miller_array=mc_mfo_dfc, + column_root_label="FOFCWT", + label_decorator=decorator, + ) + maps_mtz_obj = maps_dataset.mtz_object() + maps_columns = [str(lbl) for lbl in maps_mtz_obj.column_labels()] + if maps_columns != [ + "H", + "K", + "L", + "2FOFCWT", + "PH2FOFCWT", + "FOFCWT", + "PHFOFCWT", + ]: + raise RuntimeError(f"Unexpected phx_maps.mtz column labels: {maps_columns}") + maps_mtz_obj.write(file_name=str(out_maps_mtz_path)) + + obs_pair_dataset = mc_2mfo_dfc.as_mtz_dataset( + column_root_label="2FOFCWT", + label_decorator=decorator, + ) + obs_pair_mtz_obj = obs_pair_dataset.mtz_object() + obs_pair_columns = [str(lbl) for lbl in obs_pair_mtz_obj.column_labels()] + if obs_pair_columns != ["H", "K", "L", "2FOFCWT", "PH2FOFCWT"]: + raise RuntimeError( + f"Unexpected PHX_OBS_PAIR_ONLY.mtz column labels: {obs_pair_columns}" + ) + obs_pair_mtz_obj.write(file_name=str(out_obs_pair_mtz_path)) + + maps_readback = iotbx.mtz.object(file_name=str(out_maps_mtz_path)) + maps_column_types = [str(col.type()) for col in maps_readback.columns()] + if [str(col.label()) for col in maps_readback.columns()] != maps_columns: + raise RuntimeError("Read-back phx_maps.mtz column labels mismatch") + if maps_column_types != ["H", "H", "H", "F", "P", "F", "P"]: + raise RuntimeError( + f"Read-back phx_maps.mtz column types mismatch: {maps_column_types}" + ) + if int(maps_readback.n_reflections()) != len(final_rows): + raise RuntimeError( + f"Read-back phx_maps.mtz reflection count {maps_readback.n_reflections()} " + f"!= final_effective {len(final_rows)}" + ) + + obs_pair_readback = iotbx.mtz.object(file_name=str(out_obs_pair_mtz_path)) + obs_pair_column_types = [ + str(col.type()) for col in obs_pair_readback.columns() + ] + if [str(col.label()) for col in obs_pair_readback.columns()] != obs_pair_columns: + raise RuntimeError("Read-back PHX_OBS_PAIR_ONLY.mtz column labels mismatch") + if obs_pair_column_types != ["H", "H", "H", "F", "P"]: + raise RuntimeError( + "Read-back PHX_OBS_PAIR_ONLY.mtz column types mismatch: " + f"{obs_pair_column_types}" + ) + if int(obs_pair_readback.n_reflections()) != len(final_rows): + raise RuntimeError( + "Read-back PHX_OBS_PAIR_ONLY.mtz reflection count " + f"{obs_pair_readback.n_reflections()} != final_effective {len(final_rows)}" + ) + + mtz_supplements_record = { + "precision_note": ( + "MTZ files store float32 amplitudes and phases as a supplement " + "for downstream CCP4/Gemmi FFT consumers; full-precision float64 " + "complex coefficients are retained in this JSON payload." + ), + "phx_maps_filename": out_maps_mtz_path.name, + "phx_maps_sha256": _sha256_file_py39(out_maps_mtz_path), + "phx_maps_columns": maps_columns, + "phx_maps_column_types": maps_column_types, + "obs_pair_only_filename": out_obs_pair_mtz_path.name, + "obs_pair_only_sha256": _sha256_file_py39(out_obs_pair_mtz_path), + "obs_pair_only_columns": obs_pair_columns, + "obs_pair_only_column_types": obs_pair_column_types, + } + partial_evidence["map_coefficients"]["mtz_supplements"] = copy.deepcopy( + mtz_supplements_record + ) + partial_evidence["map_export_verified"] = True + partial_evidence["status"] = "completed_superseded_by_verified_payload" + current_phase = "completed" + diagnostics["status"] = "succeeded" + diagnostics["current_phase"] = "completed" + diagnostics["phase_history"].append("completed") + _checkpoint_partial_stage("map_export_verified") + + payload: dict[str, Any] = { + "schema_version": 1, + "algorithm": "mmtbx.model_vs_data.first_block_public_api", + "expected_resolved_prefix": expected_resolved_prefix, + "expected_phenix_version": expected_phenix_version, + "runtime_environment": runtime_env, + "diagnostics": copy.deepcopy(diagnostics), + "crystal_symmetry": crystal_symmetry_record, + "extraction": extraction_record, + "resolution_ranges": resolution_ranges_record, + "xray_structure_invariance": xrs_invariance, + "scales_and_rfactors": scales_and_rfactors_record, + "extracted_pre_fmodel": extracted_rows, + "final_effective": final_rows, + "actual_work": actual_work_rows, + "actual_free": actual_free_rows, + "map_coefficients": { + "2mFo-DFc": { + "map_type": "2mFo-DFc", + "amplitude_label": "2FOFCWT", + "phase_label": "PH2FOFCWT", + "parameters": dict(map_params_record), + "coefficient_semantics": coeff_semantics, + **map_2mfo_payload, + }, + "mFo-DFc": { + "map_type": "mFo-DFc", + "amplitude_label": "FOFCWT", + "phase_label": "PHFOFCWT", + "parameters": dict(map_params_record), + "coefficient_semantics": coeff_semantics, + **map_mfo_payload, + }, + "mtz_supplements": mtz_supplements_record, + }, + } + + _atomic_write_json_checkpoint(payload, out_json_path) + return payload + except Exception as exc: + limitation = None + if ( + current_phase == "extract_xtal_data" + and diagnostics["extract_xtal_data_log"] is None + ): + limitation = ( + "extract_xtal_data.run raised before returning; internal " + "self.log StringIO on the unreturned instance is not accessible." + ) + elif current_phase == "fmodel_manager_init": + limitation = ( + "mmtbx.f_model.manager.__init__ accepts no log parameter " + "in PHENIX 2.0-5936; no internal constructor log stream exists." + ) + failure_record = { + "failed_phase": current_phase, + "exception_type": type(exc).__name__, + "exception_message": str(exc), + "constructor_log_limitation": limitation, + } + partial_evidence["status"] = "failed_partial_unverified" + partial_evidence["current_phase"] = current_phase + partial_evidence["phase_history"] = list(diagnostics["phase_history"]) + partial_evidence["failure"] = copy.deepcopy(failure_record) + diagnostics["status"] = "failed" + diagnostics["current_phase"] = current_phase + diagnostics["partial_evidence_status"] = partial_evidence["status"] + diagnostics["completed_raw_stages"] = list( + partial_evidence["completed_stages"] + ) + diagnostics["failure"] = failure_record + try: + _prepare_partial_evidence_for_checkpoint(partial_evidence) + _atomic_write_json_checkpoint( + partial_evidence, + effective_partial_json_path, + allow_nonfinite_tags=True, + ) + _checkpoint_diagnostics(diagnostics, out_diagnostics_json_path) + sys.stderr.write( + "=== [onesar_phenix_measure] FAILURE DIAGNOSTIC ===\n" + + json.dumps( + encode_raw_evidence_nonfinite(diagnostics["failure"]), + indent=2, + sort_keys=True, + allow_nan=False, + ) + + "\n" + ) + sys.stderr.flush() + except Exception as checkpoint_exc: + sys.stderr.write( + "=== [onesar_phenix_measure] CHECKPOINT SERIALIZATION WARNING ===\n" + f"{type(checkpoint_exc).__name__}: {checkpoint_exc}\n" + ) + sys.stderr.flush() + raise + + +def main(argv: Sequence[str] | None = None) -> int: + parser = argparse.ArgumentParser( + description="Run first-block PHENIX model_vs_data R-factor and map export." + ) + parser.add_argument("--pdb", required=True, type=Path) + parser.add_argument("--mtz", required=True, type=Path) + parser.add_argument("--params-json", required=True, type=Path) + parser.add_argument("--out-json", required=True, type=Path) + parser.add_argument("--out-diagnostics-json", default=None, type=Path) + parser.add_argument("--out-partial-json", default=None, type=Path) + parser.add_argument("--out-maps-mtz", required=True, type=Path) + parser.add_argument("--out-obs-pair-mtz", required=True, type=Path) + args = parser.parse_args(argv) + + measure_first_block_rfactors( + pdb_path=args.pdb.resolve(strict=True), + mtz_path=args.mtz.resolve(strict=True), + params_path=args.params_json.resolve(strict=True), + out_json_path=args.out_json, + out_maps_mtz_path=args.out_maps_mtz, + out_obs_pair_mtz_path=args.out_obs_pair_mtz, + out_diagnostics_json_path=args.out_diagnostics_json, + out_partial_json_path=args.out_partial_json, + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/onesar_refmac_log.py b/scripts/onesar_refmac_log.py new file mode 100644 index 0000000..1670c48 --- /dev/null +++ b/scripts/onesar_refmac_log.py @@ -0,0 +1,2225 @@ +#!/usr/bin/env python3 +"""Pure native REFMAC5 log parser for 1SAR verification. + +This module parses only what a REFMAC5 log text reports: +- Named CCP4 loggraph ``$TABLE:`` blocks by title, column headers, and four + ``$$`` delimiters, preserving raw tokens and 1-based line numbers. +- Every cycle-summary row from ``$TABLE: Rfactor analysis, stats vs cycle :``. +- Every paired detailed ``Overall R factor`` / ``Free R factor`` block, kept + strictly distinct from weighted R (``WR_used``, ``WR_free``, + ``Overall weighted R factor``, ``Free weighted R factor``), weighted R2 + (``Overall weighted R2 factor``, ``Free weighted R2 factor``), and FOM, plus + optional ``CGMAT cycle number`` markers and recognized optional correlation, + DPI, and ML-uncertainty fields when present. +- Per-bin ``NR_used`` / ``NR_free`` integer reflection counts and ``Rf_used`` / + ``Rf_free`` tokens from every resolution R-factor table (initial, + intermediate, and final cycles), and per-bin ``NREFall == NREFa + NREFc`` + counts from resolution FOM tables. +- The ``$TEXT:Result: $$ Final results $$`` summary R-factor and R-free pair. + +Reconciles duplicate reporting locations across detailed cycle blocks, the +cycle-summary table, and the final-results block by exact ``Decimal`` value +equality while preserving each location's original token and printed decimal +precision (recognizing that detailed ``Cycle 1`` corresponds to summary +``Ncyc 0`` and detailed ``Cycle N+1`` to summary ``Ncyc N``). Rejects +contradictions, malformed or truncated tables, nonfinite or overflow R tokens, +out-of-domain percentages, invalid counts, and ambiguous cycles. + +This parser reports only what the log text states. It never proves process +termination/returncode, effective HKL membership, cross-table free-percentage +denominator arithmetic, model preservation, residual reconstruction, or EVAL +admission. +""" + +from __future__ import annotations + +import math +import re +from decimal import Decimal, InvalidOperation +from typing import Any + + +RESOLUTION_RFACTOR_HEADER_TOKENS: tuple[str, ...] = ( + "M(4SSQ/LL)", + "NR_used", + "%_obs", + "M(Fo_used)", + "M(Fc_used)", + "Rf_used", + "WR_used", + "NR_free", + "M(Fo_free)", + "M(Fc_free)", + "Rf_free", + "WR_free", +) + +RESOLUTION_FOM_HEADER_TOKENS: tuple[str, ...] = ( + "<4SSQ/LL>", + "NREFa", + "FOMa", + "NREFc", + "FOMc", + "NREFall", + "FOMall", + "SigmaA_Fc1", +) + +CYCLE_SUMMARY_HEADER_TOKENS: tuple[str, ...] = ( + "Ncyc", + "Rfact", + "Rfree", + "FOM", + "-LL", + "-LLfree", + "rmsBOND", + "zBOND", + "rmsANGL", + "zANGL", + "rmsCHIRAL", +) + +_TABLE_START_RE = re.compile(r"^\s*\$TABLE\s*:(.*)$") +_TEXT_START_RE = re.compile(r"^\s*\$TEXT\s*:([^:]+):\s*\$\$(.*?)\$\$\s*$") +_SUMMARY_DIRECTIVE_RE = re.compile(r"^\s*\$SUMMARY\s*:[^:]+:\s*\$\$.*\$\$\s*$") +_TEXT_REF_DIRECTIVE_RE = re.compile(r"^\s*:TEXT:[^:]+:\s*\$\$\s*$") + +_RESLN_RFACTOR_TITLE_RE = re.compile( + r"^\s*Cycle\s+(\S+)\.\s*Rfactor analysis,\s*F distribution v(?:s\.?)?\s+resln\s*:?\s*$" +) +_RESLN_FOM_TITLE_RE = re.compile( + r"^\s*Cycle\s+(\S+)\.\s*Fom\(-acentric,\s*centric,\s*overall v(?:s\.?)?\s+resln\s*:?\s*$" +) +_CYCLE_SUMMARY_TITLE_RE = re.compile( + r"^\s*Rfactor analysis,\s*stats vs\.?\s+cycle\s*:?\s*$" +) +_GRAPH_CYCLE_RE = re.compile(r"(?:^|:)\s*Cycle\s+(\S+?)\.(?=\s|$)") +_GRAPH_VS_CYCLE_RE = re.compile(r"\bvs\.?\s+cycle\b", re.IGNORECASE) +_GRAPH_ANY_CYCLE_WORD_RE = re.compile(r"\bcycle\b", re.IGNORECASE) + +_NONNEGATIVE_INT_RE = re.compile(r"^[0-9]+$") +_R_TOKEN_RE = re.compile(r"^[0-9]+\.[0-9]+$") +_FINITE_FLOAT_TOKEN_RE = re.compile( + r"^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$" +) + +_BANNER_VERSION_RE = re.compile( + r"###\s*(CCP4\s+\S+)\s*:\s*Refmac_\S+\s+version\s+(\S+)\s*:" +) +_RUN_DATE_RE = re.compile(r"Run date:\s*(\d{1,2}/\s*\d{1,2}/\d{4})") +_DATA_NCYC_RE = re.compile(r"^\s*Data line---\s+NCYC\s+(\S+)(?:\s+(.*))?$") +_REFINEMENT_CYCLES_RE = re.compile(r"^\s*Refinement cycles\s*:\s*(\S+)(?:\s+(.*))?$") + + +def _parse_nonnegative_int_token(token: str, *, context: str, line_no: int) -> int: + """Validate and parse a non-negative integer token, rejecting decimals/negatives.""" + if not isinstance(token, str) or not _NONNEGATIVE_INT_RE.fullmatch(token): + raise ValueError( + f"Invalid non-negative integer count token {token!r} for {context} at line {line_no}" + ) + return int(token) + + +def _parse_positive_int_token(token: str, *, context: str, line_no: int) -> int: + """Validate and parse a strictly positive integer token (>= 1).""" + value = _parse_nonnegative_int_token(token, context=context, line_no=line_no) + if value <= 0: + raise ValueError( + f"Positive integer required for {context} at line {line_no}, got {token!r}" + ) + return value + + +def _parse_r_token( + token: str, *, context: str, line_no: int +) -> tuple[Decimal, float, int]: + """Validate a printed unweighted R token. + + R must be a finite, non-negative decimal token with explicit fractional + digits and no overflow/invalid markers. No arbitrary upper bound (such as 1 + or 99) is imposed on finite unweighted R values. + """ + if not isinstance(token, str) or not _R_TOKEN_RE.fullmatch(token): + raise ValueError( + f"Invalid or nonfinite/sentinel R token {token!r} for {context} at line {line_no}" + ) + try: + dec_value = Decimal(token) + except InvalidOperation as exc: + raise ValueError( + f"Unparseable R token {token!r} for {context} at line {line_no}" + ) from exc + if not dec_value.is_finite() or dec_value < Decimal("0"): + raise ValueError( + f"Non-finite or negative R token {token!r} for {context} at line {line_no}" + ) + float_value = float(dec_value) + if not math.isfinite(float_value) or float_value < 0.0: + raise ValueError( + f"Non-finite R value {token!r} for {context} at line {line_no}" + ) + decimal_places = len(token.partition(".")[2]) + return dec_value, float_value, decimal_places + + +def _parse_finite_float_token( + token: str, + *, + context: str, + line_no: int, + nonnegative: bool = False, +) -> float: + """Validate a finite numeric token, rejecting NaN, infinity, and '*' overflow tokens.""" + if not isinstance(token, str) or not token or "*" in token: + raise ValueError( + f"Invalid numeric token {token!r} for {context} at line {line_no}" + ) + lowered = token.lower() + if lowered in {"nan", "+nan", "-nan", "inf", "+inf", "-inf", "infinity", "+infinity", "-infinity"}: + raise ValueError( + f"Non-finite numeric token {token!r} for {context} at line {line_no}" + ) + if not _FINITE_FLOAT_TOKEN_RE.fullmatch(token): + raise ValueError( + f"Malformed numeric token {token!r} for {context} at line {line_no}" + ) + try: + dec_value = Decimal(token) + except InvalidOperation as exc: + raise ValueError( + f"Malformed numeric token {token!r} for {context} at line {line_no}" + ) from exc + if not dec_value.is_finite(): + raise ValueError( + f"Non-finite numeric token {token!r} for {context} at line {line_no}" + ) + if nonnegative and dec_value < Decimal("0"): + raise ValueError( + f"Negative numeric token {token!r} for {context} at line {line_no}" + ) + value = float(dec_value) + if not math.isfinite(value): + raise ValueError( + f"Overflow numeric token {token!r} for {context} at line {line_no}" + ) + return value + + +def _parse_percentage_token( + token: str, + *, + context: str, + line_no: int, +) -> tuple[Decimal, float, int]: + """Validate a finite percentage token in the closed physical domain [0, 100]. + + Returns ``(dec_value, float_value, effective_decimal_places)`` where + ``effective_decimal_places`` is the number of fractional decimal places + after applying any scientific-notation exponent (bounded below by 0), + preserving significant trailing zeros while retaining the raw token. + """ + value = _parse_finite_float_token( + token, + context=context, + line_no=line_no, + nonnegative=True, + ) + dec_value = Decimal(token) + if dec_value < Decimal("0") or dec_value > Decimal("100"): + raise ValueError( + f"Percentage token {token!r} out of [0, 100] domain for {context} at line {line_no}" + ) + exp = dec_value.as_tuple().exponent + if not isinstance(exp, int): + raise ValueError( + f"Non-finite percentage token {token!r} for {context} at line {line_no}" + ) + decimal_places = max(0, -exp) + return dec_value, value, decimal_places + + +def _looks_like_numeric_or_invalid_value_token(token: str) -> bool: + """Return True if a token parses as a Decimal/NaN/Inf or contains '*'.""" + if "*" in token: + return True + try: + Decimal(token) + return True + except InvalidOperation: + return False + + +def _parse_table_block( + lines: list[str], start_idx: int +) -> tuple[dict[str, Any], int]: + """Parse one ``$TABLE:`` block starting at 0-based ``start_idx``. + + Enforces the four-``$$`` CCP4 loggraph delimiter structure: + 1. ``header_start_line``: standalone ``$$`` line after ``$GRAPHS`` lines + 2. ``header_end_line``: ``$$`` closing the header tokens (at the end of the + last header line or on its own ``$$`` line) + 3. ``data_start_line``: standalone ``$$`` line immediately following + ``header_end_line`` + 4. ``data_end_line``: standalone ``$$`` line closing the data rows + """ + title_line_no = start_idx + 1 + title_line_text = lines[start_idx] + title_match = _TABLE_START_RE.match(title_line_text) + if not title_match: + raise ValueError(f"Expected $TABLE: at line {title_line_no}") + raw_title_body = title_match.group(1).strip() + if not raw_title_body or raw_title_body == ":" or "$$" in title_line_text: + raise ValueError( + f"Malformed $TABLE title at line {title_line_no}: {title_line_text!r}" + ) + + total_lines = len(lines) + idx = start_idx + 1 + graph_declaration_lines: list[dict[str, Any]] = [] + header_start_line: int | None = None + + # Phase 1: $GRAPHS declarations up to header_start_line ($$) + while idx < total_lines: + line_no = idx + 1 + raw_line = lines[idx] + stripped = raw_line.strip() + if raw_line.count("$$") > 1: + raise ValueError( + f"Multiple $$ delimiters on line {line_no} in $TABLE starting at line {title_line_no}" + ) + if stripped == "$$": + header_start_line = line_no + idx += 1 + break + if "$$" in raw_line: + raise ValueError( + f"Malformed header_start $$ delimiter at line {line_no} in $TABLE starting at line {title_line_no}" + ) + if stripped.startswith("$GRAPHS") or ( + graph_declaration_lines and stripped.startswith(":") + ): + graph_declaration_lines.append({"line": line_no, "text": raw_line}) + idx += 1 + continue + raise ValueError( + f"Unexpected line {line_no} before header_start $$ in $TABLE starting at line {title_line_no}: {raw_line!r}" + ) + + if header_start_line is None: + raise ValueError( + f"Unclosed $TABLE starting at line {title_line_no}: missing header_start $$" + ) + + # Phase 2: Header lines up to header_end_line ($$) + header_lines: list[dict[str, Any]] = [] + header_tokens: list[str] = [] + header_end_line: int | None = None + + while idx < total_lines: + line_no = idx + 1 + raw_line = lines[idx] + stripped = raw_line.strip() + if raw_line.count("$$") > 1: + raise ValueError( + f"Multiple $$ delimiters on header line {line_no} in $TABLE starting at line {title_line_no}" + ) + if stripped.startswith(("$TABLE", "$GRAPHS", "$TEXT", "$SUMMARY", ":TEXT:")) or stripped.startswith("----"): + raise ValueError( + f"Malformed $TABLE starting at line {title_line_no}: unexpected boundary at line {line_no} before header_end $$" + ) + if "$$" in raw_line: + before_dd, _, after_dd = raw_line.partition("$$") + if after_dd.strip(): + raise ValueError( + f"Trailing tokens after header_end $$ at line {line_no} in $TABLE starting at line {title_line_no}" + ) + if before_dd.strip(): + header_lines.append({"line": line_no, "text": raw_line}) + header_tokens.extend(before_dd.split()) + header_end_line = line_no + idx += 1 + break + if not stripped: + raise ValueError( + f"Blank line {line_no} inside header of $TABLE starting at line {title_line_no}" + ) + header_lines.append({"line": line_no, "text": raw_line}) + header_tokens.extend(stripped.split()) + idx += 1 + + if header_end_line is None: + raise ValueError( + f"Unclosed $TABLE header starting at line {title_line_no}: missing header_end $$" + ) + if not header_tokens: + raise ValueError( + f"Empty header tokens in $TABLE starting at line {title_line_no}" + ) + + # Phase 3: data_start_line must immediately follow header_end_line and be standalone $$ + if idx >= total_lines: + raise ValueError( + f"Truncated $TABLE starting at line {title_line_no}: missing data_start $$ after line {header_end_line}" + ) + data_start_line = idx + 1 + raw_data_start = lines[idx] + if raw_data_start.count("$$") != 1 or raw_data_start.strip() != "$$": + raise ValueError( + f"Expected standalone data_start $$ at line {data_start_line} in $TABLE starting at line {title_line_no}, got {raw_data_start!r}" + ) + idx += 1 + + # Phase 4: Data rows up to data_end_line (standalone $$) + data_rows: list[dict[str, Any]] = [] + data_end_line: int | None = None + expected_col_count = len(header_tokens) + + while idx < total_lines: + line_no = idx + 1 + raw_line = lines[idx] + stripped = raw_line.strip() + if raw_line.count("$$") > 1: + raise ValueError( + f"Multiple $$ delimiters on line {line_no} in $TABLE starting at line {title_line_no}" + ) + if stripped == "$$": + data_end_line = line_no + idx += 1 + break + if "$$" in raw_line: + raise ValueError( + f"Malformed data_end $$ delimiter at line {line_no} in $TABLE starting at line {title_line_no}" + ) + if stripped.startswith(("$TABLE", "$GRAPHS", "$TEXT", "$SUMMARY", ":TEXT:")) or stripped.startswith("----"): + raise ValueError( + f"Unclosed data block in $TABLE starting at line {title_line_no}: unexpected boundary at line {line_no} before data_end $$" + ) + if not stripped: + raise ValueError( + f"Blank line {line_no} inside data rows of $TABLE starting at line {title_line_no}" + ) + tokens = stripped.split() + if len(tokens) != expected_col_count: + raise ValueError( + f"Row width mismatch at line {line_no} in $TABLE starting at line {title_line_no}: " + f"expected {expected_col_count} columns ({header_tokens}), got {len(tokens)} tokens ({tokens})" + ) + data_rows.append({"line": line_no, "text": raw_line, "tokens": tokens}) + idx += 1 + + if data_end_line is None: + raise ValueError( + f"Unclosed $TABLE starting at line {title_line_no}: missing data_end $$" + ) + if not data_rows: + raise ValueError( + f"Empty data rows in $TABLE starting at line {title_line_no}" + ) + + table_block: dict[str, Any] = { + "title": {"line": title_line_no, "text": title_line_text}, + "title_body": raw_title_body, + "graph_declaration_lines": graph_declaration_lines, + "header_lines": header_lines, + "header_tokens": header_tokens, + "double_dollar_roles": { + "header_start_line": header_start_line, + "header_end_line": header_end_line, + "data_start_line": data_start_line, + "data_end_line": data_end_line, + }, + "data_line_range_inclusive": [ + data_rows[0]["line"], + data_rows[-1]["line"], + ], + "row_count": len(data_rows), + "column_count": expected_col_count, + "rows": data_rows, + } + return table_block, idx + + +def _validate_graph_declaration_cycles( + table_block: dict[str, Any], *, expected_cycle: int | None, context: str +) -> list[dict[str, Any]]: + """Validate explicit ``Cycle .`` labels on ``$GRAPHS`` and continuation lines.""" + title_line_no = table_block["title"]["line"] + graph_cycles: list[dict[str, Any]] = [] + for g_entry in table_block["graph_declaration_lines"]: + g_line = g_entry["line"] + g_text = g_entry["text"] + matches = list(_GRAPH_CYCLE_RE.finditer(g_text)) + residual = _GRAPH_CYCLE_RE.sub("", g_text) + residual = _GRAPH_VS_CYCLE_RE.sub("", residual) + if _GRAPH_ANY_CYCLE_WORD_RE.search(residual): + raise ValueError( + f"Malformed Cycle label in $GRAPHS declaration at line {g_line} " + f"for {context} (table at line {title_line_no}): {g_text!r}" + ) + for m in matches: + cyc_tok = m.group(1) + g_cycle = _parse_positive_int_token( + cyc_tok, + context=f"{context} $GRAPHS Cycle label (table at line {title_line_no})", + line_no=g_line, + ) + if expected_cycle is None: + raise ValueError( + f"Unexpected explicit Cycle {g_cycle} in $GRAPHS declaration at line {g_line} " + f"for {context} (table at line {title_line_no})" + ) + if g_cycle != expected_cycle: + raise ValueError( + f"Conflicting $GRAPHS Cycle {g_cycle} at line {g_line} vs {context} " + f"table title Cycle {expected_cycle} at line {title_line_no}" + ) + graph_cycles.append( + { + "line": g_line, + "token": cyc_tok, + "cycle_number": g_cycle, + } + ) + return graph_cycles + + +def _classify_and_validate_table(table_block: dict[str, Any]) -> str: + """Classify a parsed ``$TABLE:`` block by title and header tokens.""" + title_line_no = table_block["title"]["line"] + title_body = table_block["title_body"] + header_tokens = tuple(table_block["header_tokens"]) + + rfactor_title_match = _RESLN_RFACTOR_TITLE_RE.match(title_body) + fom_title_match = _RESLN_FOM_TITLE_RE.match(title_body) + summary_title_match = _CYCLE_SUMMARY_TITLE_RE.match(title_body) + + looks_like_resln_rfactor_title = ( + "rfactor analysis" in title_body.lower() + and "f distribution" in title_body.lower() + ) + looks_like_cycle_summary_title = ( + "rfactor analysis" in title_body.lower() + and "stats vs" in title_body.lower() + ) + + if ( + rfactor_title_match + or looks_like_resln_rfactor_title + or header_tokens == RESOLUTION_RFACTOR_HEADER_TOKENS + ): + if not rfactor_title_match: + raise ValueError( + f"Malformed resolution R-factor table title at line {title_line_no}: {title_body!r}" + ) + cycle_token = rfactor_title_match.group(1) + cycle_number = _parse_positive_int_token( + cycle_token, + context="resolution R-factor table cycle label", + line_no=title_line_no, + ) + if header_tokens != RESOLUTION_RFACTOR_HEADER_TOKENS: + raise ValueError( + f"Unexpected resolution R-factor table header at line {title_line_no}: " + f"expected {list(RESOLUTION_RFACTOR_HEADER_TOKENS)}, got {list(header_tokens)}" + ) + _validate_graph_declaration_cycles( + table_block, + expected_cycle=cycle_number, + context="resolution R-factor", + ) + table_block["table_kind"] = "resolution_rfactor" + table_block["cycle_number"] = cycle_number + return "resolution_rfactor" + + if ( + summary_title_match + or looks_like_cycle_summary_title + or header_tokens == CYCLE_SUMMARY_HEADER_TOKENS + ): + if not summary_title_match: + raise ValueError( + f"Malformed cycle-summary table title at line {title_line_no}: {title_body!r}" + ) + if header_tokens != CYCLE_SUMMARY_HEADER_TOKENS: + raise ValueError( + f"Unexpected cycle-summary table header at line {title_line_no}: " + f"expected {list(CYCLE_SUMMARY_HEADER_TOKENS)}, got {list(header_tokens)}" + ) + _validate_graph_declaration_cycles( + table_block, + expected_cycle=None, + context="cycle-summary", + ) + table_block["table_kind"] = "cycle_summary" + table_block["cycle_number"] = None + return "cycle_summary" + + looks_like_resln_fom_title = ( + "fom(" in title_body.lower() or "delphi" in title_body.lower() + ) + if ( + fom_title_match + or looks_like_resln_fom_title + or header_tokens == RESOLUTION_FOM_HEADER_TOKENS + ): + if not fom_title_match or header_tokens != RESOLUTION_FOM_HEADER_TOKENS: + raise ValueError( + f"Malformed FOM resolution table title/header at line {title_line_no}" + ) + cycle_token = fom_title_match.group(1) + cycle_number = _parse_positive_int_token( + cycle_token, + context="resolution FOM table cycle label", + line_no=title_line_no, + ) + _validate_graph_declaration_cycles( + table_block, + expected_cycle=cycle_number, + context="resolution FOM", + ) + prev_fom_s2: float | None = None + for row in table_block["rows"]: + row_line = row["line"] + ( + s2_tok, + nref_a_tok, + fom_a_tok, + nref_c_tok, + fom_c_tok, + nref_all_tok, + fom_all_tok, + sigmaa_tok, + ) = row["tokens"] + s2_val = _parse_finite_float_token( + s2_tok, + context="FOM table <4SSQ/LL>", + line_no=row_line, + nonnegative=True, + ) + if prev_fom_s2 is not None and s2_val <= prev_fom_s2: + raise ValueError( + f"Non-increasing FOM <4SSQ/LL> bin coordinate {s2_tok!r} at line {row_line}" + ) + prev_fom_s2 = s2_val + nref_a = _parse_nonnegative_int_token( + nref_a_tok, context="FOM table NREFa", line_no=row_line + ) + _parse_finite_float_token( + fom_a_tok, context="FOM table FOMa", line_no=row_line + ) + nref_c = _parse_nonnegative_int_token( + nref_c_tok, context="FOM table NREFc", line_no=row_line + ) + _parse_finite_float_token( + fom_c_tok, context="FOM table FOMc", line_no=row_line + ) + nref_all = _parse_nonnegative_int_token( + nref_all_tok, context="FOM table NREFall", line_no=row_line + ) + _parse_finite_float_token( + fom_all_tok, context="FOM table FOMall", line_no=row_line + ) + _parse_finite_float_token( + sigmaa_tok, context="FOM table SigmaA_Fc1", line_no=row_line + ) + if nref_all != nref_a + nref_c: + raise ValueError( + f"FOM table row NREFall ({nref_all}) != NREFa ({nref_a}) + NREFc ({nref_c}) " + f"at line {row_line}" + ) + table_block["table_kind"] = "resolution_fom" + table_block["cycle_number"] = cycle_number + return "resolution_fom" + + # Competing unrelated table: reject NaN/Inf/sentinel tokens, keep distinct from R tables + for row in table_block["rows"]: + row_line = row["line"] + for col_name, tok in zip(header_tokens, row["tokens"]): + _parse_finite_float_token( + tok, context=f"table {title_body!r} column {col_name}", line_no=row_line + ) + table_block["table_kind"] = "other" + table_block["cycle_number"] = None + return "other" + + +def _parse_resolution_rfactor_table( + table_block: dict[str, Any], *, phase: str, summary_ncyc: int +) -> dict[str, Any]: + """Validate and structure one per-bin resolution R-factor table.""" + bins: list[dict[str, Any]] = [] + compact_rows: list[list[Any]] = [] + nr_used_sum = 0 + nr_free_sum = 0 + rf_used_places: set[int] = set() + rf_free_places: set[int] = set() + prev_s2: float | None = None + + for ordinal, row in enumerate(table_block["rows"], start=1): + line_no = row["line"] + tokens = row["tokens"] + ( + m_4ssq_ll_tok, + nr_used_tok, + pct_obs_tok, + m_fo_used_tok, + m_fc_used_tok, + rf_used_tok, + wr_used_tok, + nr_free_tok, + m_fo_free_tok, + m_fc_free_tok, + rf_free_tok, + wr_free_tok, + ) = tokens + + m_4ssq_ll = _parse_finite_float_token( + m_4ssq_ll_tok, + context="M(4SSQ/LL)", + line_no=line_no, + nonnegative=True, + ) + if prev_s2 is not None and m_4ssq_ll <= prev_s2: + raise ValueError( + f"Non-increasing M(4SSQ/LL) bin coordinate {m_4ssq_ll_tok!r} at line {line_no}" + ) + prev_s2 = m_4ssq_ll + + nr_used = _parse_nonnegative_int_token( + nr_used_tok, context="NR_used", line_no=line_no + ) + _, pct_obs_val, pct_obs_dp = _parse_percentage_token( + pct_obs_tok, context="%_obs", line_no=line_no + ) + _parse_finite_float_token( + m_fo_used_tok, context="M(Fo_used)", line_no=line_no, nonnegative=True + ) + _parse_finite_float_token( + m_fc_used_tok, context="M(Fc_used)", line_no=line_no, nonnegative=True + ) + _, rf_used_val, rf_used_dp = _parse_r_token( + rf_used_tok, context="Rf_used", line_no=line_no + ) + wr_used_val = _parse_finite_float_token( + wr_used_tok, + context="WR_used", + line_no=line_no, + nonnegative=True, + ) + + nr_free = _parse_nonnegative_int_token( + nr_free_tok, context="NR_free", line_no=line_no + ) + _parse_finite_float_token( + m_fo_free_tok, context="M(Fo_free)", line_no=line_no, nonnegative=True + ) + _parse_finite_float_token( + m_fc_free_tok, context="M(Fc_free)", line_no=line_no, nonnegative=True + ) + _, rf_free_val, rf_free_dp = _parse_r_token( + rf_free_tok, context="Rf_free", line_no=line_no + ) + wr_free_val = _parse_finite_float_token( + wr_free_tok, + context="WR_free", + line_no=line_no, + nonnegative=True, + ) + + nr_used_sum += nr_used + nr_free_sum += nr_free + rf_used_places.add(rf_used_dp) + rf_free_places.add(rf_free_dp) + + bins.append( + { + "bin_ordinal": ordinal, + "line": line_no, + "text": row["text"], + "tokens": list(tokens), + "m_4ssq_ll_token": m_4ssq_ll_tok, + "m_4ssq_ll": m_4ssq_ll, + "nr_used_token": nr_used_tok, + "nr_used": nr_used, + "pct_obs_token": pct_obs_tok, + "pct_obs": pct_obs_val, + "pct_obs_decimal_places": pct_obs_dp, + "m_fo_used_token": m_fo_used_tok, + "m_fc_used_token": m_fc_used_tok, + "rf_used_token": rf_used_tok, + "rf_used": rf_used_val, + "rf_used_decimal_places": rf_used_dp, + "wr_used_token": wr_used_tok, + "wr_used": wr_used_val, + "nr_free_token": nr_free_tok, + "nr_free": nr_free, + "m_fo_free_token": m_fo_free_tok, + "m_fc_free_token": m_fc_free_tok, + "rf_free_token": rf_free_tok, + "rf_free": rf_free_val, + "rf_free_decimal_places": rf_free_dp, + "wr_free_token": wr_free_tok, + "wr_free": wr_free_val, + } + ) + compact_rows.append( + [ + ordinal, + line_no, + m_4ssq_ll_tok, + nr_used, + nr_free, + rf_used_tok, + rf_free_tok, + ] + ) + + if nr_used_sum <= 0: + raise ValueError( + f"Resolution R-factor table at line {table_block['title']['line']} has zero total NR_used" + ) + if nr_free_sum <= 0: + raise ValueError( + f"Resolution R-factor table at line {table_block['title']['line']} has zero total NR_free" + ) + + return { + "phase": phase, + "detailed_cycle": table_block["cycle_number"], + "summary_ncyc": summary_ncyc, + "title": dict(table_block["title"]), + "graph_declaration_lines": list(table_block["graph_declaration_lines"]), + "header_lines": list(table_block["header_lines"]), + "header_tokens": list(table_block["header_tokens"]), + "double_dollar_roles": dict(table_block["double_dollar_roles"]), + "data_line_range_inclusive": list(table_block["data_line_range_inclusive"]), + "bin_count": len(bins), + "nr_used_sum": nr_used_sum, + "nr_free_sum": nr_free_sum, + "combined_reported_count": nr_used_sum + nr_free_sum, + "rf_used_printed_decimal_places": ( + next(iter(rf_used_places)) if len(rf_used_places) == 1 else sorted(rf_used_places) + ), + "rf_free_printed_decimal_places": ( + next(iter(rf_free_places)) if len(rf_free_places) == 1 else sorted(rf_free_places) + ), + "bins": bins, + "rows": compact_rows, + } + + +def _parse_cycle_summary_table(table_block: dict[str, Any]) -> dict[str, Any]: + """Validate and structure ``$TABLE: Rfactor analysis, stats vs cycle :``.""" + structured_rows: list[dict[str, Any]] = [] + rfact_places: set[int] = set() + rfree_places: set[int] = set() + + for expected_ncyc, row in enumerate(table_block["rows"]): + line_no = row["line"] + tokens = row["tokens"] + ( + ncyc_tok, + rfact_tok, + rfree_tok, + fom_tok, + ll_tok, + llfree_tok, + rmsbond_tok, + zbond_tok, + rmsangl_tok, + zangl_tok, + rmschiral_tok, + ) = tokens + + ncyc = _parse_nonnegative_int_token( + ncyc_tok, context="summary Ncyc", line_no=line_no + ) + if ncyc != expected_ncyc: + raise ValueError( + f"Non-contiguous or out-of-order summary Ncyc at line {line_no}: " + f"expected {expected_ncyc}, got {ncyc}" + ) + + rfact_dec, rfact_val, rfact_dp = _parse_r_token( + rfact_tok, context=f"summary Rfact (Ncyc {ncyc})", line_no=line_no + ) + rfree_dec, rfree_val, rfree_dp = _parse_r_token( + rfree_tok, context=f"summary Rfree (Ncyc {ncyc})", line_no=line_no + ) + fom_val = _parse_finite_float_token( + fom_tok, + context=f"summary FOM (Ncyc {ncyc})", + line_no=line_no, + nonnegative=True, + ) + for col_label, col_tok in ( + ("-LL", ll_tok), + ("-LLfree", llfree_tok), + ("rmsBOND", rmsbond_tok), + ("zBOND", zbond_tok), + ("rmsANGL", rmsangl_tok), + ("zANGL", zangl_tok), + ("rmsCHIRAL", rmschiral_tok), + ): + _parse_finite_float_token( + col_tok, + context=f"summary {col_label} (Ncyc {ncyc})", + line_no=line_no, + ) + + rfact_places.add(rfact_dp) + rfree_places.add(rfree_dp) + + structured_rows.append( + { + "ncyc": ncyc, + "ncyc_token": ncyc_tok, + "line": line_no, + "text": row["text"], + "tokens": list(tokens), + "rfact_token": rfact_tok, + "rfact": rfact_val, + "rfact_decimal": rfact_dec, + "rfact_decimal_places": rfact_dp, + "rfree_token": rfree_tok, + "rfree": rfree_val, + "rfree_decimal": rfree_dec, + "rfree_decimal_places": rfree_dp, + "fom_token": fom_tok, + "fom": fom_val, + "ll_token": ll_tok, + "llfree_token": llfree_tok, + "rmsbond_token": rmsbond_tok, + "zbond_token": zbond_tok, + "rmsangl_token": rmsangl_tok, + "zangl_token": zangl_tok, + "rmschiral_token": rmschiral_tok, + } + ) + + return { + "title": dict(table_block["title"]), + "graph_declaration_lines": list(table_block["graph_declaration_lines"]), + "header_lines": list(table_block["header_lines"]), + "header_tokens": list(table_block["header_tokens"]), + "double_dollar_roles": dict(table_block["double_dollar_roles"]), + "data_line_range_inclusive": list(table_block["data_line_range_inclusive"]), + "row_count": len(structured_rows), + "summary_cycle_min": structured_rows[0]["ncyc"], + "summary_cycle_max": structured_rows[-1]["ncyc"], + "rfact_printed_decimal_places": ( + next(iter(rfact_places)) if len(rfact_places) == 1 else sorted(rfact_places) + ), + "rfree_printed_decimal_places": ( + next(iter(rfree_places)) if len(rfree_places) == 1 else sorted(rfree_places) + ), + "rows": structured_rows, + } + + +def _parse_text_block( + lines: list[str], start_idx: int +) -> tuple[dict[str, Any], int]: + """Parse one ``$TEXT:: $$ $$`` block closed by ``$$``.""" + title_line_no = start_idx + 1 + title_line_text = lines[start_idx] + match = _TEXT_START_RE.match(title_line_text) + if not match or title_line_text.count("$$") != 2: + raise ValueError( + f"Malformed $TEXT directive at line {title_line_no}: {title_line_text!r}" + ) + text_name = match.group(1).strip() + subtitle = match.group(2).strip() + + idx = start_idx + 1 + body_lines: list[dict[str, Any]] = [] + end_line_no: int | None = None + end_line_text: str | None = None + + while idx < len(lines): + line_no = idx + 1 + raw_line = lines[idx] + stripped = raw_line.strip() + if raw_line.count("$$") > 1: + raise ValueError( + f"Multiple $$ delimiters on line {line_no} inside $TEXT:{text_name}:" + ) + if stripped == "$$": + end_line_no = line_no + end_line_text = raw_line + idx += 1 + break + if "$$" in raw_line: + raise ValueError( + f"Malformed closing $$ at line {line_no} inside $TEXT:{text_name}:" + ) + if stripped.startswith(("$TABLE", "$GRAPHS", "$TEXT", "$SUMMARY", ":TEXT:")): + raise ValueError( + f"Unclosed $TEXT:{text_name}: starting at line {title_line_no} before line {line_no}" + ) + body_lines.append({"line": line_no, "text": raw_line}) + idx += 1 + + if end_line_no is None or end_line_text is None: + raise ValueError( + f"Unclosed $TEXT:{text_name}: starting at line {title_line_no}: missing closing $$" + ) + + return ( + { + "name": text_name, + "subtitle": subtitle, + "title": {"line": title_line_no, "text": title_line_text}, + "body_lines": body_lines, + "end_delimiter": {"line": end_line_no, "text": end_line_text}, + }, + idx, + ) + + +_KNOWN_FINAL_RESULTS_LABELS: tuple[str, ...] = ( + "R factor", + "R free", + "Rms BondLength", + "Rms BondAngle", + "Rms ChirVolume", +) + + +def _parse_final_results_block(text_block: dict[str, Any]) -> dict[str, Any]: + """Validate and structure ``$TEXT:Result: $$ Final results $$``.""" + title_line_no = text_block["title"]["line"] + if text_block["subtitle"] != "Final results": + raise ValueError( + f"Unexpected $TEXT:Result: subtitle at line {title_line_no}: {text_block['subtitle']!r}" + ) + non_empty_lines = [ + entry for entry in text_block["body_lines"] if entry["text"].strip() + ] + if len(non_empty_lines) < 3: + raise ValueError( + f"Truncated Final results block starting at line {title_line_no}" + ) + + header_entry = non_empty_lines[0] + header_tokens = header_entry["text"].split() + if header_tokens != ["Initial", "Final"]: + raise ValueError( + f"Unexpected Final results header at line {header_entry['line']}: " + f"expected ['Initial', 'Final'], got {header_tokens}" + ) + + parsed_rows: list[dict[str, Any]] = [] + by_label: dict[str, dict[str, Any]] = {} + for entry in non_empty_lines[1:]: + line_no = entry["line"] + tokens = entry["text"].split() + if not tokens: + continue + + matched_known_label: str | None = None + value_tokens: list[str] = [] + for known_label in _KNOWN_FINAL_RESULTS_LABELS: + known_parts = known_label.split() + n_parts = len(known_parts) + if len(tokens) >= n_parts and [t.lower() for t in tokens[:n_parts]] == [ + k.lower() for k in known_parts + ]: + if tokens[:n_parts] != known_parts: + raise ValueError( + f"Malformed label casing in Final results block at line {line_no}: {entry['text']!r}" + ) + matched_known_label = known_label + value_tokens = tokens[n_parts:] + break + + if matched_known_label is not None: + label = matched_known_label + else: + first_val_idx: int | None = None + for idx_tok, tok in enumerate(tokens): + if _looks_like_numeric_or_invalid_value_token(tok): + first_val_idx = idx_tok + break + if first_val_idx is None or first_val_idx == 0: + raise ValueError( + f"Truncated or malformed row in Final results block at line {line_no}: {entry['text']!r}" + ) + label_tokens = tokens[:first_val_idx] + value_tokens = tokens[first_val_idx:] + label = " ".join(label_tokens) + if "r factor" in label.lower() or "r free" in label.lower(): + raise ValueError( + f"Unrecognized R label {label!r} in Final results block at line {line_no}" + ) + + if len(value_tokens) != 2: + raise ValueError( + f"Expected exactly 2 values (Initial, Final) for {label!r} in Final results block " + f"at line {line_no}, got {len(value_tokens)}: {entry['text']!r}" + ) + initial_tok, final_tok = value_tokens + if label in by_label: + raise ValueError( + f"Duplicate {label!r} row in Final results block at line {line_no}" + ) + row_record: dict[str, Any] = { + "line": line_no, + "text": entry["text"], + "label": label, + "initial_token": initial_tok, + "final_token": final_tok, + } + if label in {"R factor", "R free"}: + init_dec, init_val, init_dp = _parse_r_token( + initial_tok, context=f"Final results {label} Initial", line_no=line_no + ) + fin_dec, fin_val, fin_dp = _parse_r_token( + final_tok, context=f"Final results {label} Final", line_no=line_no + ) + row_record.update( + initial_decimal=init_dec, + initial_value=init_val, + initial_decimal_places=init_dp, + final_decimal=fin_dec, + final_value=fin_val, + final_decimal_places=fin_dp, + ) + else: + init_val = _parse_finite_float_token( + initial_tok, + context=f"Final results {label} Initial", + line_no=line_no, + nonnegative=(label in _KNOWN_FINAL_RESULTS_LABELS), + ) + fin_val = _parse_finite_float_token( + final_tok, + context=f"Final results {label} Final", + line_no=line_no, + nonnegative=(label in _KNOWN_FINAL_RESULTS_LABELS), + ) + row_record.update( + initial_value=init_val, + final_value=fin_val, + ) + parsed_rows.append(row_record) + by_label[label] = row_record + + if "R factor" not in by_label or "R free" not in by_label: + raise ValueError( + f"Final results block starting at line {title_line_no} is missing 'R factor' or 'R free'" + ) + + rfactor_row = by_label["R factor"] + rfree_row = by_label["R free"] + return { + "title": dict(text_block["title"]), + "header": { + "line": header_entry["line"], + "text": header_entry["text"], + "tokens": header_tokens, + }, + "rows": [ + { + "line": r["line"], + "text": r["text"], + "label": r["label"], + "initial_token": r["initial_token"], + "final_token": r["final_token"], + } + for r in parsed_rows + ], + "end_delimiter": dict(text_block["end_delimiter"]), + "r_factor_line": rfactor_row["line"], + "r_free_line": rfree_row["line"], + "Rfactor_initial_token": rfactor_row["initial_token"], + "Rfactor_final_token": rfactor_row["final_token"], + "Rfree_initial_token": rfree_row["initial_token"], + "Rfree_final_token": rfree_row["final_token"], + "Rfactor_printed_decimal_places": rfactor_row["final_decimal_places"], + "Rfree_printed_decimal_places": rfree_row["final_decimal_places"], + "rfactor_initial_token": rfactor_row["initial_token"], + "rfactor_initial": rfactor_row["initial_value"], + "rfactor_initial_decimal": rfactor_row["initial_decimal"], + "rfactor_initial_decimal_places": rfactor_row["initial_decimal_places"], + "rfactor_final_token": rfactor_row["final_token"], + "rfactor_final": rfactor_row["final_value"], + "rfactor_final_decimal": rfactor_row["final_decimal"], + "rfactor_final_decimal_places": rfactor_row["final_decimal_places"], + "rfree_initial_token": rfree_row["initial_token"], + "rfree_initial": rfree_row["initial_value"], + "rfree_initial_decimal": rfree_row["initial_decimal"], + "rfree_initial_decimal_places": rfree_row["initial_decimal_places"], + "rfree_final_token": rfree_row["final_token"], + "rfree_final": rfree_row["final_value"], + "rfree_final_decimal": rfree_row["final_decimal"], + "rfree_final_decimal_places": rfree_row["final_decimal_places"], + } + + +_KNOWN_DETAIL_LABELS: frozenset[str] = frozenset( + { + "Resolution limits", + "Number of used reflections", + "Percentage observed", + "Percentage of free reflections", + "Overall R factor", + "Free R factor", + "Overall weighted R factor", + "Free weighted R factor", + "Overall weighted R2 factor", + "Free weighted R2 factor", + "Overall correlation coefficient", + "Free correlation coefficient", + "Cruickshanks DPI for coordinate error", + "DPI based on free R factor", + "Overall figure of merit", + "ML based su of positional parameters", + "ML based su of thermal parameters", + } +) + + +def _finalize_detail_group( + entries: list[tuple[int, str, str, str]] +) -> dict[str, Any] | None: + """Validate one contiguous group of detailed cycle key=value lines.""" + if not entries: + return None + labels_present = {label for _, _, label, _ in entries} + + seen: dict[str, tuple[int, str, str]] = {} + for line_no, raw_line, label, rhs in entries: + if label in seen: + raise ValueError( + f"Duplicate {label!r} in detailed cycle block at line {line_no}" + ) + seen[label] = (line_no, raw_line, rhs) + + if "Overall R factor" not in seen or "Free R factor" not in seen: + first_line = entries[0][0] + raise ValueError( + f"Detailed cycle block starting at line {first_line} is missing paired " + f"unweighted 'Overall R factor' / 'Free R factor' (found {sorted(labels_present)})" + ) + + overall_line, overall_raw, overall_rhs = seen["Overall R factor"] + free_line, free_raw, free_rhs = seen["Free R factor"] + if overall_line >= free_line: + raise ValueError( + f"Detailed 'Free R factor' (line {free_line}) precedes 'Overall R factor' (line {overall_line})" + ) + + overall_tokens = overall_rhs.split() + free_tokens = free_rhs.split() + if len(overall_tokens) != 1: + raise ValueError( + f"Expected single R token for 'Overall R factor' at line {overall_line}, got {overall_rhs!r}" + ) + if len(free_tokens) != 1: + raise ValueError( + f"Expected single R token for 'Free R factor' at line {free_line}, got {free_rhs!r}" + ) + + overall_dec, overall_val, overall_dp = _parse_r_token( + overall_tokens[0], context="Overall R factor", line_no=overall_line + ) + free_dec, free_val, free_dp = _parse_r_token( + free_tokens[0], context="Free R factor", line_no=free_line + ) + + detail: dict[str, Any] = { + "cgmat_cycle_line": None, + "cgmat_cycle_token": None, + "cgmat_cycle_number": None, + "overall_r_line": overall_line, + "overall_r_text": overall_raw, + "overall_r_token": overall_tokens[0], + "overall_r": overall_val, + "overall_r_decimal": overall_dec, + "overall_r_decimal_places": overall_dp, + "free_r_line": free_line, + "free_r_text": free_raw, + "free_r_token": free_tokens[0], + "free_r": free_val, + "free_r_decimal": free_dec, + "free_r_decimal_places": free_dp, + "overall_weighted_r_line": None, + "overall_weighted_r_token": None, + "overall_weighted_r": None, + "free_weighted_r_line": None, + "free_weighted_r_token": None, + "free_weighted_r": None, + "overall_weighted_r2_line": None, + "overall_weighted_r2_token": None, + "overall_weighted_r2": None, + "free_weighted_r2_line": None, + "free_weighted_r2_token": None, + "free_weighted_r2": None, + "overall_fom_line": None, + "overall_fom_token": None, + "overall_fom": None, + "overall_correlation_coefficient_line": None, + "overall_correlation_coefficient_token": None, + "overall_correlation_coefficient": None, + "free_correlation_coefficient_line": None, + "free_correlation_coefficient_token": None, + "free_correlation_coefficient": None, + "cruickshanks_dpi_line": None, + "cruickshanks_dpi_token": None, + "cruickshanks_dpi": None, + "dpi_free_r_line": None, + "dpi_free_r_token": None, + "dpi_free_r": None, + "ml_su_positional_line": None, + "ml_su_positional_token": None, + "ml_su_positional": None, + "ml_su_thermal_line": None, + "ml_su_thermal_token": None, + "ml_su_thermal": None, + "number_of_used_reflections_line": None, + "number_of_used_reflections_token": None, + "number_of_used_reflections": None, + "resolution_limits_line": None, + "resolution_limits_tokens": None, + "percentage_observed_line": None, + "percentage_observed_token": None, + "percentage_observed": None, + "percentage_observed_decimal_places": None, + "percentage_free_line": None, + "percentage_free_token": None, + "percentage_free": None, + "percentage_free_decimal_places": None, + "endpoint_lines": [ + {"line": line_no, "text": raw_line} + for line_no, raw_line, label, _ in entries + if label + in { + "Resolution limits", + "Number of used reflections", + "Percentage observed", + "Percentage of free reflections", + "Overall R factor", + "Free R factor", + } + ], + "distinct_weighted_metric_lines": [ + {"line": line_no, "text": raw_line} + for line_no, raw_line, label, _ in entries + if label + in { + "Overall weighted R factor", + "Free weighted R factor", + "Overall weighted R2 factor", + "Free weighted R2 factor", + } + ], + } + + for metric_label, line_key, tok_key, val_key in ( + ( + "Overall weighted R factor", + "overall_weighted_r_line", + "overall_weighted_r_token", + "overall_weighted_r", + ), + ( + "Free weighted R factor", + "free_weighted_r_line", + "free_weighted_r_token", + "free_weighted_r", + ), + ( + "Overall weighted R2 factor", + "overall_weighted_r2_line", + "overall_weighted_r2_token", + "overall_weighted_r2", + ), + ( + "Free weighted R2 factor", + "free_weighted_r2_line", + "free_weighted_r2_token", + "free_weighted_r2", + ), + ( + "Overall figure of merit", + "overall_fom_line", + "overall_fom_token", + "overall_fom", + ), + ): + if metric_label in seen: + m_line, _, m_rhs = seen[metric_label] + m_toks = m_rhs.split() + if len(m_toks) != 1: + raise ValueError( + f"Expected single token for {metric_label!r} at line {m_line}, got {m_rhs!r}" + ) + m_val = _parse_finite_float_token( + m_toks[0], + context=metric_label, + line_no=m_line, + nonnegative=True, + ) + detail[line_key] = m_line + detail[tok_key] = m_toks[0] + detail[val_key] = m_val + + for opt_label, line_key, tok_key, val_key in ( + ( + "Overall correlation coefficient", + "overall_correlation_coefficient_line", + "overall_correlation_coefficient_token", + "overall_correlation_coefficient", + ), + ( + "Free correlation coefficient", + "free_correlation_coefficient_line", + "free_correlation_coefficient_token", + "free_correlation_coefficient", + ), + ( + "Cruickshanks DPI for coordinate error", + "cruickshanks_dpi_line", + "cruickshanks_dpi_token", + "cruickshanks_dpi", + ), + ( + "DPI based on free R factor", + "dpi_free_r_line", + "dpi_free_r_token", + "dpi_free_r", + ), + ( + "ML based su of positional parameters", + "ml_su_positional_line", + "ml_su_positional_token", + "ml_su_positional", + ), + ( + "ML based su of thermal parameters", + "ml_su_thermal_line", + "ml_su_thermal_token", + "ml_su_thermal", + ), + ): + if opt_label in seen: + o_line, _, o_rhs = seen[opt_label] + o_toks = o_rhs.split() + if len(o_toks) != 1: + raise ValueError( + f"Expected single scalar value for {opt_label!r} at line {o_line}, got {o_rhs!r}" + ) + o_val = _parse_finite_float_token( + o_toks[0], + context=opt_label, + line_no=o_line, + ) + detail[line_key] = o_line + detail[tok_key] = o_toks[0] + detail[val_key] = o_val + + if "Number of used reflections" in seen: + u_line, _, u_rhs = seen["Number of used reflections"] + u_toks = u_rhs.split() + if len(u_toks) != 1: + raise ValueError( + f"Expected single integer for 'Number of used reflections' at line {u_line}" + ) + u_val = _parse_positive_int_token( + u_toks[0], context="Number of used reflections", line_no=u_line + ) + detail["number_of_used_reflections_line"] = u_line + detail["number_of_used_reflections_token"] = u_toks[0] + detail["number_of_used_reflections"] = u_val + + if "Resolution limits" in seen: + r_line, _, r_rhs = seen["Resolution limits"] + r_toks = r_rhs.split() + if len(r_toks) != 2: + raise ValueError( + f"Expected 2 resolution limits at line {r_line}, got {r_rhs!r}" + ) + for tok in r_toks: + _parse_finite_float_token( + tok, context="Resolution limits", line_no=r_line, nonnegative=True + ) + detail["resolution_limits_line"] = r_line + detail["resolution_limits_tokens"] = r_toks + + if "Percentage observed" in seen: + p_line, _, p_rhs = seen["Percentage observed"] + p_toks = p_rhs.split() + if len(p_toks) != 1: + raise ValueError( + f"Expected 1 value for 'Percentage observed' at line {p_line}" + ) + _, p_val, p_dp = _parse_percentage_token( + p_toks[0], context="Percentage observed", line_no=p_line + ) + detail["percentage_observed_line"] = p_line + detail["percentage_observed_token"] = p_toks[0] + detail["percentage_observed"] = p_val + detail["percentage_observed_decimal_places"] = p_dp + + if "Percentage of free reflections" in seen: + pf_line, _, pf_rhs = seen["Percentage of free reflections"] + pf_toks = pf_rhs.split() + if len(pf_toks) != 1: + raise ValueError( + f"Expected 1 value for 'Percentage of free reflections' at line {pf_line}" + ) + _, pf_val, pf_dp = _parse_percentage_token( + pf_toks[0], + context="Percentage of free reflections", + line_no=pf_line, + ) + detail["percentage_free_line"] = pf_line + detail["percentage_free_token"] = pf_toks[0] + detail["percentage_free"] = pf_val + detail["percentage_free_decimal_places"] = pf_dp + + return detail + + +def _reconcile_r_pair( + *, + cycle_label: str, + detail_overall_tok: str, + detail_overall_dec: Decimal, + detail_overall_line: int, + detail_free_tok: str, + detail_free_dec: Decimal, + detail_free_line: int, + summary_rfact_tok: str, + summary_rfact_dec: Decimal, + summary_rfree_tok: str, + summary_rfree_dec: Decimal, + summary_line: int, + final_results_rfactor_tok: str | None = None, + final_results_rfactor_dec: Decimal | None = None, + final_results_rfactor_line: int | None = None, + final_results_rfree_tok: str | None = None, + final_results_rfree_dec: Decimal | None = None, + final_results_rfree_line: int | None = None, +) -> None: + """Require exact Decimal value agreement across reporting locations for a cycle.""" + if detail_overall_dec != summary_rfact_dec: + raise ValueError( + f"Conflicting R-work for {cycle_label}: detailed Overall R factor " + f"{detail_overall_tok!r} (line {detail_overall_line}) != cycle summary Rfact " + f"{summary_rfact_tok!r} (line {summary_line})" + ) + if detail_free_dec != summary_rfree_dec: + raise ValueError( + f"Conflicting R-free for {cycle_label}: detailed Free R factor " + f"{detail_free_tok!r} (line {detail_free_line}) != cycle summary Rfree " + f"{summary_rfree_tok!r} (line {summary_line})" + ) + if final_results_rfactor_tok is not None and final_results_rfactor_dec is not None: + if final_results_rfactor_dec != summary_rfact_dec: + raise ValueError( + f"Conflicting R-work for {cycle_label}: Final results R factor " + f"{final_results_rfactor_tok!r} (line {final_results_rfactor_line}) != " + f"cycle summary Rfact {summary_rfact_tok!r} (line {summary_line})" + ) + if final_results_rfree_tok is not None and final_results_rfree_dec is not None: + if final_results_rfree_dec != summary_rfree_dec: + raise ValueError( + f"Conflicting R-free for {cycle_label}: Final results R free " + f"{final_results_rfree_tok!r} (line {final_results_rfree_line}) != " + f"cycle summary Rfree {summary_rfree_tok!r} (line {summary_line})" + ) + + +def parse_refmac_log(text: str) -> dict[str, Any]: + """Parse and reconcile a native REFMAC5 log string. + + Parameters + ---------- + text: + Complete REFMAC5 log content as a string. + + Returns + ------- + dict + Structured dictionary containing parsed ``table_blocks``, + ``cycle_summary``, ``detailed_cycles``, ``cycle_r_values``, + ``resolution_bin_values``, ``initial_resolution_bins``, + ``final_resolution_bins``, ``endpoint_detail``, ``final_results_block``, + ``reconciled_initial_cycle``, ``reconciled_final_cycle``, and + ``consistency_checks``. + """ + if not isinstance(text, str) or not text.strip(): + raise ValueError("REFMAC log text must be a non-empty string") + + lines = text.splitlines() + total_lines = len(lines) + + raw_tables: list[dict[str, Any]] = [] + text_blocks: list[dict[str, Any]] = [] + detailed_cycles: list[dict[str, Any]] = [] + current_detail_entries: list[tuple[int, str, str, str]] = [] + cgmat_markers_raw: list[dict[str, Any]] = [] + + reported_suite: str | None = None + reported_refmac_version: str | None = None + reported_run_date_token: str | None = None + data_ncyc_values: list[tuple[int, int]] = [] + refinement_cycles_values: list[tuple[int, int]] = [] + + def _flush_detail_group() -> None: + nonlocal current_detail_entries + if current_detail_entries: + finalized = _finalize_detail_group(current_detail_entries) + current_detail_entries = [] + if finalized is not None: + detailed_cycles.append(finalized) + + idx = 0 + while idx < total_lines: + line_no = idx + 1 + raw_line = lines[idx] + stripped = raw_line.strip() + + banner_match = _BANNER_VERSION_RE.search(raw_line) + if banner_match and reported_refmac_version is None: + reported_suite = banner_match.group(1) + reported_refmac_version = banner_match.group(2) + + date_match = _RUN_DATE_RE.search(raw_line) + if date_match and reported_run_date_token is None: + reported_run_date_token = date_match.group(1) + + ncyc_match = _DATA_NCYC_RE.match(raw_line) + if ncyc_match: + if ncyc_match.group(2): + raise ValueError( + f"Unexpected trailing tokens on NCYC line {line_no}: {raw_line!r}" + ) + ncyc_val = _parse_nonnegative_int_token( + ncyc_match.group(1), context="Data line--- NCYC", line_no=line_no + ) + data_ncyc_values.append((line_no, ncyc_val)) + + ref_cyc_match = _REFINEMENT_CYCLES_RE.match(raw_line) + if ref_cyc_match: + if ref_cyc_match.group(2): + raise ValueError( + f"Unexpected trailing tokens on Refinement cycles line {line_no}: {raw_line!r}" + ) + ref_cyc_val = _parse_nonnegative_int_token( + ref_cyc_match.group(1), context="Refinement cycles", line_no=line_no + ) + refinement_cycles_values.append((line_no, ref_cyc_val)) + + if stripped.startswith("$TABLE"): + _flush_detail_group() + table_block, idx = _parse_table_block(lines, idx) + _classify_and_validate_table(table_block) + table_block["preceding_detailed_cycle_count"] = len(detailed_cycles) + raw_tables.append(table_block) + continue + + if stripped.startswith("$TEXT"): + _flush_detail_group() + text_block, idx = _parse_text_block(lines, idx) + text_block["preceding_detailed_cycle_count"] = len(detailed_cycles) + text_blocks.append(text_block) + continue + + if _SUMMARY_DIRECTIVE_RE.match(raw_line) or _TEXT_REF_DIRECTIVE_RE.match(raw_line): + _flush_detail_group() + idx += 1 + continue + + if "$$" in raw_line or stripped.startswith(("$GRAPHS", "$SUMMARY", ":TEXT:")): + raise ValueError( + f"Stray or malformed loggraph delimiter/directive at line {line_no}: {raw_line!r}" + ) + + if not stripped or stripped.startswith("----"): + _flush_detail_group() + idx += 1 + continue + + if "=" in raw_line: + lhs, _, rhs = raw_line.partition("=") + label = " ".join(lhs.strip().split()) + if label.lower() == "cgmat cycle number": + if label != "CGMAT cycle number": + raise ValueError( + f"Malformed CGMAT cycle marker label {label!r} at line {line_no}" + ) + _flush_detail_group() + rhs_tokens = rhs.split() + if len(rhs_tokens) != 1: + raise ValueError( + f"Expected single integer token for 'CGMAT cycle number' at line {line_no}, got {rhs!r}" + ) + cgmat_num = _parse_positive_int_token( + rhs_tokens[0], context="CGMAT cycle number", line_no=line_no + ) + cgmat_markers_raw.append( + { + "line": line_no, + "text": raw_line, + "token": rhs_tokens[0], + "cycle_number": cgmat_num, + "preceding_detailed_cycle_count": len(detailed_cycles), + } + ) + idx += 1 + continue + if label in _KNOWN_DETAIL_LABELS: + current_detail_entries.append((line_no, raw_line, label, rhs.strip())) + idx += 1 + continue + if label.lower() in {k.lower() for k in _KNOWN_DETAIL_LABELS}: + raise ValueError( + f"Malformed detail label {label!r} at line {line_no}" + ) + if "r factor" in label.lower() or "r2 factor" in label.lower(): + raise ValueError( + f"Unrecognized R-factor detail label {label!r} at line {line_no}" + ) + + _flush_detail_group() + idx += 1 + + _flush_detail_group() + + # Validate NCYC declarations if present + requested_ncyc: int | None = None + for src_line, val in data_ncyc_values + refinement_cycles_values: + if requested_ncyc is None: + requested_ncyc = val + elif val != requested_ncyc: + raise ValueError( + f"Contradictory requested NCYC count {val} at line {src_line} vs {requested_ncyc}" + ) + + # Locate required cycle_summary table + cycle_summary_blocks = [ + t for t in raw_tables if t["table_kind"] == "cycle_summary" + ] + if not cycle_summary_blocks: + raise ValueError( + "Missing required '$TABLE: Rfactor analysis, stats vs cycle :' block" + ) + if len(cycle_summary_blocks) > 1: + raise ValueError( + f"Ambiguous REFMAC log: found {len(cycle_summary_blocks)} cycle-summary tables" + ) + cycle_summary_raw = cycle_summary_blocks[0] + cycle_summary = _parse_cycle_summary_table(cycle_summary_raw) + summary_rows = cycle_summary["rows"] + num_cycles = len(summary_rows) + + if requested_ncyc is not None and cycle_summary["summary_cycle_max"] != requested_ncyc: + raise ValueError( + f"Cycle summary max Ncyc ({cycle_summary['summary_cycle_max']}) contradicts " + f"declared NCYC ({requested_ncyc})" + ) + + # Validate detailed cycles count and ordering relative to cycle_summary + if len(detailed_cycles) != num_cycles: + raise ValueError( + f"Cycle count mismatch: found {len(detailed_cycles)} detailed Overall/Free R pairs " + f"but {num_cycles} cycle-summary rows" + ) + if cycle_summary_raw["preceding_detailed_cycle_count"] != num_cycles: + raise ValueError( + "Ambiguous cycle ordering: detailed Overall/Free R pairs appear after the cycle-summary table" + ) + + # Validate and associate optional CGMAT cycle number markers when present + cgmat_cycle_markers: list[dict[str, Any]] = [] + prev_cgmat_cycle: int | None = None + seen_cgmat_detail_indices: set[int] = set() + for marker in cgmat_markers_raw: + m_line = marker["line"] + m_cyc = marker["cycle_number"] + prec_detail = marker["preceding_detailed_cycle_count"] + if prev_cgmat_cycle is not None and m_cyc <= prev_cgmat_cycle: + raise ValueError( + f"Non-increasing or duplicate CGMAT cycle number {m_cyc} at line {m_line} " + f"after CGMAT cycle number {prev_cgmat_cycle}" + ) + if prec_detail in seen_cgmat_detail_indices: + raise ValueError( + f"Multiple CGMAT cycle number markers before detailed cycle {prec_detail + 1} (at line {m_line})" + ) + if prec_detail >= num_cycles: + raise ValueError( + f"CGMAT cycle number {m_cyc} at line {m_line} appears after all {num_cycles} detailed cycles" + ) + expected_detailed_cycle = prec_detail + 1 + if m_cyc != expected_detailed_cycle: + raise ValueError( + f"CGMAT cycle number {m_cyc} at line {m_line} does not match associated detailed Cycle {expected_detailed_cycle}" + ) + prev_cgmat_cycle = m_cyc + seen_cgmat_detail_indices.add(prec_detail) + detail_target = detailed_cycles[prec_detail] + detail_target["cgmat_cycle_line"] = m_line + detail_target["cgmat_cycle_token"] = marker["token"] + detail_target["cgmat_cycle_number"] = m_cyc + cgmat_cycle_markers.append( + { + "line": m_line, + "text": marker["text"], + "token": marker["token"], + "cycle_number": m_cyc, + "detailed_cycle": expected_detailed_cycle, + "summary_ncyc": summary_rows[prec_detail]["ncyc"], + } + ) + + # Locate required Final results ($TEXT:Result:) block + result_text_blocks = [b for b in text_blocks if b["name"] == "Result"] + if not result_text_blocks: + raise ValueError( + "Missing required '$TEXT:Result: $$ Final results $$' block" + ) + if len(result_text_blocks) > 1: + raise ValueError( + f"Ambiguous REFMAC log: found {len(result_text_blocks)} '$TEXT:Result:' blocks" + ) + if result_text_blocks[0]["preceding_detailed_cycle_count"] != num_cycles: + raise ValueError( + "Ambiguous cycle ordering: detailed Overall/Free R pairs appear after Final results block" + ) + final_results_block = _parse_final_results_block(result_text_blocks[0]) + + # Validate resolution_rfactor tables and resolve initial, intermediate, and final cycle tables + resln_rfactor_blocks = [ + t for t in raw_tables if t["table_kind"] == "resolution_rfactor" + ] + if not resln_rfactor_blocks: + raise ValueError( + "Missing required '$TABLE: Cycle . Rfactor analysis, F distribution v resln :' block" + ) + + seen_resln_cycles: list[int] = [] + for block in resln_rfactor_blocks: + cyc_num = block["cycle_number"] + if seen_resln_cycles and cyc_num <= seen_resln_cycles[-1]: + raise ValueError( + f"Non-increasing or duplicate resolution R-factor table Cycle {cyc_num} " + f"at line {block['title']['line']} after Cycle {seen_resln_cycles[-1]}" + ) + if cyc_num < 1 or cyc_num > num_cycles: + raise ValueError( + f"Resolution R-factor table Cycle {cyc_num} at line {block['title']['line']} " + f"is out of range for 1..{num_cycles} detailed cycles" + ) + # Table for detailed cycle C must precede the C-th detailed Overall/Free pair + if block["preceding_detailed_cycle_count"] != cyc_num - 1: + raise ValueError( + f"Resolution R-factor table Cycle {cyc_num} at line {block['title']['line']} " + f"is misaligned with detailed cycle pairs (preceded by " + f"{block['preceding_detailed_cycle_count']} pairs, expected {cyc_num - 1})" + ) + seen_resln_cycles.append(cyc_num) + + if resln_rfactor_blocks[0]["cycle_number"] != 1: + raise ValueError( + f"First resolution R-factor table must be Cycle 1 (corresponding to Ncyc 0), " + f"got Cycle {resln_rfactor_blocks[0]['cycle_number']}" + ) + if resln_rfactor_blocks[-1]["cycle_number"] != num_cycles: + raise ValueError( + f"Ambiguous or missing final resolution R-factor table: last table is " + f"Cycle {resln_rfactor_blocks[-1]['cycle_number']}, expected final detailed Cycle {num_cycles} " + f"(for summary Ncyc {num_cycles - 1})" + ) + + parsed_resln_rfactor_tables: list[dict[str, Any]] = [] + for block in resln_rfactor_blocks: + cyc_num = block["cycle_number"] + if num_cycles == 1: + phase_label = "final" + elif cyc_num == 1: + phase_label = "initial" + elif cyc_num == num_cycles: + phase_label = "final" + else: + phase_label = "intermediate" + parsed_table = _parse_resolution_rfactor_table( + block, phase=phase_label, summary_ncyc=cyc_num - 1 + ) + detail_for_cyc = detailed_cycles[cyc_num - 1] + if detail_for_cyc["number_of_used_reflections"] is not None: + if ( + detail_for_cyc["number_of_used_reflections"] + != parsed_table["nr_used_sum"] + ): + raise ValueError( + f"Detailed Cycle {cyc_num} Number of used reflections " + f"({detail_for_cyc['number_of_used_reflections']} at line " + f"{detail_for_cyc['number_of_used_reflections_line']}) != " + f"Cycle {cyc_num} resolution table NR_used sum ({parsed_table['nr_used_sum']})" + ) + parsed_resln_rfactor_tables.append(parsed_table) + + if num_cycles == 1: + initial_resolution_bins = _parse_resolution_rfactor_table( + resln_rfactor_blocks[0], phase="initial", summary_ncyc=0 + ) + final_resolution_bins = parsed_resln_rfactor_tables[-1] + else: + initial_resolution_bins = parsed_resln_rfactor_tables[0] + final_resolution_bins = parsed_resln_rfactor_tables[-1] + + # Validate resolution_fom tables cycle uniqueness, ordering, and association + fom_blocks = [t for t in raw_tables if t["table_kind"] == "resolution_fom"] + seen_fom_cycles: list[int] = [] + for f_block in fom_blocks: + f_cyc = f_block["cycle_number"] + if seen_fom_cycles and f_cyc <= seen_fom_cycles[-1]: + raise ValueError( + f"Non-increasing or duplicate resolution FOM table Cycle {f_cyc} " + f"at line {f_block['title']['line']} after Cycle {seen_fom_cycles[-1]}" + ) + if f_cyc < 1 or f_cyc > num_cycles: + raise ValueError( + f"Resolution FOM table Cycle {f_cyc} at line {f_block['title']['line']} " + f"is out of range for 1..{num_cycles} detailed cycles" + ) + if f_block["preceding_detailed_cycle_count"] != f_cyc - 1: + raise ValueError( + f"Resolution FOM table Cycle {f_cyc} at line {f_block['title']['line']} " + f"is misaligned with detailed cycle pairs (preceded by " + f"{f_block['preceding_detailed_cycle_count']} pairs, expected {f_cyc - 1})" + ) + seen_fom_cycles.append(f_cyc) + + # Assign table_block IDs matching 141-refmac-parser-truth.json conventions + table_blocks_out: list[dict[str, Any]] = [] + other_idx = 0 + for block in raw_tables: + kind = block["table_kind"] + cyc = block["cycle_number"] + if kind == "resolution_rfactor": + if num_cycles == 1: + block_id = "final_resolution_rfactor" + elif cyc == 1: + block_id = "initial_resolution_rfactor" + elif cyc == num_cycles: + block_id = "final_resolution_rfactor" + else: + block_id = f"cycle_{cyc}_resolution_rfactor" + elif kind == "resolution_fom": + if num_cycles == 1: + block_id = "final_resolution_fom" + elif cyc == 1: + block_id = "initial_resolution_fom" + elif cyc == num_cycles: + block_id = "final_resolution_fom" + else: + block_id = f"cycle_{cyc}_resolution_fom" + elif kind == "cycle_summary": + block_id = "cycle_summary" + else: + other_idx += 1 + block_id = f"other_table_{other_idx}" + + table_blocks_out.append( + { + "id": block_id, + "table_kind": kind, + "cycle_number": cyc, + "title": dict(block["title"]), + "graph_declaration_lines": list(block["graph_declaration_lines"]), + "header_lines": list(block["header_lines"]), + "header_tokens": list(block["header_tokens"]), + "double_dollar_roles": dict(block["double_dollar_roles"]), + "data_line_range_inclusive": list(block["data_line_range_inclusive"]), + "row_count": block["row_count"], + "column_count": block["column_count"], + "rows": [ + { + "line": r["line"], + "text": r["text"], + "tokens": list(r["tokens"]), + } + for r in block["rows"] + ], + } + ) + + # Reconcile every cycle i in 0..num_cycles-1 across detailed_cycles, cycle_summary, and final_results + cycle_r_rows: list[list[Any]] = [] + for i in range(num_cycles): + detail = detailed_cycles[i] + summary = summary_rows[i] + detailed_cycle_num = i + 1 + summary_ncyc = summary["ncyc"] + detail["detailed_cycle"] = detailed_cycle_num + detail["summary_ncyc"] = summary_ncyc + + is_initial = i == 0 + is_final = i == num_cycles - 1 + + _reconcile_r_pair( + cycle_label=f"detailed Cycle {detailed_cycle_num} / summary Ncyc {summary_ncyc}", + detail_overall_tok=detail["overall_r_token"], + detail_overall_dec=detail["overall_r_decimal"], + detail_overall_line=detail["overall_r_line"], + detail_free_tok=detail["free_r_token"], + detail_free_dec=detail["free_r_decimal"], + detail_free_line=detail["free_r_line"], + summary_rfact_tok=summary["rfact_token"], + summary_rfact_dec=summary["rfact_decimal"], + summary_rfree_tok=summary["rfree_token"], + summary_rfree_dec=summary["rfree_decimal"], + summary_line=summary["line"], + final_results_rfactor_tok=( + final_results_block["rfactor_initial_token"] if is_initial else None + ), + final_results_rfactor_dec=( + final_results_block["rfactor_initial_decimal"] if is_initial else None + ), + final_results_rfactor_line=( + final_results_block["r_factor_line"] if is_initial else None + ), + final_results_rfree_tok=( + final_results_block["rfree_initial_token"] if is_initial else None + ), + final_results_rfree_dec=( + final_results_block["rfree_initial_decimal"] if is_initial else None + ), + final_results_rfree_line=( + final_results_block["r_free_line"] if is_initial else None + ), + ) + + if is_final: + _reconcile_r_pair( + cycle_label=f"final detailed Cycle {detailed_cycle_num} / summary Ncyc {summary_ncyc}", + detail_overall_tok=detail["overall_r_token"], + detail_overall_dec=detail["overall_r_decimal"], + detail_overall_line=detail["overall_r_line"], + detail_free_tok=detail["free_r_token"], + detail_free_dec=detail["free_r_decimal"], + detail_free_line=detail["free_r_line"], + summary_rfact_tok=summary["rfact_token"], + summary_rfact_dec=summary["rfact_decimal"], + summary_rfree_tok=summary["rfree_token"], + summary_rfree_dec=summary["rfree_decimal"], + summary_line=summary["line"], + final_results_rfactor_tok=final_results_block["rfactor_final_token"], + final_results_rfactor_dec=final_results_block["rfactor_final_decimal"], + final_results_rfactor_line=final_results_block["r_factor_line"], + final_results_rfree_tok=final_results_block["rfree_final_token"], + final_results_rfree_dec=final_results_block["rfree_final_decimal"], + final_results_rfree_line=final_results_block["r_free_line"], + ) + + cycle_r_rows.append( + [ + summary_ncyc, + summary["rfact_token"], + summary["rfree_token"], + summary["line"], + detail["overall_r_line"], + detail["free_r_line"], + ] + ) + + initial_detail = detailed_cycles[0] + final_detail = detailed_cycles[-1] + + # Strip internal Decimal objects before returning JSON-serializable dict + for row in summary_rows: + row.pop("rfact_decimal", None) + row.pop("rfree_decimal", None) + for detail in detailed_cycles: + detail.pop("overall_r_decimal", None) + detail.pop("free_r_decimal", None) + final_results_block.pop("rfactor_initial_decimal", None) + final_results_block.pop("rfactor_final_decimal", None) + final_results_block.pop("rfree_initial_decimal", None) + final_results_block.pop("rfree_final_decimal", None) + + initial_summary = summary_rows[0] + final_summary = summary_rows[-1] + + resolution_tables_list = parsed_resln_rfactor_tables + initial_count_vector = [ + (b["m_4ssq_ll_token"], b["nr_used"], b["nr_free"]) + for b in initial_resolution_bins["bins"] + ] + final_count_vector = [ + (b["m_4ssq_ll_token"], b["nr_used"], b["nr_free"]) + for b in final_resolution_bins["bins"] + ] + + reconciled_initial_cycle: dict[str, Any] = { + "summary_ncyc": 0, + "detailed_cycle": 1, + "resolution_table_cycle": initial_resolution_bins["detailed_cycle"], + "r_work_token": initial_summary["rfact_token"], + "r_work": initial_summary["rfact"], + "r_work_decimal_places": initial_summary["rfact_decimal_places"], + "r_free_token": initial_summary["rfree_token"], + "r_free": initial_summary["rfree"], + "r_free_decimal_places": initial_summary["rfree_decimal_places"], + "detailed_r_work_token": initial_detail["overall_r_token"], + "detailed_r_work_decimal_places": initial_detail["overall_r_decimal_places"], + "detailed_r_free_token": initial_detail["free_r_token"], + "detailed_r_free_decimal_places": initial_detail["free_r_decimal_places"], + "summary_r_work_token": initial_summary["rfact_token"], + "summary_r_work_decimal_places": initial_summary["rfact_decimal_places"], + "summary_r_free_token": initial_summary["rfree_token"], + "summary_r_free_decimal_places": initial_summary["rfree_decimal_places"], + "final_results_r_work_token": final_results_block["rfactor_initial_token"], + "final_results_r_work_decimal_places": final_results_block[ + "rfactor_initial_decimal_places" + ], + "final_results_r_free_token": final_results_block["rfree_initial_token"], + "final_results_r_free_decimal_places": final_results_block[ + "rfree_initial_decimal_places" + ], + "nr_used_sum": initial_resolution_bins["nr_used_sum"], + "nr_free_sum": initial_resolution_bins["nr_free_sum"], + "combined_reported_count": initial_resolution_bins["combined_reported_count"], + "sources": { + "detailed_overall_line": initial_detail["overall_r_line"], + "detailed_free_line": initial_detail["free_r_line"], + "cycle_summary_line": initial_summary["line"], + "final_results_r_factor_line": final_results_block["r_factor_line"], + "final_results_r_free_line": final_results_block["r_free_line"], + "resolution_rfactor_title_line": initial_resolution_bins["title"]["line"], + }, + } + + reconciled_final_cycle: dict[str, Any] = { + "summary_ncyc": final_summary["ncyc"], + "detailed_cycle": num_cycles, + "resolution_table_cycle": final_resolution_bins["detailed_cycle"], + "r_work_token": final_summary["rfact_token"], + "r_work": final_summary["rfact"], + "r_work_decimal_places": final_summary["rfact_decimal_places"], + "r_free_token": final_summary["rfree_token"], + "r_free": final_summary["rfree"], + "r_free_decimal_places": final_summary["rfree_decimal_places"], + "detailed_r_work_token": final_detail["overall_r_token"], + "detailed_r_work_decimal_places": final_detail["overall_r_decimal_places"], + "detailed_r_free_token": final_detail["free_r_token"], + "detailed_r_free_decimal_places": final_detail["free_r_decimal_places"], + "summary_r_work_token": final_summary["rfact_token"], + "summary_r_work_decimal_places": final_summary["rfact_decimal_places"], + "summary_r_free_token": final_summary["rfree_token"], + "summary_r_free_decimal_places": final_summary["rfree_decimal_places"], + "final_results_r_work_token": final_results_block["rfactor_final_token"], + "final_results_r_work_decimal_places": final_results_block[ + "rfactor_final_decimal_places" + ], + "final_results_r_free_token": final_results_block["rfree_final_token"], + "final_results_r_free_decimal_places": final_results_block[ + "rfree_final_decimal_places" + ], + "nr_used_sum": final_resolution_bins["nr_used_sum"], + "nr_free_sum": final_resolution_bins["nr_free_sum"], + "combined_reported_count": final_resolution_bins["combined_reported_count"], + "sources": { + "detailed_overall_line": final_detail["overall_r_line"], + "detailed_free_line": final_detail["free_r_line"], + "cycle_summary_line": final_summary["line"], + "final_results_r_factor_line": final_results_block["r_factor_line"], + "final_results_r_free_line": final_results_block["r_free_line"], + "resolution_rfactor_title_line": final_resolution_bins["title"]["line"], + }, + } + + return { + "source_metadata": { + "line_count": total_lines, + "reported_suite": reported_suite, + "reported_refmac_version": reported_refmac_version, + "reported_run_date_token": reported_run_date_token, + "requested_ncyc": requested_ncyc, + }, + "table_blocks": table_blocks_out, + "cycle_summary": cycle_summary, + "detailed_cycles": detailed_cycles, + "cgmat_cycle_markers": cgmat_cycle_markers, + "cycle_r_values": { + "row_fields": [ + "summary_Ncyc", + "Rfact_token", + "Rfree_token", + "summary_line", + "matching_ordered_overall_detail_line", + "matching_ordered_free_detail_line", + ], + "rows": cycle_r_rows, + "summary_cycle_min": cycle_summary["summary_cycle_min"], + "summary_cycle_max": cycle_summary["summary_cycle_max"], + "summary_row_count": num_cycles, + "Rfact_printed_decimal_places": cycle_summary[ + "rfact_printed_decimal_places" + ], + "Rfree_printed_decimal_places": cycle_summary[ + "rfree_printed_decimal_places" + ], + "all_ordered_detail_pairs_equal_summary_values": True, + "detail_rfactor_table_cycle_labels": seen_resln_cycles, + }, + "resolution_bin_values": { + "row_fields": [ + "bin_ordinal", + "line", + "M(4SSQ/LL)_token", + "NR_used", + "NR_free", + "Rf_used_token", + "Rf_free_token", + ], + "tables": resolution_tables_list, + "Rf_used_printed_decimal_places": final_resolution_bins[ + "rf_used_printed_decimal_places" + ], + "Rf_free_printed_decimal_places": final_resolution_bins[ + "rf_free_printed_decimal_places" + ], + "initial_and_final_count_vectors_equal": ( + initial_count_vector == final_count_vector + ), + }, + "initial_resolution_bins": initial_resolution_bins, + "final_resolution_bins": final_resolution_bins, + "endpoint_detail": { + "initial": initial_detail["endpoint_lines"], + "final": final_detail["endpoint_lines"], + "final_distinct_weighted_metrics": final_detail[ + "distinct_weighted_metric_lines" + ], + "Rwork_label": "Overall R factor", + "Rfree_label": "Free R factor", + "Rwork_final_token": final_detail["overall_r_token"], + "Rfree_final_token": final_detail["free_r_token"], + "Rwork_printed_decimal_places": final_detail["overall_r_decimal_places"], + "Rfree_printed_decimal_places": final_detail["free_r_decimal_places"], + }, + "final_results_block": final_results_block, + "reconciled_initial_cycle": reconciled_initial_cycle, + "reconciled_final_cycle": reconciled_final_cycle, + "consistency_checks": { + "initial_detail_summary_and_final_results_initial_values_equal": True, + "final_detail_summary_and_final_results_final_values_equal": True, + "final_nr_used_sum": final_resolution_bins["nr_used_sum"], + "final_nr_free_sum": final_resolution_bins["nr_free_sum"], + "final_combined_reported_count": final_resolution_bins[ + "combined_reported_count" + ], + "final_nr_used_sum_matches_reported_used_reflections": ( + final_detail["number_of_used_reflections"] + == final_resolution_bins["nr_used_sum"] + if final_detail["number_of_used_reflections"] is not None + else None + ), + }, + } diff --git a/scripts/onesar_rerun.py b/scripts/onesar_rerun.py new file mode 100644 index 0000000..cdded11 --- /dev/null +++ b/scripts/onesar_rerun.py @@ -0,0 +1,3326 @@ +#!/usr/bin/env python3 +"""Input admission, observation-only MTZ projection, and PHENIX R-factor worker for 1SAR. + +This module implements Phase 1 of the preregistered 1SAR rerun harness: +1. Exact archive and member SHA-256 admission for the four allowed trajectory + subjects (``start``, ``R4``, ``R5``, ``R6``) and shared ``data/1sar.mtz``, + while rejecting ``R7`` and any unlisted subject. +2. Fresh repository-local evidence-root creation that rejects symlinks and + pre-existing directories so prior logs or outputs cannot be reused. +3. Projection of ``data/1sar.mtz`` to the six observation columns + ``H, K, L, F-obs, SIGF-obs, R-free-flags`` while preserving all 7,248 raw + rows, cell, symmetry, ``VALM NAN``, and exact IEEE-754 4-byte words, and + excluding all archived model/map columns. +4. Construction of the raw and input-policy-eligible HKL ledger (where finite + zero or negative ``F-obs`` with finite positive ``SIGF-obs`` are eligible, + excluded padding rows with NaN ``R-free-flags`` are retained as unassigned, + and duplicate HKLs or invalid/NaN ``R-free-flags`` on eligible observations + fail closed). +5. Execution of the public first-block ``mmtbx.model_vs_data`` R-factor helper + through :func:`refinement_cache.cached_phenix_operation` and full-precision + arithmetic, HKL-set, scatterer-snapshot, and map-MTZ reconciliation. +""" + +from __future__ import annotations + +import copy +import hashlib +import json +import math +import os +import re +import stat +import struct +import sys +import zipfile +from decimal import Decimal, InvalidOperation +from pathlib import Path +from typing import Any, Callable, Mapping, Sequence + +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +import onesar_phenix_measure # noqa: E402 +import refinement_cache # noqa: E402 +import toolchain # noqa: E402 +from audit_1sar_retained_evidence import ( # noqa: E402 + _mtz_headers, + mtz_crystallographic_context, +) +from t03_reflection_retention import ( # noqa: E402 + _column_bindings, + _require_supported_mtz, + audit_mtz_retention, +) + + +REPO_ROOT = _SCRIPTS_DIR.parent + +ARCHIVE_ID = "cdba2c07-daff-4f60-ae96-12452b3a5fbb" +ARCHIVE_REL_PATH = Path( + "data/coscientists/openscientist/cdba2c07-daff-4f60-ae96-12452b3a5fbb_artifacts.zip" +) +ARCHIVE_SHA256 = "9fc5352284b7ffa7763c633a3eb4d3d0db96b8c25a1be9bcea361722d2c53054" + +MTZ_MEMBER = "data/1sar.mtz" +MTZ_SHA256 = "f36d5fe685a3e524e8809d17d19ce12073acdf339eba2cfd3b0347a818850ab2" +MTZ_SUBJECT_REF = f"mtz:sha256:{MTZ_SHA256}" + +OBSERVATION_LABELS: tuple[str, str, str] = ("F-obs", "SIGF-obs", "R-free-flags") +PROJECTED_MTZ_COLUMNS: tuple[str, ...] = ( + "H", + "K", + "L", + "F-obs", + "SIGF-obs", + "R-free-flags", +) +FORBIDDEN_ARCHIVED_MTZ_COLUMNS: frozenset[str] = frozenset( + { + "F-obs-filtered", + "SIGF-obs-filtered", + "F-model", + "PHIF-model", + "2FOFCWT", + "PH2FOFCWT", + "2FOFCWT_no_fill", + "PH2FOFCWT_no_fill", + "FOFCWT", + "PHFOFCWT", + } +) + +DECLARED_FLAG_VALUES: frozenset[int] = frozenset({0, 1}) +WORK_FLAG_VALUE = 0 +FREE_FLAG_VALUE = 1 + +ALLOWED_SUBJECTS: dict[str, dict[str, str]] = { + "start": { + "subject_id": "start", + "stage": "start", + "member": "data/1sar.pdb", + "sha256": "6b34c5d0dec6647df20eae9021ff7d511d6dedc03b4349c0b634d8e5b1f869e4", + "subject_ref": f"artifact:{ARCHIVE_ID}#data/1sar.pdb", + }, + "R4": { + "subject_id": "R4", + "stage": "final", + "member": "data/1sar_final.pdb", + "sha256": "8cbc1650f8be5a3833f98833fa514ec7d490c2c3bb9aa930413abac05e2a510b", + "subject_ref": f"artifact:{ARCHIVE_ID}#data/1sar_final.pdb", + }, + "R5": { + "subject_id": "R5", + "stage": "intermediate", + "member": "data/1sar_round5.pdb", + "sha256": "25c140b46e18a1d1ec211d985868894f227c827ea0c76f7187d41e1f75c03733", + "subject_ref": f"artifact:{ARCHIVE_ID}#data/1sar_round5.pdb", + }, + "R6": { + "subject_id": "R6", + "stage": "intermediate", + "member": "data/1sar_round6.pdb", + "sha256": "5d01ce3645d1295a42f620987785fd8368441418e82a0b6d6992d7f79500c110", + "subject_ref": f"artifact:{ARCHIVE_ID}#data/1sar_round6.pdb", + }, +} + +FORBIDDEN_SUBJECTS: frozenset[str] = frozenset( + { + "R7", + "round7", + "1sar_round7", + "data/1sar_round7.pdb", + "deposited", + "1sar_deposited", + } +) + +PHENIX_VENDOR_SOURCE_RELPATHS: dict[str, Path] = { + "vendor_mmtbx_model_vs_data": Path("mmtbx/model_vs_data.py"), + "vendor_iotbx_extract_xtal_data": Path("iotbx/extract_xtal_data.py"), + "vendor_iotbx_reflection_file_utils": Path("iotbx/reflection_file_utils.py"), + "vendor_mmtbx_f_model": Path("mmtbx/f_model/f_model.py"), + "vendor_mmtbx_map_tools": Path("mmtbx/map_tools.py"), + "vendor_iotbx_mtz": Path("iotbx/mtz/__init__.py"), +} + + +def _sha256_bytes(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def _sha256_regular_file(path: Path) -> str: + st = os.lstat(path) + if stat.S_ISLNK(st.st_mode) or not stat.S_ISREG(st.st_mode): + raise ValueError(f"Regular non-symlink file required: {path}") + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def resolve_subject_spec(subject_id: str) -> dict[str, str]: + """Return a copy of the preregistered subject specification or fail closed.""" + if not isinstance(subject_id, str) or not subject_id: + raise ValueError("subject_id must be a non-empty string") + if subject_id in FORBIDDEN_SUBJECTS: + raise ValueError( + f"Subject {subject_id!r} is excluded from the 1SAR trajectory rerun contract" + ) + if subject_id not in ALLOWED_SUBJECTS: + allowed = ", ".join(sorted(ALLOWED_SUBJECTS)) + raise ValueError( + f"Unknown 1SAR subject {subject_id!r}; allowed subjects are: {allowed}" + ) + return dict(ALLOWED_SUBJECTS[subject_id]) + + +def _validate_archive_member_name(member_name: str) -> None: + if not isinstance(member_name, str) or not member_name: + raise ValueError("Archive member name must be a non-empty string") + if member_name.startswith("/") or "\\" in member_name or "\x00" in member_name: + raise ValueError(f"Unsafe archive member path: {member_name!r}") + parts = member_name.split("/") + if any(part in ("", ".", "..") for part in parts): + raise ValueError(f"Unsafe archive member path segments: {member_name!r}") + + +def read_verified_archive_member( + archive_path: Path, + member_name: str, + *, + expected_archive_sha256: str = ARCHIVE_SHA256, + expected_member_sha256: str, +) -> bytes: + """Safely read one exact member from a hash-verified ZIP archive in memory. + + Never calls ``extractall``; rejects symlinks, missing/duplicate central-directory + entries, directories, and archive or member SHA-256 mismatches. + """ + _validate_archive_member_name(member_name) + if not isinstance(expected_archive_sha256, str) or not expected_archive_sha256: + raise ValueError("expected_archive_sha256 must be a non-empty hex digest") + if not isinstance(expected_member_sha256, str) or not expected_member_sha256: + raise ValueError("expected_member_sha256 must be a non-empty hex digest") + + archive_candidate = Path(archive_path) + if not archive_candidate.exists() and not archive_candidate.is_symlink(): + raise FileNotFoundError(f"Archive not found: {archive_candidate}") + actual_archive_sha256 = _sha256_regular_file(archive_candidate) + if actual_archive_sha256 != expected_archive_sha256: + raise ValueError( + f"Archive SHA-256 mismatch for {archive_candidate}: " + f"expected {expected_archive_sha256}, got {actual_archive_sha256}" + ) + + with zipfile.ZipFile(archive_candidate, "r") as zf: + matches = [info for info in zf.infolist() if info.filename == member_name] + if not matches: + raise KeyError( + f"Member {member_name!r} not found in archive {archive_candidate}" + ) + if len(matches) != 1: + raise ValueError( + f"Archive {archive_candidate} contains {len(matches)} duplicate entries " + f"for {member_name!r}" + ) + info = matches[0] + if info.is_dir(): + raise ValueError( + f"Archive member {member_name!r} in {archive_candidate} is a directory" + ) + unix_mode = (info.external_attr >> 16) & 0o170000 + if unix_mode == stat.S_IFLNK: + raise ValueError( + f"Archive member {member_name!r} in {archive_candidate} is a symlink" + ) + data = zf.read(info.filename) + + actual_member_sha256 = _sha256_bytes(data) + if actual_member_sha256 != expected_member_sha256: + raise ValueError( + f"Archive member {member_name!r} SHA-256 mismatch: " + f"expected {expected_member_sha256}, got {actual_member_sha256}" + ) + return data + + +def prepare_fresh_evidence_root( + evidence_root: Path, *, repo_root: Path = REPO_ROOT +) -> Path: + """Create a fresh repository-local directory, rejecting symlinks and existing paths.""" + repo_candidate = Path(repo_root) + if repo_candidate.is_symlink(): + raise ValueError(f"Repository root must not be a symlink: {repo_candidate}") + resolved_repo = repo_candidate.resolve(strict=True) + if not resolved_repo.is_dir(): + raise ValueError(f"Repository root is not a directory: {resolved_repo}") + + raw_target = Path(evidence_root) + if not raw_target.is_absolute(): + raw_target = resolved_repo / raw_target + + # Normalize lexical "." and ".." without resolving symlinks along the suffix. + normalized_target = Path(os.path.abspath(os.fspath(raw_target))) + if ( + normalized_target == resolved_repo + or not normalized_target.is_relative_to(resolved_repo) + ): + raise ValueError( + f"Evidence root {evidence_root} must be a strict subdirectory inside " + f"repository root {resolved_repo}" + ) + + rel_parts = normalized_target.relative_to(resolved_repo).parts + current = resolved_repo + for index, part in enumerate(rel_parts): + current = current / part + is_last = index == len(rel_parts) - 1 + try: + st = os.lstat(current) + except FileNotFoundError: + continue + if stat.S_ISLNK(st.st_mode): + raise ValueError( + f"Evidence path component must not be a symlink: {current}" + ) + if is_last: + raise FileExistsError( + f"Fresh evidence root required; destination already exists: {current}" + ) + if not stat.S_ISDIR(st.st_mode): + raise ValueError( + f"Evidence ancestor is not a directory: {current}" + ) + + normalized_target.mkdir(parents=True, exist_ok=False) + return normalized_target + + +def _format_mtz_card(text: str) -> bytes: + if len(text) > 80: + raise ValueError(f"MTZ header card exceeds 80 characters: {text!r}") + return text.ljust(80).encode("ascii") + + +def _integral_value(value: float) -> int | None: + if not isinstance(value, (int, float)) or isinstance(value, bool): + return None + fval = float(value) + if not math.isfinite(fval) or fval != int(fval): + return None + return int(fval) + + +def _usable_observation(f_val: float, sigf_val: float) -> bool: + return ( + math.isfinite(f_val) + and math.isfinite(sigf_val) + and sigf_val > 0.0 + ) + + +def _inspect_mtz_layout( + mtz_bytes: bytes, labels: tuple[str, str, str] +) -> dict[str, Any]: + _require_supported_mtz(mtz_bytes) + header_byte_offset, headers = _mtz_headers(mtz_bytes) + context = mtz_crystallographic_context(mtz_bytes) + bindings = _column_bindings(mtz_bytes, labels) + count_rows = [line.split() for line in headers if line.startswith("NCOL ")] + if len(count_rows) != 1: + raise ValueError("MTZ requires exactly one NCOL header") + ncols, nrows, nbatches = map(int, count_rows[0][1:4]) + column_records = [ + line.split() for line in headers if line.startswith("COLUMN ") + ] + columns = [parts[1] for parts in column_records] + col_types = [parts[2] for parts in column_records] + if ( + ncols != len(columns) + or len(set(columns)) != ncols + or columns[:3] != ["H", "K", "L"] + or col_types[:3] != ["H", "H", "H"] + or nrows <= 0 + or nbatches != 0 + or header_byte_offset != 80 + 4 * ncols * nrows + ): + raise ValueError( + "MTZ table dimensions, column identities, or header offset are inconsistent" + ) + required_names = ("H", "K", "L", *labels) + col_indices = {name: columns.index(name) for name in required_names} + return { + "header_byte_offset": header_byte_offset, + "headers": headers, + "ncols": ncols, + "nrows": nrows, + "columns": columns, + "col_indices": col_indices, + "bindings": bindings, + "context": context, + } + + +def project_observation_mtz( + mtz_bytes: bytes, + *, + labels: tuple[str, str, str] = OBSERVATION_LABELS, + flag_values: set[int] | frozenset[int] = DECLARED_FLAG_VALUES, + free_value: int = FREE_FLAG_VALUE, +) -> tuple[bytes, dict[str, Any]]: + """Project an MTZ byte stream to ``H, K, L, F-obs, SIGF-obs, R-free-flags``. + + Preserves all raw rows in exact row order, exact 4-byte IEEE-754 words, cell, + symmetry, resolution, ``VALM NAN``, and dataset metadata for the retained + observation dataset while stripping all archived model and map columns. + Verifies retention via :func:`t03_reflection_retention.audit_mtz_retention`. + """ + layout = _inspect_mtz_layout(mtz_bytes, labels) + header = layout["headers"] + header_byte_offset = layout["header_byte_offset"] + ncols = layout["ncols"] + nrows = layout["nrows"] + input_context = layout["context"] + f_label, sigf_label, flag_label = labels + retained_names = ("H", "K", "L", f_label, sigf_label, flag_label) + if retained_names != PROJECTED_MTZ_COLUMNS: + raise ValueError( + f"Projected column order {retained_names} does not match " + f"required {PROJECTED_MTZ_COLUMNS}" + ) + + col_indices = [layout["col_indices"][name] for name in retained_names] + data_start = 80 + row_stride = ncols * 4 + expected_data_end = data_start + nrows * row_stride + if expected_data_end != header_byte_offset: + raise ValueError("Input MTZ data block does not end at header byte offset") + + projected_chunks: list[bytes] = [] + for row_idx in range(nrows): + row_base = data_start + row_idx * row_stride + for col_idx in col_indices: + word_start = row_base + col_idx * 4 + projected_chunks.append(mtz_bytes[word_start : word_start + 4]) + projected_data = b"".join(projected_chunks) + + # Identify the COLUMN cards and dataset IDs for the six retained columns. + column_cards_by_name: dict[str, str] = {} + retained_dataset_ids: set[str] = set() + for rec in header: + if rec.startswith("COLUMN "): + tokens = rec.split() + if len(tokens) == 6 and tokens[1] in retained_names: + column_cards_by_name[tokens[1]] = rec + retained_dataset_ids.add(tokens[5]) + if set(column_cards_by_name) != set(retained_names): + raise ValueError( + f"Could not locate all retained COLUMN cards {retained_names} in MTZ header" + ) + + dataset_card_prefixes = ("PROJECT", "CRYSTAL", "DATASET", "DCELL", "DWAVEL", "DWAV") + projected_cards: list[bytes] = [] + emitted_columns = False + for rec in header: + stripped = rec.strip() + if not stripped: + continue + tokens = stripped.split() + keyword = tokens[0] + if keyword == "NCOL": + projected_cards.append( + _format_mtz_card(f"NCOL {len(retained_names):8d} {nrows:12d} {0:8d}") + ) + elif keyword == "NDIF" and retained_dataset_ids: + projected_cards.append( + _format_mtz_card(f"NDIF {len(retained_dataset_ids):8d}") + ) + elif keyword in dataset_card_prefixes and retained_dataset_ids: + if len(tokens) >= 2 and tokens[1] in retained_dataset_ids: + projected_cards.append(_format_mtz_card(rec)) + elif keyword == "COLUMN": + if not emitted_columns: + for col_name in retained_names: + projected_cards.append(_format_mtz_card(column_cards_by_name[col_name])) + emitted_columns = True + else: + projected_cards.append(_format_mtz_card(rec)) + projected_cards.append(_format_mtz_card("END")) + + projected_header_bytes = b"".join(projected_cards) + new_header_offset = (80 + len(projected_data)) // 4 + 1 + projected_bytes = ( + b"MTZ " + + struct.pack(" dict[str, Any]: + """Build the exact raw and input-policy-eligible reflection ledger from an MTZ. + + Input-policy eligibility requires finite ``F-obs`` (including zero or negative + ``F-obs``) and finite positive ``SIGF-obs``. Every eligible observation must + carry a finite integral ``R-free-flags`` value in ``flag_values``. Excluded + observations (e.g. ``VALM NAN`` padding rows) are retained in ``ineligible_rows`` + and ``raw_rows`` with their exact 4-byte IEEE-754 words; if their flag is NaN, + ``r_free_flag`` is ``None`` and ``partition`` is ``"unassigned"``. Duplicate + HKLs, non-integral Miller indices, or non-integral/undeclared finite + ``R-free-flags`` fail closed. + """ + if free_value not in flag_values: + raise ValueError(f"free_value {free_value} not in declared flag_values {flag_values}") + + layout = _inspect_mtz_layout(mtz_bytes, labels) + ncols = layout["ncols"] + nrows = layout["nrows"] + context = layout["context"] + col_indices = layout["col_indices"] + f_label, sigf_label, flag_label = labels + idx_h = col_indices["H"] + idx_k = col_indices["K"] + idx_l = col_indices["L"] + idx_f = col_indices[f_label] + idx_sigf = col_indices[sigf_label] + idx_flag = col_indices[flag_label] + + raw_floats = struct.unpack_from(f"<{nrows * ncols}f", mtz_bytes, 80) + raw_words = struct.unpack_from(f"<{nrows * ncols}I", mtz_bytes, 80) + + seen_hkls: set[tuple[int, int, int]] = set() + raw_rows: list[dict[str, Any]] = [] + eligible_by_hkl: dict[str, dict[str, Any]] = {} + ineligible_rows: list[dict[str, Any]] = [] + + raw_work_count = 0 + raw_free_count = 0 + raw_unassigned_count = 0 + eligible_work_count = 0 + eligible_free_count = 0 + eligible_zero_f_count = 0 + eligible_negative_f_count = 0 + ineligible_nonfinite_f_count = 0 + ineligible_nonpositive_or_nonfinite_sigf_count = 0 + ineligible_unassigned_flag_count = 0 + + for row_index in range(nrows): + base = row_index * ncols + h_val = raw_floats[base + idx_h] + k_val = raw_floats[base + idx_k] + l_val = raw_floats[base + idx_l] + h_i = _integral_value(h_val) + k_i = _integral_value(k_val) + l_i = _integral_value(l_val) + if h_i is None or k_i is None or l_i is None: + raise ValueError( + f"Non-integral Miller index at row {row_index}: {(h_val, k_val, l_val)}" + ) + hkl = (h_i, k_i, l_i) + if hkl in seen_hkls: + raise ValueError(f"Duplicate Miller index in MTZ at row {row_index}: {hkl}") + seen_hkls.add(hkl) + + f_val = float(raw_floats[base + idx_f]) + sigf_val = float(raw_floats[base + idx_sigf]) + flag_raw = float(raw_floats[base + idx_flag]) + f_word = f"{raw_words[base + idx_f]:08x}" + sigf_word = f"{raw_words[base + idx_sigf]:08x}" + flag_word = f"{raw_words[base + idx_flag]:08x}" + + eligible = _usable_observation(f_val, sigf_val) + flag_int = _integral_value(flag_raw) + hkl_key = f"{h_i},{k_i},{l_i}" + + if eligible: + if flag_int is None or flag_int not in flag_values: + raise ValueError( + f"Invalid R-free flag value {flag_raw!r} at HKL {hkl} (row {row_index}); " + f"declared values are {sorted(flag_values)}" + ) + is_free: bool | None = flag_int == free_value + r_free_flag: int | None = flag_int + partition = "free" if is_free else "work" + if is_free: + raw_free_count += 1 + eligible_free_count += 1 + else: + raw_work_count += 1 + eligible_work_count += 1 + + eligibility_status = "eligible" + if f_val == 0.0: + eligible_zero_f_count += 1 + elif f_val < 0.0: + eligible_negative_f_count += 1 + + eligible_by_hkl[hkl_key] = { + "row_index": row_index, + "hkl": [h_i, k_i, l_i], + "f_obs": f_val, + "sigf_obs": sigf_val, + "f_obs_word_hex": f_word, + "sigf_obs_word_hex": sigf_word, + "r_free_flag": flag_int, + "r_free_flag_word_hex": flag_word, + "is_free": bool(is_free), + "partition": partition, + "is_zero_f": f_val == 0.0, + "is_negative_f": f_val < 0.0, + } + else: + if flag_int is not None and flag_int in flag_values: + r_free_flag = flag_int + is_free = flag_int == free_value + partition = "free" if is_free else "work" + if is_free: + raw_free_count += 1 + else: + raw_work_count += 1 + elif not math.isfinite(flag_raw): + r_free_flag = None + is_free = None + partition = "unassigned" + raw_unassigned_count += 1 + ineligible_unassigned_flag_count += 1 + else: + raise ValueError( + f"Invalid R-free flag value {flag_raw!r} at HKL {hkl} (row {row_index}); " + f"declared values are {sorted(flag_values)}" + ) + + if not math.isfinite(f_val): + eligibility_status = "excluded_nonfinite_f" + ineligible_nonfinite_f_count += 1 + else: + eligibility_status = "excluded_nonpositive_or_nonfinite_sigf" + ineligible_nonpositive_or_nonfinite_sigf_count += 1 + ineligible_rows.append( + { + "row_index": row_index, + "hkl": [h_i, k_i, l_i], + "f_obs": None if not math.isfinite(f_val) else f_val, + "sigf_obs": None if not math.isfinite(sigf_val) else sigf_val, + "f_obs_word_hex": f_word, + "sigf_obs_word_hex": sigf_word, + "r_free_flag": r_free_flag, + "r_free_flag_word_hex": flag_word, + "is_free": is_free, + "partition": partition, + "reason": eligibility_status, + } + ) + + raw_rows.append( + { + "row_index": row_index, + "hkl": [h_i, k_i, l_i], + "f_obs": None if not math.isfinite(f_val) else f_val, + "sigf_obs": None if not math.isfinite(sigf_val) else sigf_val, + "f_obs_word_hex": f_word, + "sigf_obs_word_hex": sigf_word, + "r_free_flag": r_free_flag, + "r_free_flag_word_hex": flag_word, + "is_free": is_free, + "partition": partition, + "eligible": eligible, + "eligibility_status": eligibility_status, + } + ) + + if eligible_work_count == 0 or eligible_free_count == 0: + raise ValueError( + f"Input MTZ must contain non-empty eligible work and free sets; " + f"got work={eligible_work_count}, free={eligible_free_count}" + ) + + return { + "mtz_sha256": _sha256_bytes(mtz_bytes), + "labels": { + "f_obs": f_label, + "sigf_obs": sigf_label, + "r_free_flags": flag_label, + }, + "declared_flag_values": sorted(flag_values), + "work_flag_value": WORK_FLAG_VALUE, + "free_flag_value": free_value, + "crystallographic_context": copy.deepcopy(context), + "counts": { + "raw_total": nrows, + "raw_work": raw_work_count, + "raw_free": raw_free_count, + "raw_unassigned": raw_unassigned_count, + "eligible_total": len(eligible_by_hkl), + "eligible_work": eligible_work_count, + "eligible_free": eligible_free_count, + "eligible_zero_f": eligible_zero_f_count, + "eligible_negative_f": eligible_negative_f_count, + "ineligible_total": len(ineligible_rows), + "ineligible_nonfinite_f": ineligible_nonfinite_f_count, + "ineligible_nonpositive_or_nonfinite_sigf": ( + ineligible_nonpositive_or_nonfinite_sigf_count + ), + "ineligible_unassigned_flag": ineligible_unassigned_flag_count, + }, + "eligible_by_hkl": eligible_by_hkl, + "ineligible_rows": ineligible_rows, + "raw_rows": raw_rows, + } + + +def stage_subject_inputs( + evidence_root: Path, + subject_id: str = "R4", + *, + repo_root: Path = REPO_ROOT, + archive_path: Path | None = None, +) -> dict[str, Any]: + """Stage hash-verified PDB and MTZ inputs into a fresh repository-local root.""" + subject_spec = resolve_subject_spec(subject_id) + fresh_root = prepare_fresh_evidence_root(evidence_root, repo_root=repo_root) + resolved_archive = ( + Path(repo_root).resolve(strict=True) / ARCHIVE_REL_PATH + if archive_path is None + else Path(archive_path) + ) + + pdb_bytes = read_verified_archive_member( + resolved_archive, + subject_spec["member"], + expected_archive_sha256=ARCHIVE_SHA256, + expected_member_sha256=subject_spec["sha256"], + ) + mtz_bytes = read_verified_archive_member( + resolved_archive, + MTZ_MEMBER, + expected_archive_sha256=ARCHIVE_SHA256, + expected_member_sha256=MTZ_SHA256, + ) + + projected_mtz_bytes, projection_summary = project_observation_mtz(mtz_bytes) + input_ledger = build_input_hkl_ledger(projected_mtz_bytes) + + inputs_dir = fresh_root / "inputs" + inputs_dir.mkdir(parents=True, exist_ok=False) + original_pdb_path = inputs_dir / "original_model.pdb" + original_mtz_path = inputs_dir / "original_data.mtz" + projected_mtz_path = inputs_dir / "projected_observations.mtz" + input_ledger_path = inputs_dir / "input_hkl_ledger.json" + admission_json_path = inputs_dir / "input_admission.json" + + original_pdb_path.write_bytes(pdb_bytes) + original_mtz_path.write_bytes(mtz_bytes) + projected_mtz_path.write_bytes(projected_mtz_bytes) + input_ledger_path.write_text( + json.dumps(input_ledger, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + + admission_record = { + "schema_version": 1, + "archive": { + "archive_id": ARCHIVE_ID, + "archive_path": str(resolved_archive), + "archive_sha256": ARCHIVE_SHA256, + }, + "subject": subject_spec, + "mtz": { + "member": MTZ_MEMBER, + "sha256": MTZ_SHA256, + "subject_ref": MTZ_SUBJECT_REF, + }, + "staged_files": { + "original_model_pdb": str(original_pdb_path), + "original_model_pdb_sha256": _sha256_regular_file(original_pdb_path), + "original_data_mtz": str(original_mtz_path), + "original_data_mtz_sha256": _sha256_regular_file(original_mtz_path), + "projected_observations_mtz": str(projected_mtz_path), + "projected_observations_mtz_sha256": _sha256_regular_file( + projected_mtz_path + ), + "input_hkl_ledger_json": str(input_ledger_path), + "input_hkl_ledger_json_sha256": _sha256_regular_file(input_ledger_path), + }, + "projection": projection_summary, + "input_ledger_counts": input_ledger["counts"], + } + admission_json_path.write_text( + json.dumps(admission_record, indent=2, sort_keys=True) + "\n", + encoding="utf-8", + ) + return { + "evidence_root": fresh_root, + "inputs_dir": inputs_dir, + "original_pdb_path": original_pdb_path, + "original_mtz_path": original_mtz_path, + "projected_mtz_path": projected_mtz_path, + "input_ledger_path": input_ledger_path, + "admission_json_path": admission_json_path, + "admission_record": admission_record, + "input_ledger": input_ledger, + } + + +def validate_measured_phenix_build( + build_info: Mapping[str, Any], + *, + expected_version: str = toolchain.PHENIX_VERSION, +) -> dict[str, Any]: + """Validate that a PHENIX build record satisfies #957 and matches ``expected_version``.""" + if not isinstance(build_info, Mapping): + raise ValueError("PHENIX build_info must be a mapping") + if not isinstance(expected_version, str) or not expected_version: + raise ValueError("expected_version must be a non-empty string") + if ( + not refinement_cache._build_is_measured(build_info) + or build_info.get("reported_version_source") != "vendor_setup_environment" + ): + raise ValueError( + "PHENIX vendor-setup build measurement failed #957 admission gate" + ) + version_probe = build_info.get("version_probe") + if not isinstance(version_probe, Mapping): + raise ValueError("Measured PHENIX build missing version_probe mapping") + resolved_prefix = version_probe.get("resolved_prefix") + setup_resolved = version_probe.get("setup_resolved_path") + dispatcher_resolved = version_probe.get("resolved_path") + if ( + not isinstance(resolved_prefix, str) + or not resolved_prefix + or not Path(resolved_prefix).is_absolute() + ): + raise ValueError( + f"Measured PHENIX build missing absolute resolved_prefix: {resolved_prefix!r}" + ) + if ( + not isinstance(setup_resolved, str) + or not isinstance(dispatcher_resolved, str) + or not Path(setup_resolved).is_relative_to(Path(resolved_prefix)) + or not Path(dispatcher_resolved).is_relative_to(Path(resolved_prefix)) + ): + raise ValueError( + "Measured PHENIX setup/dispatcher paths do not belong to resolved_prefix" + ) + + top_parsed = build_info.get("parsed_build") + probe_parsed = version_probe.get("parsed_build") + setup_parsed = version_probe.get("setup_parsed_build") + banner_parsed = toolchain.parse_phenix_build( + str(build_info.get("reported_version", "")) + ) + if ( + top_parsed != expected_version + or probe_parsed != expected_version + or setup_parsed != expected_version + or banner_parsed != expected_version + ): + raise ValueError( + f"PHENIX build version mismatch: expected {expected_version!r}, got " + f"parsed_build={top_parsed!r}, probe.parsed_build={probe_parsed!r}, " + f"probe.setup_parsed_build={setup_parsed!r}, banner={banner_parsed!r}" + ) + + normalized_build = json.loads( + json.dumps(dict(build_info), sort_keys=True, allow_nan=False) + ) + return { + "expected_version": expected_version, + "parsed_build": expected_version, + "resolved_prefix": str(Path(resolved_prefix)), + "setup_resolved_path": str(Path(setup_resolved)), + "dispatcher_resolved_path": str(Path(dispatcher_resolved)), + "build_info": normalized_build, + } + + +def inspect_map_mtz_bytes( + mtz_bytes: bytes, + *, + expected_columns: Sequence[str], + expected_column_types: Sequence[str], +) -> dict[str, Any]: + """Parse and validate a binary map-coefficient MTZ byte stream in pure Python.""" + _require_supported_mtz(mtz_bytes) + header_byte_offset, headers = _mtz_headers(mtz_bytes) + context = mtz_crystallographic_context(mtz_bytes) + count_rows = [line.split() for line in headers if line.startswith("NCOL ")] + if len(count_rows) != 1: + raise ValueError("Map MTZ requires exactly one NCOL header") + ncols, nrows, nbatches = map(int, count_rows[0][1:4]) + column_records = [ + line.split() for line in headers if line.startswith("COLUMN ") + ] + columns = [parts[1] for parts in column_records] + col_types = [parts[2] for parts in column_records] + if ( + ncols != len(columns) + or len(set(columns)) != ncols + or nrows <= 0 + or nbatches != 0 + or header_byte_offset != 80 + 4 * ncols * nrows + ): + raise ValueError( + "Map MTZ table dimensions, column identities, or header offset are inconsistent" + ) + if columns != list(expected_columns): + raise ValueError( + f"Map MTZ column labels {columns} do not match expected {list(expected_columns)}" + ) + if col_types != list(expected_column_types): + raise ValueError( + f"Map MTZ column types {col_types} do not match expected {list(expected_column_types)}" + ) + + raw_floats = struct.unpack_from(f"<{nrows * ncols}f", mtz_bytes, 80) + raw_words = struct.unpack_from(f"<{nrows * ncols}I", mtz_bytes, 80) + rows_by_hkl: dict[str, dict[str, Any]] = {} + for row_index in range(nrows): + base = row_index * ncols + h_i = _integral_value(raw_floats[base]) + k_i = _integral_value(raw_floats[base + 1]) + l_i = _integral_value(raw_floats[base + 2]) + if h_i is None or k_i is None or l_i is None: + raise ValueError( + f"Non-integral Miller index in map MTZ at row {row_index}: " + f"{(raw_floats[base], raw_floats[base + 1], raw_floats[base + 2])}" + ) + key = f"{h_i},{k_i},{l_i}" + if key in rows_by_hkl: + raise ValueError(f"Duplicate Miller index in map MTZ at row {row_index}: {key}") + values: dict[str, float] = {} + words: dict[str, str] = {} + for col_idx in range(3, ncols): + col_name = columns[col_idx] + val = float(raw_floats[base + col_idx]) + if not math.isfinite(val): + raise ValueError( + f"Non-finite value {val!r} in map MTZ column {col_name!r} at HKL {key}" + ) + values[col_name] = val + words[col_name] = f"{raw_words[base + col_idx]:08x}" + rows_by_hkl[key] = { + "row_index": row_index, + "hkl": [h_i, k_i, l_i], + "values": values, + "words": words, + } + + return { + "sha256": _sha256_bytes(mtz_bytes), + "ncols": ncols, + "nrows": nrows, + "columns": columns, + "column_types": col_types, + "crystallographic_context": context, + "rows_by_hkl": rows_by_hkl, + } + + +def _circular_phase_diff_deg(phase_a: float, phase_b: float) -> float: + return abs((phase_a - phase_b + 180.0) % 360.0 - 180.0) + + +def resolve_phenix_vendor_source_files(resolved_prefix: Path) -> dict[str, Path]: + """Locate and verify the installed PHENIX Python source modules under ``resolved_prefix``.""" + prefix = Path(resolved_prefix) + if prefix.is_symlink(): + raise ValueError(f"PHENIX installation prefix must not be a symlink: {prefix}") + strict_prefix = prefix.resolve(strict=True) + lib_dir = strict_prefix / "lib" + if not lib_dir.is_dir(): + raise FileNotFoundError(f"PHENIX lib directory not found: {lib_dir}") + + site_packages_candidates = sorted( + path + for path in lib_dir.glob("python*/site-packages") + if path.is_dir() and not path.is_symlink() + ) + if len(site_packages_candidates) != 1: + raise ValueError( + f"Expected exactly one python*/site-packages directory under {lib_dir}; " + f"found {[str(p) for p in site_packages_candidates]}" + ) + site_packages = site_packages_candidates[0].resolve(strict=True) + if not site_packages.is_relative_to(strict_prefix): + raise ValueError( + f"PHENIX site-packages escapes installation prefix: {site_packages}" + ) + + resolved_sources: dict[str, Path] = {} + for key, rel_path in PHENIX_VENDOR_SOURCE_RELPATHS.items(): + candidate = site_packages / rel_path + if candidate.is_symlink(): + raise ValueError(f"PHENIX vendor source must not be a symlink: {candidate}") + resolved_candidate = candidate.resolve(strict=True) + if not resolved_candidate.is_relative_to(strict_prefix): + raise ValueError( + f"PHENIX vendor source escapes installation prefix: {resolved_candidate}" + ) + _sha256_regular_file(resolved_candidate) + resolved_sources[key] = resolved_candidate + return resolved_sources + + +def _hkl_key(hkl: list[int] | tuple[int, int, int]) -> str: + if ( + not isinstance(hkl, (list, tuple)) + or len(hkl) != 3 + or any(type(v) is not int for v in hkl) + ): + raise ValueError(f"Invalid HKL triplet: {hkl!r}") + return f"{hkl[0]},{hkl[1]},{hkl[2]}" + + +def _verify_finite_positive(value: Any, label: str) -> float: + if type(value) not in (int, float) or not math.isfinite(float(value)): + raise ValueError(f"{label} must be a finite number, got {value!r}") + fval = float(value) + if fval <= 0.0: + raise ValueError(f"{label} must be positive, got {fval!r}") + return fval + + +def _verify_finite_nonnegative(value: Any, label: str) -> float: + if type(value) not in (int, float) or not math.isfinite(float(value)): + raise ValueError(f"{label} must be a finite number, got {value!r}") + fval = float(value) + if fval < 0.0: + raise ValueError(f"{label} must be non-negative, got {fval!r}") + return fval + + +def _verify_finite(value: Any, label: str) -> float: + if type(value) not in (int, float) or not math.isfinite(float(value)): + raise ValueError(f"{label} must be a finite number, got {value!r}") + return float(value) + + +def _verify_int_count(value: Any, label: str) -> int: + if type(value) is not int or value <= 0: + raise ValueError(f"{label} must be a positive int, got {value!r}") + return value + + +def _verify_nonnegative_int_count(value: Any, label: str) -> int: + if type(value) is not int or value < 0: + raise ValueError(f"{label} must be a non-negative int, got {value!r}") + return value + + +EXPECTED_RESOLUTION_STAGES: tuple[str, str, str, str] = ( + "extracted_pre_fmodel", + "final_effective", + "actual_work", + "actual_free", +) + +_DECIMAL_CELL_TOKEN_RE = re.compile( + r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?" +) + + +def _normalize_unit_cell_token(value: Any, label: str) -> float: + """Normalize an admitted unit-cell parameter token (exact decimal string or int/float).""" + if type(value) in (int, float): + return _verify_finite_positive(value, label) + if type(value) is not str: + raise ValueError(f"{label} must be a finite number, got {value!r}") + if not value or value != value.strip() or "_" in value or not value.isascii(): + raise ValueError( + f"{label} must be a valid decimal unit-cell token, got {value!r}" + ) + try: + dec = Decimal(value) + except InvalidOperation as exc: + raise ValueError( + f"{label} must be a valid decimal unit-cell token, got {value!r}" + ) from exc + if not dec.is_finite(): + raise ValueError(f"{label} must be a finite number, got {value!r}") + if _DECIMAL_CELL_TOKEN_RE.fullmatch(value) is None: + raise ValueError( + f"{label} must be a valid decimal unit-cell token, got {value!r}" + ) + fval = float(dec) + if not math.isfinite(fval): + raise ValueError(f"{label} must be a finite number, got {value!r}") + if dec <= 0 or fval <= 0.0: + raise ValueError(f"{label} must be positive, got {fval!r}") + return fval + + +def validate_unit_cell_parameters( + cell: Any, *, label: str = "cell_a_b_c_alpha_beta_gamma" +) -> tuple[float, float, float, float, float, float]: + """Validate crystallographic unit cell ``(a, b, c, alpha, beta, gamma)``.""" + if not isinstance(cell, (list, tuple)) or len(cell) != 6: + raise ValueError( + f"{label} must be a 6-element sequence (a, b, c, alpha, beta, gamma), " + f"got {cell!r}" + ) + a = _normalize_unit_cell_token(cell[0], f"{label}[0] (a)") + b = _normalize_unit_cell_token(cell[1], f"{label}[1] (b)") + c = _normalize_unit_cell_token(cell[2], f"{label}[2] (c)") + alpha = _normalize_unit_cell_token(cell[3], f"{label}[3] (alpha)") + beta = _normalize_unit_cell_token(cell[4], f"{label}[4] (beta)") + gamma = _normalize_unit_cell_token(cell[5], f"{label}[5] (gamma)") + for angle_name, angle_val in ( + ("alpha", alpha), + ("beta", beta), + ("gamma", gamma), + ): + if angle_val >= 180.0: + raise ValueError( + f"{label} angle {angle_name} must be in (0, 180) degrees, got {angle_val!r}" + ) + if ( + alpha + beta + gamma >= 360.0 + or alpha + beta <= gamma + or alpha + gamma <= beta + or beta + gamma <= alpha + ): + raise ValueError( + f"{label} angles ({alpha}, {beta}, {gamma}) violate crystallographic " + "cone inequalities" + ) + cos_a = 0.0 if alpha == 90.0 else math.cos(math.radians(alpha)) + cos_b = 0.0 if beta == 90.0 else math.cos(math.radians(beta)) + cos_g = 0.0 if gamma == 90.0 else math.cos(math.radians(gamma)) + omega = ( + 1.0 + - cos_a * cos_a + - cos_b * cos_b + - cos_g * cos_g + + 2.0 * cos_a * cos_b * cos_g + ) + if not math.isfinite(omega) or omega <= 1e-12: + raise ValueError( + f"{label} defines a singular or non-positive metric tensor (omega={omega!r})" + ) + return (a, b, c, alpha, beta, gamma) + + +def ccp4_mtz_float32_unit_cell( + cell: Any, *, label: str = "cell_a_b_c_alpha_beta_gamma" +) -> tuple[float, float, float, float, float, float]: + """Return the installed CCP4/iotbx MTZ reader's float32->float64 unit-cell conversion. + + In PHENIX 2.0-5936, ``ccp4io/libccp4/ccp4/mtzdata.h:86`` and + ``cmtzlib.c:476,549-551`` store MTZ ``CELL``/``DCELL`` header tokens in + IEEE-754 binary32 (``float cell[6]``), and ``iotbx/mtz/crystal.cpp:61-67`` + promotes those 32-bit floats to ``double`` when constructing Miller-array + crystal symmetries. + """ + validated = validate_unit_cell_parameters(cell, label=label) + f32_tuple = struct.unpack("<6f", struct.pack("<6f", *validated)) + return validate_unit_cell_parameters( + f32_tuple, label=f"{label} (ccp4_mtz_float32)" + ) + + +def reciprocal_metric_tensor_from_cell( + cell: Any, *, label: str = "cell_a_b_c_alpha_beta_gamma" +) -> tuple[ + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], +]: + """Compute the inverse direct metric tensor ``G^-1`` for ``d = 1 / sqrt(h^T G^-1 h)``.""" + a, b, c, alpha, beta, gamma = validate_unit_cell_parameters( + cell, label=label + ) + cos_a = 0.0 if alpha == 90.0 else math.cos(math.radians(alpha)) + cos_b = 0.0 if beta == 90.0 else math.cos(math.radians(beta)) + cos_g = 0.0 if gamma == 90.0 else math.cos(math.radians(gamma)) + g11 = a * a + g22 = b * b + g33 = c * c + g12 = a * b * cos_g + g13 = a * c * cos_b + g23 = b * c * cos_a + + c11 = g22 * g33 - g23 * g23 + c22 = g11 * g33 - g13 * g13 + c33 = g11 * g22 - g12 * g12 + c12 = g13 * g23 - g12 * g33 + c13 = g12 * g23 - g13 * g22 + c23 = g12 * g13 - g11 * g23 + det_g = g11 * c11 + g12 * c12 + g13 * c13 + if not math.isfinite(det_g) or det_g <= 0.0: + raise ValueError( + f"{label} direct metric tensor determinant must be positive, got {det_g!r}" + ) + gi11 = c11 / det_g + gi22 = c22 / det_g + gi33 = c33 / det_g + gi12 = c12 / det_g + gi13 = c13 / det_g + gi23 = c23 / det_g + return ( + (gi11, gi12, gi13), + (gi12, gi22, gi23), + (gi13, gi23, gi33), + ) + + +def compute_hkl_d_spacing( + hkl: Any, + cell: Any, + *, + g_inv: tuple[ + tuple[float, float, float], + tuple[float, float, float], + tuple[float, float, float], + ] + | None = None, +) -> float: + """Compute crystallographic d-spacing ``d = 1 / sqrt(h^T G^-1 h)`` for a Miller index.""" + _hkl_key(hkl) + h, k, l = int(hkl[0]), int(hkl[1]), int(hkl[2]) + if h == 0 and k == 0 and l == 0: + raise ValueError("Miller index (0, 0, 0) has undefined d-spacing") + if g_inv is None: + g_inv = reciprocal_metric_tensor_from_cell(cell) + gi11, gi12, gi13 = g_inv[0] + _, gi22, gi23 = g_inv[1] + _, _, gi33 = g_inv[2] + inv_d_sq = ( + gi11 * h * h + + gi22 * k * k + + gi33 * l * l + + 2.0 * (gi12 * h * k + gi13 * h * l + gi23 * k * l) + ) + if not math.isfinite(inv_d_sq) or inv_d_sq <= 0.0: + raise ValueError( + f"Non-positive reciprocal metric norm for HKL {(h, k, l)!r}: {inv_d_sq!r}" + ) + return 1.0 / math.sqrt(inv_d_sq) + + +def compute_stage_resolution_range( + hkls: Sequence[Any], + cell: Any, + *, + stage_label: str = "stage", +) -> dict[str, float]: + """Recompute ``{"d_max": ..., "d_min": ...}`` from a stage's reflection HKLs and cell.""" + if not isinstance(hkls, (list, tuple)) or not hkls: + raise ValueError( + f"{stage_label} reflection list must be non-empty for resolution range computation" + ) + g_inv = reciprocal_metric_tensor_from_cell(cell) + d_values = [compute_hkl_d_spacing(hkl, cell, g_inv=g_inv) for hkl in hkls] + return { + "d_max": max(d_values), + "d_min": min(d_values), + } + + +SYMMETRY_OPERATOR_NORMALIZATION_POLICY = ( + "cctbx_rt_mx_as_xyz_to_mtz_symm_uppercase_v1" +) +_SYMMETRY_COMPONENT_RE = re.compile( + r"^(?P-?[XYZ])(?P[+-](?P[1-9]\d*)/(?P[1-9]\d*))?$" +) +_SPACE_GROUP_SYMBOL_AND_NUMBER_RE = re.compile( + r"^(?P[A-Za-z0-9_/\-\s]+?)\s+\(No\.\s*(?P[1-9]\d*)\)$" +) + + +def normalize_symmetry_operator_xyz( + operator: Any, + *, + label: str = "operator_xyz", +) -> str: + """Normalize an inspected cctbx ``rt_mx.as_xyz()`` or MTZ ``SYMM`` operator string. + + Policy ``cctbx_rt_mx_as_xyz_to_mtz_symm_uppercase_v1``: + - Accepts only the inspected ``cctbx.sgtbx.rt_mx.as_xyz(decimal=False, + t_first=False, symbol_letters="xyz", separator=",")`` and MTZ ``SYMM`` + 3-component axial-first rational grammar (e.g. ``x,y,z``, + ``-x+1/2,-y,z+1/2``, ``X,Y,Z``). + - Removes ASCII spaces and uppercases ``xyz -> XYZ``. + - Requires 3 comma-separated components, each with a signed single axis + term (``X``, ``Y``, ``Z``, ``-X``, ``-Y``, ``-Z``; no leading ``+``) and + an optional reduced proper rational translation ``[+-]n/d`` with + ``0 < n < d`` and ``gcd(n, d) == 1``, covering all three axes ``X, Y, Z``. + - Fails closed on unsupported alternate spellings (decimal translations, + translation-first order, unreduced fractions, non-XYZ variable symbols, + or control characters). + """ + if ( + type(operator) is not str + or not operator.strip() + or not operator.isascii() + or any(ord(ch) < 32 or ord(ch) > 126 for ch in operator) + ): + raise ValueError( + f"{label} must be a non-empty printable ASCII operator string, got {operator!r}" + ) + compact = operator.replace(" ", "").upper() + components = compact.split(",") + if len(components) != 3: + raise ValueError( + f"{label} must have 3 comma-separated XYZ components, got {operator!r}" + ) + for comp in components: + m = _SYMMETRY_COMPONENT_RE.fullmatch(comp) + if m is None: + raise ValueError( + f"{label} uses unsupported or malformed symmetry operator spelling {operator!r}" + ) + if m.group("trans") is not None: + num = int(m.group("num")) + den = int(m.group("den")) + if not (0 < num < den) or math.gcd(num, den) != 1: + raise ValueError( + f"{label} has non-canonical or unreduced translation in {operator!r}" + ) + if sorted(re.findall(r"[XYZ]", compact)) != ["X", "Y", "Z"]: + raise ValueError( + f"{label} must reference each axis X, Y, Z exactly once, got {operator!r}" + ) + return compact + + +def validate_runtime_space_group( + crystal_symmetry: Mapping[str, Any], + input_symmetry: Any, + *, + maps_symmetry: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Validate runtime space-group number, operators, and display symbol against input/map MTZ symmetry.""" + if not isinstance(crystal_symmetry, Mapping): + raise ValueError("phenix_payload missing crystal_symmetry section") + if not isinstance(input_symmetry, Mapping): + raise ValueError( + "input_ledger.crystallographic_context missing symmetry mapping" + ) + + space_group_symbol = crystal_symmetry.get("space_group_symbol") + if type(space_group_symbol) is not str or not space_group_symbol.strip(): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_symbol must be a non-empty string" + ) + if ( + space_group_symbol != space_group_symbol.strip() + or not space_group_symbol.isascii() + or any(ord(ch) < 32 or ord(ch) > 126 for ch in space_group_symbol) + ): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_symbol must be trimmed printable ASCII, " + f"got {space_group_symbol!r}" + ) + + raw_sg_number = crystal_symmetry.get("space_group_number") + if type(raw_sg_number) is not int or not (1 <= raw_sg_number <= 230): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_number must be an int in 1..230, " + f"got {raw_sg_number!r}" + ) + expected_sg_number = input_symmetry.get("space_group_number") + if type(expected_sg_number) is not int or not (1 <= expected_sg_number <= 230): + raise ValueError( + "input_ledger.crystallographic_context.symmetry.space_group_number must be an int in 1..230, " + f"got {expected_sg_number!r}" + ) + if raw_sg_number != expected_sg_number: + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_number {raw_sg_number} " + f"disagrees with input_ledger space_group_number {expected_sg_number}" + ) + + raw_operators = crystal_symmetry.get("space_group_operators_xyz") + if not isinstance(raw_operators, (list, tuple)) or not raw_operators: + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_operators_xyz must be a non-empty " + f"sequence of operator strings, got {raw_operators!r}" + ) + preserved_raw_operators: list[str] = [] + normalized_runtime_ops: list[str] = [] + for idx, op in enumerate(raw_operators): + norm_op = normalize_symmetry_operator_xyz( + op, + label=f"phenix_payload.crystal_symmetry.space_group_operators_xyz[{idx}]", + ) + preserved_raw_operators.append(op) + normalized_runtime_ops.append(norm_op) + if len(set(normalized_runtime_ops)) != len(normalized_runtime_ops): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_operators_xyz contains duplicate " + f"normalized symmetry operators: {normalized_runtime_ops!r}" + ) + + input_ops_raw = input_symmetry.get("operators_xyz") + input_op_count = input_symmetry.get("operator_count") + if ( + type(input_op_count) is not int + or input_op_count <= 0 + or not isinstance(input_ops_raw, (list, tuple)) + or len(input_ops_raw) != input_op_count + ): + raise ValueError( + "input_ledger.crystallographic_context.symmetry has invalid operator_count / operators_xyz" + ) + normalized_input_ops = sorted( + normalize_symmetry_operator_xyz( + op, + label=f"input_ledger.crystallographic_context.symmetry.operators_xyz[{idx}]", + ) + for idx, op in enumerate(input_ops_raw) + ) + sorted_runtime_ops = sorted(normalized_runtime_ops) + if ( + len(sorted_runtime_ops) != input_op_count + or sorted_runtime_ops != normalized_input_ops + ): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_operators_xyz " + f"(normalized={sorted_runtime_ops!r}) disagrees with input_ledger " + f"symmetry.operators_xyz {normalized_input_ops!r}" + ) + + m_sym = _SPACE_GROUP_SYMBOL_AND_NUMBER_RE.fullmatch(space_group_symbol) + if m_sym is None: + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_symbol " + f"{space_group_symbol!r} must match installed " + "cctbx.sgtbx.space_group_info.symbol_and_number() format " + "' (No. )'" + ) + symbol_number = int(m_sym.group("number")) + if symbol_number != raw_sg_number: + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_symbol {space_group_symbol!r} " + f"declares space-group number {symbol_number}, which disagrees with " + f"space_group_number {raw_sg_number}" + ) + normalized_lookup_symbol = "".join(m_sym.group("lookup_symbol").upper().split()) + expected_input_symbol = input_symmetry.get("space_group_symbol") + if type(expected_input_symbol) is not str or not expected_input_symbol.strip(): + raise ValueError( + "input_ledger.crystallographic_context.symmetry.space_group_symbol must be a non-empty string" + ) + normalized_expected_symbol = "".join(expected_input_symbol.upper().split()) + if normalized_lookup_symbol != normalized_expected_symbol: + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_symbol {space_group_symbol!r} " + f"(normalized symbol {normalized_lookup_symbol!r}) disagrees with " + f"input_ledger space_group_symbol {normalized_expected_symbol!r}" + ) + expected_lattice = input_symmetry.get("lattice") + if ( + type(expected_lattice) is not str + or not expected_lattice.strip() + or not normalized_lookup_symbol.startswith(expected_lattice.strip().upper()) + ): + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_symbol {space_group_symbol!r} " + f"lattice disagrees with input_ledger lattice {expected_lattice!r}" + ) + + if maps_symmetry is not None: + if not isinstance(maps_symmetry, Mapping): + raise ValueError("map_mtz.crystallographic_context missing symmetry mapping") + maps_sg_number = maps_symmetry.get("space_group_number") + if maps_sg_number != raw_sg_number: + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_number {raw_sg_number} " + f"disagrees with map MTZ space_group_number {maps_sg_number!r}" + ) + maps_ops_raw = maps_symmetry.get("operators_xyz") + if not isinstance(maps_ops_raw, (list, tuple)): + raise ValueError("map_mtz.crystallographic_context.symmetry missing operators_xyz") + normalized_maps_ops = sorted( + normalize_symmetry_operator_xyz( + op, + label=f"map_mtz.crystallographic_context.symmetry.operators_xyz[{idx}]", + ) + for idx, op in enumerate(maps_ops_raw) + ) + if sorted_runtime_ops != normalized_maps_ops: + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_operators_xyz " + f"(normalized={sorted_runtime_ops!r}) disagrees with map MTZ " + f"symmetry.operators_xyz {normalized_maps_ops!r}" + ) + maps_symbol = maps_symmetry.get("space_group_symbol") + if ( + type(maps_symbol) is not str + or "".join(maps_symbol.upper().split()) != normalized_lookup_symbol + ): + raise ValueError( + f"phenix_payload.crystal_symmetry.space_group_symbol {space_group_symbol!r} " + f"disagrees with map MTZ space_group_symbol {maps_symbol!r}" + ) + + return { + "space_group_symbol": space_group_symbol, + "space_group_number": raw_sg_number, + "raw_space_group_operators_xyz": preserved_raw_operators, + "normalized_space_group_operators_xyz": sorted_runtime_ops, + "normalized_lookup_symbol": normalized_lookup_symbol, + "symmetry_normalization_policy": SYMMETRY_OPERATOR_NORMALIZATION_POLICY, + } + + +def reconcile_phenix_rfactor_output( + phenix_payload: Mapping[str, Any], + input_ledger: Mapping[str, Any], + *, + expected_vendor_source_sha256: Mapping[str, str] | None = None, + expected_vendor_module_paths: Mapping[str, str | Path] | None = None, + expected_resolved_prefix: str | Path | None = None, + expected_phenix_version: str = toolchain.PHENIX_VERSION, + measured_build: Mapping[str, Any] | None = None, + phx_maps_mtz_path: Path | None = None, + phx_maps_mtz_bytes: bytes | None = None, + phx_obs_pair_only_mtz_path: Path | None = None, + phx_obs_pair_only_mtz_bytes: bytes | None = None, + phenix_diagnostics_path: Path | None = None, + phenix_diagnostics_payload: Mapping[str, Any] | None = None, + cache_manifest_artifacts: Mapping[str, Any] | None = None, +) -> dict[str, Any]: + """Validate and reconcile the full-precision PHENIX helper payload against input ledger. + + Enforces: + - Exact PHENIX version pin (``2.0-5936``), installation prefix, and runtime + vendor module provenance under the admitted ``resolved_prefix``. + - Recomputed canonical SHA-256 digests and zero per-scatterer diffs from the + full retained ``before``, ``after_model``, and ``after_fmodel`` scatterer + snapshots. + - Retained ``extract_xtal_data``, ``mmtbx.model.manager``, and + ``fmodel.update_all_scales`` diagnostic logs. + - Exact 1-to-1 HKL tracking and per-HKL ``f_obs`` / ``sigf_obs`` / ``is_free`` + agreement across ``extracted_pre_fmodel``, ``final_effective``, + ``actual_work``, and ``actual_free``. + - Full-precision float64 replay of ``r_work``, ``r_free``, ``r_all``, gap, + and reported reflection counts and numerator/denominator sums. + - Stage-by-stage ``d_max`` and ``d_min`` resolution extrema recomputed from + each stage's reconciled reflection HKLs and the admitted input unit cell + via the general crystallographic reciprocal metric tensor. + - Internal consistency of ``2mFo-DFc`` and ``mFo-DFc`` JSON map coefficients + and binary verification of ``phx_maps.mtz`` and ``PHX_OBS_PAIR_ONLY.mtz``. + """ + if not isinstance(phenix_payload, Mapping): + raise ValueError("phenix_payload must be a mapping") + if not isinstance(input_ledger, Mapping): + raise ValueError("input_ledger must be a mapping") + + eligible_by_hkl = input_ledger.get("eligible_by_hkl") + raw_rows = input_ledger.get("raw_rows") + input_context = input_ledger.get("crystallographic_context") + if not isinstance(eligible_by_hkl, Mapping) or not eligible_by_hkl: + raise ValueError("input_ledger missing non-empty eligible_by_hkl mapping") + if not isinstance(raw_rows, list) or not raw_rows: + raise ValueError("input_ledger missing non-empty raw_rows list") + if not isinstance(input_context, Mapping): + raise ValueError("input_ledger missing crystallographic_context") + validated_input_cell = validate_unit_cell_parameters( + input_context.get("cell_a_b_c_alpha_beta_gamma"), + label="input_ledger.crystallographic_context.cell_a_b_c_alpha_beta_gamma", + ) + + raw_by_hkl: dict[str, Mapping[str, Any]] = {} + for row in raw_rows: + key = _hkl_key(row["hkl"]) + if key in raw_by_hkl: + raise ValueError(f"Duplicate HKL {key} in input_ledger raw_rows") + raw_by_hkl[key] = row + + # 1. Verify PHENIX version pin, prefix binding, and runtime module provenance. + if ( + phenix_payload.get("artifact_role") + == "partial_unverified_phenix_evidence" + or phenix_payload.get("reusable_cache_output") is False + or phenix_payload.get("canonical_eval_qds_eligible") is False + ): + raise ValueError( + "Partial/unverified PHENIX evidence artifact cannot be reconciled " + "as a completed payload or admitted into canonical EVAL/QDS" + ) + if expected_phenix_version != toolchain.PHENIX_VERSION: + raise ValueError( + f"expected_phenix_version {expected_phenix_version!r} does not match " + f"repository pin {toolchain.PHENIX_VERSION!r}" + ) + if measured_build is not None: + validated_build = validate_measured_phenix_build( + measured_build, expected_version=expected_phenix_version + ) + if expected_resolved_prefix is None: + expected_resolved_prefix = validated_build["resolved_prefix"] + elif str(Path(expected_resolved_prefix)) != validated_build["resolved_prefix"]: + raise ValueError( + f"expected_resolved_prefix {expected_resolved_prefix!r} disagrees with " + f"measured_build resolved_prefix {validated_build['resolved_prefix']!r}" + ) + + payload_version = phenix_payload.get("expected_phenix_version") + if payload_version != expected_phenix_version: + raise ValueError( + f"phenix_payload expected_phenix_version {payload_version!r} != " + f"{expected_phenix_version!r}" + ) + payload_prefix = phenix_payload.get("expected_resolved_prefix") + if ( + not isinstance(payload_prefix, str) + or not payload_prefix + or not Path(payload_prefix).is_absolute() + ): + raise ValueError( + f"phenix_payload missing absolute expected_resolved_prefix: {payload_prefix!r}" + ) + + runtime_env = phenix_payload.get("runtime_environment") + if not isinstance(runtime_env, Mapping): + raise ValueError("phenix_payload missing runtime_environment") + sys_prefix = runtime_env.get("sys_prefix") + resolved_sys_prefix = runtime_env.get("resolved_sys_prefix") + sys_executable = runtime_env.get("sys_executable") + resolved_sys_executable = runtime_env.get("resolved_sys_executable", sys_executable) + if ( + not isinstance(sys_prefix, str) + or not sys_prefix + or not isinstance(resolved_sys_prefix, str) + or not resolved_sys_prefix + ): + raise ValueError("runtime_environment missing sys_prefix / resolved_sys_prefix") + prefix_path = Path(resolved_sys_prefix) + if not prefix_path.is_absolute(): + raise ValueError(f"resolved_sys_prefix must be absolute: {resolved_sys_prefix}") + if resolved_sys_prefix != str(Path(payload_prefix)): + raise ValueError( + f"runtime_environment.resolved_sys_prefix {resolved_sys_prefix!r} != " + f"payload expected_resolved_prefix {payload_prefix!r}" + ) + if expected_resolved_prefix is not None and resolved_sys_prefix != str( + Path(expected_resolved_prefix) + ): + raise ValueError( + f"runtime_environment.resolved_sys_prefix {resolved_sys_prefix!r} != " + f"expected_resolved_prefix {str(Path(expected_resolved_prefix))!r}" + ) + for exec_label, exec_val in ( + ("sys_executable", sys_executable), + ("resolved_sys_executable", resolved_sys_executable), + ): + if ( + not isinstance(exec_val, str) + or not exec_val + or not Path(exec_val).is_absolute() + or not Path(exec_val).is_relative_to(prefix_path) + ): + raise ValueError( + f"runtime_environment.{exec_label} {exec_val!r} is not inside " + f"resolved_sys_prefix {prefix_path}" + ) + + modules = runtime_env.get("vendor_modules") + if not isinstance(modules, Mapping) or set(modules) != set( + PHENIX_VENDOR_SOURCE_RELPATHS + ): + raise ValueError( + f"runtime_environment.vendor_modules keys must match " + f"{sorted(PHENIX_VENDOR_SOURCE_RELPATHS)}" + ) + for mod_key, mod_info in modules.items(): + if not isinstance(mod_info, Mapping): + raise ValueError(f"vendor_modules[{mod_key!r}] must be a mapping") + mod_path_str = mod_info.get("resolved_path") + mod_sha = mod_info.get("sha256") + if not isinstance(mod_path_str, str) or not mod_path_str: + raise ValueError(f"vendor_modules[{mod_key!r}] missing resolved_path") + if not isinstance(mod_sha, str) or len(mod_sha) != 64: + raise ValueError(f"vendor_modules[{mod_key!r}] missing valid sha256") + mod_path = Path(mod_path_str) + if not mod_path.is_absolute() or not mod_path.is_relative_to(prefix_path): + raise ValueError( + f"Vendor module {mod_key} at {mod_path} escapes resolved_sys_prefix " + f"{prefix_path}" + ) + rel_parts = mod_path.relative_to(prefix_path).parts + expected_rel = PHENIX_VENDOR_SOURCE_RELPATHS[mod_key] + if ( + len(rel_parts) != 3 + len(expected_rel.parts) + or rel_parts[0] != "lib" + or not rel_parts[1].startswith("python") + or rel_parts[2] != "site-packages" + or Path(*rel_parts[3:]) != expected_rel + ): + raise ValueError( + f"Vendor module {mod_key} path {mod_path} does not match canonical " + f"lib/python*/site-packages/{expected_rel} under {prefix_path}" + ) + if expected_vendor_module_paths is not None: + expected_mod_path = expected_vendor_module_paths.get(mod_key) + if expected_mod_path is None or str(mod_path) != str( + Path(expected_mod_path) + ): + raise ValueError( + f"Vendor module {mod_key} resolved_path mismatch: expected " + f"{expected_mod_path}, runtime loaded {mod_path}" + ) + if expected_vendor_source_sha256 is not None: + expected_sha = expected_vendor_source_sha256.get(mod_key) + if expected_sha != mod_sha: + raise ValueError( + f"Vendor module {mod_key} SHA-256 mismatch: expected " + f"{expected_sha}, runtime loaded {mod_sha}" + ) + + # 2. Verify extraction parameters, absence of second-block cutoff, and retained logs. + extraction_info = phenix_payload.get("extraction") + if not isinstance(extraction_info, Mapping): + raise ValueError("phenix_payload missing extraction section") + if extraction_info.get("labels") != ["F-obs", "SIGF-obs"]: + raise ValueError( + f"Unexpected extraction labels: {extraction_info.get('labels')!r}" + ) + if extraction_info.get("r_free_flags_label") != "R-free-flags": + raise ValueError( + f"Unexpected R-free label: {extraction_info.get('r_free_flags_label')!r}" + ) + if ( + type(extraction_info.get("test_flag_value")) is not int + or extraction_info.get("test_flag_value") != FREE_FLAG_VALUE + ): + raise ValueError( + f"Expected test_flag_value == {FREE_FLAG_VALUE}, got " + f"{extraction_info.get('test_flag_value')!r}" + ) + if extraction_info.get("keep_going") is not False: + raise ValueError("extract_xtal_data must run with keep_going=False") + if extraction_info.get("err"): + raise ValueError( + f"extract_xtal_data reported errors: {extraction_info.get('err')!r}" + ) + if extraction_info.get("applied_pdb_header_resolution_cut") is not False: + raise ValueError( + "Second-block PDB header resolution cutoff must not be applied" + ) + if not isinstance(extraction_info.get("log_text"), str): + raise ValueError("extraction.log_text must be a string") + + diagnostics = phenix_payload.get("diagnostics") + if not isinstance(diagnostics, Mapping): + raise ValueError("phenix_payload missing diagnostics section") + for log_key in ( + "extract_xtal_data_log", + "model_manager_log", + "fmodel_update_all_scales_log", + ): + if not isinstance(diagnostics.get(log_key), str): + raise ValueError(f"diagnostics[{log_key!r}] must be a string") + if diagnostics.get("failure") is not None: + raise ValueError( + f"diagnostics.failure must be None on success, got {diagnostics.get('failure')!r}" + ) + if diagnostics["extract_xtal_data_log"] != extraction_info["log_text"]: + raise ValueError( + "diagnostics.extract_xtal_data_log disagrees with extraction.log_text" + ) + + if phenix_diagnostics_path is not None or phenix_diagnostics_payload is not None: + if phenix_diagnostics_path is not None: + diag_sha = _sha256_regular_file(Path(phenix_diagnostics_path)) + if ( + cache_manifest_artifacts is not None + and "phenix_diagnostics_json" in cache_manifest_artifacts + ): + expected_diag_sha = cache_manifest_artifacts["phenix_diagnostics_json"][ + "sha256" + ] + if diag_sha != expected_diag_sha: + raise ValueError( + f"phenix_diagnostics.json SHA-256 {diag_sha} != " + f"manifest {expected_diag_sha}" + ) + loaded_diag = json.loads( + Path(phenix_diagnostics_path).read_text(encoding="utf-8") + ) + if ( + phenix_diagnostics_payload is not None + and dict(loaded_diag) != dict(phenix_diagnostics_payload) + ): + raise ValueError( + "phenix_diagnostics_path content disagrees with phenix_diagnostics_payload" + ) + diag_doc: Mapping[str, Any] = loaded_diag + else: + assert phenix_diagnostics_payload is not None + diag_doc = phenix_diagnostics_payload + if ( + not isinstance(diag_doc, Mapping) + or diag_doc.get("status") != "succeeded" + or diag_doc.get("failure") is not None + or diag_doc.get("extract_xtal_data_log") + != diagnostics["extract_xtal_data_log"] + or diag_doc.get("model_manager_log") != diagnostics["model_manager_log"] + or diag_doc.get("fmodel_update_all_scales_log") + != diagnostics["fmodel_update_all_scales_log"] + ): + raise ValueError( + "Standalone phenix_diagnostics.json disagrees with phenix_payload.diagnostics" + ) + + # 3. Recompute canonical scatterer snapshot digests and verify zero mutation. + xrs_check = phenix_payload.get("xray_structure_invariance") + if not isinstance(xrs_check, Mapping): + raise ValueError("phenix_payload missing xray_structure_invariance") + snapshots = xrs_check.get("snapshots") + if not isinstance(snapshots, Mapping) or set(snapshots) != { + "before", + "after_model", + "after_fmodel", + }: + raise ValueError( + "xray_structure_invariance must contain snapshots.{before,after_model,after_fmodel}" + ) + recomputed_xrs = onesar_phenix_measure.compare_scatterer_snapshots( + snapshots["before"], + snapshots["after_model"], + snapshots["after_fmodel"], + ) + if ( + type(xrs_check.get("n_scatterers")) is not int + or xrs_check["n_scatterers"] <= 0 + or xrs_check["n_scatterers"] != recomputed_xrs["n_scatterers"] + ): + raise ValueError( + "xray_structure_invariance n_scatterers disagrees with recomputed snapshots" + ) + before_sha = xrs_check.get("before_sha256") + after_model_sha = xrs_check.get("after_model_sha256") + after_fmodel_sha = xrs_check.get("after_fmodel_sha256") + if ( + before_sha != recomputed_xrs["before_sha256"] + or after_model_sha != recomputed_xrs["after_model_sha256"] + or after_fmodel_sha != recomputed_xrs["after_fmodel_sha256"] + ): + raise ValueError( + "xray_structure_invariance snapshot SHA-256 fields disagree with " + "recomputed scatterer records" + ) + if ( + not isinstance(before_sha, str) + or not before_sha + or before_sha != after_model_sha + or before_sha != after_fmodel_sha + or xrs_check.get("identical_before_vs_after_model") is not True + or xrs_check.get("identical_before_vs_after_fmodel") is not True + or recomputed_xrs["identical_before_vs_after_model"] is not True + or recomputed_xrs["identical_before_vs_after_fmodel"] is not True + ): + raise ValueError( + "Model xray_structure mutated during f_model construction or scaling: " + f"before={before_sha!r}, after_model={after_model_sha!r}, " + f"after_fmodel={after_fmodel_sha!r}" + ) + for diff_key in ( + "max_abs_site_cart_diff", + "max_abs_occupancy_diff", + "max_abs_u_iso_diff", + "max_abs_u_star_diff", + ): + diff_val = _verify_finite_nonnegative(xrs_check.get(diff_key), diff_key) + recomputed_diff = float(recomputed_xrs[diff_key]) + if diff_val != 0.0 or recomputed_diff != 0.0: + raise ValueError( + f"Non-zero {diff_key} across scaling: reported={diff_val}, " + f"recomputed={recomputed_diff}" + ) + + # 4. Reconcile extracted pre-fmodel and final effective reflection sets. + extracted_rows = phenix_payload.get("extracted_pre_fmodel") + final_rows = phenix_payload.get("final_effective") + work_rows = phenix_payload.get("actual_work") + free_rows = phenix_payload.get("actual_free") + if ( + not isinstance(extracted_rows, list) + or not isinstance(final_rows, list) + or not isinstance(work_rows, list) + or not isinstance(free_rows, list) + ): + raise ValueError( + "phenix_payload must contain list arrays for extracted_pre_fmodel, " + "final_effective, actual_work, and actual_free" + ) + if not extracted_rows or not final_rows or not work_rows or not free_rows: + raise ValueError( + "extracted_pre_fmodel, final_effective, actual_work, and actual_free " + "must all be non-empty" + ) + + extracted_by_hkl: dict[str, Mapping[str, Any]] = {} + for entry in extracted_rows: + key = _hkl_key(entry.get("hkl")) + if key in extracted_by_hkl: + raise ValueError( + f"Duplicate HKL {key} in extracted_pre_fmodel (many-to-one collision)" + ) + if key not in raw_by_hkl: + raise ValueError( + f"Extracted HKL {key} is absent from input MTZ raw ledger " + "(unexpected symmetry/reindexing transformation)" + ) + if key not in eligible_by_hkl: + raise ValueError( + f"Extracted HKL {key} was not input-eligible in input MTZ ledger" + ) + f_obs_val = _verify_finite_positive( + entry.get("f_obs"), f"extracted_pre_fmodel[{key}].f_obs" + ) + sigf_val = _verify_finite_positive( + entry.get("sigf_obs"), f"extracted_pre_fmodel[{key}].sigf_obs" + ) + is_free = entry.get("is_free") + if type(is_free) is not bool: + raise ValueError(f"extracted_pre_fmodel[{key}].is_free must be bool") + orig_entry = eligible_by_hkl[key] + if is_free != bool(orig_entry["is_free"]): + raise ValueError( + f"Flag mismatch at extracted HKL {key}: PHENIX is_free={is_free}, " + f"input ledger is_free={orig_entry['is_free']}" + ) + if not math.isclose( + f_obs_val, float(orig_entry["f_obs"]), rel_tol=0.0, abs_tol=1e-6 + ) or not math.isclose( + sigf_val, float(orig_entry["sigf_obs"]), rel_tol=0.0, abs_tol=1e-6 + ): + raise ValueError( + f"Pre-scaling extracted F/SIGF drifted from input MTZ at {key}: " + f"extracted=({f_obs_val}, {sigf_val}), " + f"input=({orig_entry['f_obs']}, {orig_entry['sigf_obs']})" + ) + extracted_by_hkl[key] = entry + + final_by_hkl: dict[str, Mapping[str, Any]] = {} + for entry in final_rows: + key = _hkl_key(entry.get("hkl")) + if key in final_by_hkl: + raise ValueError(f"Duplicate HKL {key} in final_effective") + if key not in extracted_by_hkl: + raise ValueError( + f"Final effective HKL {key} was not in extracted_pre_fmodel" + ) + f_final = _verify_finite_positive( + entry.get("f_obs"), f"final_effective[{key}].f_obs" + ) + sigf_final = _verify_finite_positive( + entry.get("sigf_obs"), f"final_effective[{key}].sigf_obs" + ) + is_free = entry.get("is_free") + if type(is_free) is not bool or is_free != bool(eligible_by_hkl[key]["is_free"]): + raise ValueError( + f"Flag mismatch at final_effective HKL {key}: {is_free!r}" + ) + final_by_hkl[key] = { + "hkl": list(entry["hkl"]), + "f_obs": f_final, + "sigf_obs": sigf_final, + "is_free": is_free, + } + + # 5. Reconcile actual_work and actual_free sets and replay R_work / R_free / R_all in float64. + scales = phenix_payload.get("scales_and_rfactors") + if not isinstance(scales, Mapping): + raise ValueError("phenix_payload missing scales_and_rfactors") + scale_k1_w = _verify_finite_positive(scales.get("scale_k1_w"), "scale_k1_w") + scale_k1_t = _verify_finite_positive(scales.get("scale_k1_t"), "scale_k1_t") + scale_k1 = _verify_finite_positive(scales.get("scale_k1"), "scale_k1") + reported_r_work = _verify_finite_nonnegative(scales.get("r_work"), "r_work") + reported_r_free = _verify_finite_nonnegative(scales.get("r_free"), "r_free") + reported_r_all = _verify_finite_nonnegative(scales.get("r_all"), "r_all") + reported_gap = _verify_finite( + scales.get("r_free_r_work_gap"), "r_free_r_work_gap" + ) + reported_extracted_count = _verify_int_count( + scales.get("extracted_pre_fmodel_count"), + "scales_and_rfactors.extracted_pre_fmodel_count", + ) + reported_final_count = _verify_int_count( + scales.get("final_effective_count"), + "scales_and_rfactors.final_effective_count", + ) + reported_work_count = _verify_int_count( + scales.get("work_count"), "scales_and_rfactors.work_count" + ) + reported_free_count = _verify_int_count( + scales.get("free_count"), "scales_and_rfactors.free_count" + ) + reported_work_num = _verify_finite_nonnegative( + scales.get("work_numerator"), "scales_and_rfactors.work_numerator" + ) + reported_work_den = _verify_finite_positive( + scales.get("work_denominator"), "scales_and_rfactors.work_denominator" + ) + reported_free_num = _verify_finite_nonnegative( + scales.get("free_numerator"), "scales_and_rfactors.free_numerator" + ) + reported_free_den = _verify_finite_positive( + scales.get("free_denominator"), "scales_and_rfactors.free_denominator" + ) + + work_by_hkl: dict[str, dict[str, Any]] = {} + work_num = 0.0 + work_den = 0.0 + all_num = 0.0 + all_den = 0.0 + max_f_obs_scale_diff = 0.0 + + for entry in work_rows: + key = _hkl_key(entry.get("hkl")) + if key in work_by_hkl: + raise ValueError(f"Duplicate HKL {key} in actual_work") + if key not in final_by_hkl: + raise ValueError(f"actual_work HKL {key} not in final_effective") + orig = eligible_by_hkl[key] + if orig["is_free"] or orig["r_free_flag"] != WORK_FLAG_VALUE: + raise ValueError( + f"actual_work HKL {key} has free flag {orig['r_free_flag']} in input MTZ" + ) + final_entry = final_by_hkl[key] + if final_entry["is_free"] is not False: + raise ValueError( + f"actual_work HKL {key} has is_free={final_entry['is_free']!r} in final_effective" + ) + fo = _verify_finite_positive(entry.get("f_obs"), f"actual_work[{key}].f_obs") + sigfo = _verify_finite_positive( + entry.get("sigf_obs"), f"actual_work[{key}].sigf_obs" + ) + if not math.isclose( + fo, float(final_entry["f_obs"]), rel_tol=1e-15, abs_tol=1e-15 + ) or not math.isclose( + sigfo, float(final_entry["sigf_obs"]), rel_tol=1e-15, abs_tol=1e-15 + ): + raise ValueError( + f"actual_work[{key}] F/SIGF ({fo}, {sigfo}) disagrees with " + f"final_effective[{key}] ({final_entry['f_obs']}, {final_entry['sigf_obs']})" + ) + fm_raw_re = _verify_finite( + entry.get("f_model_real"), f"actual_work[{key}].f_model_real" + ) + fm_raw_im = _verify_finite( + entry.get("f_model_imag"), f"actual_work[{key}].f_model_imag" + ) + fm_raw_amp = _verify_finite_nonnegative( + entry.get("f_model_amplitude"), f"actual_work[{key}].f_model_amplitude" + ) + fm_scaled_re = _verify_finite( + entry.get("f_model_scaled_with_k1_w_real"), + f"actual_work[{key}].f_model_scaled_with_k1_w_real", + ) + fm_scaled_im = _verify_finite( + entry.get("f_model_scaled_with_k1_w_imag"), + f"actual_work[{key}].f_model_scaled_with_k1_w_imag", + ) + fm_scaled_amp = _verify_finite_nonnegative( + entry.get("f_model_scaled_with_k1_w_amplitude"), + f"actual_work[{key}].f_model_scaled_with_k1_w_amplitude", + ) + calc_raw_amp = math.hypot(fm_raw_re, fm_raw_im) + calc_scaled_amp = math.hypot(fm_scaled_re, fm_scaled_im) + if not math.isclose(calc_raw_amp, fm_raw_amp, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"actual_work[{key}] raw amplitude {fm_raw_amp} != hypot {calc_raw_amp}" + ) + if not math.isclose( + calc_scaled_amp, fm_scaled_amp, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_work[{key}] scaled amplitude {fm_scaled_amp} != hypot " + f"{calc_scaled_amp}" + ) + if not math.isclose( + fm_scaled_amp, scale_k1_w * fm_raw_amp, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_work[{key}] f_model_scaled_with_k1_w amplitude {fm_scaled_amp} " + f"disagrees with scale_k1_w * |f_model_work| = {scale_k1_w * fm_raw_amp}" + ) + if not math.isclose( + fm_scaled_re, scale_k1_w * fm_raw_re, rel_tol=1e-12, abs_tol=1e-12 + ) or not math.isclose( + fm_scaled_im, scale_k1_w * fm_raw_im, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_work[{key}] f_model_scaled_with_k1_w complex components " + f"({fm_scaled_re}, {fm_scaled_im}) disagree with " + f"scale_k1_w * f_model_work = " + f"({scale_k1_w * fm_raw_re}, {scale_k1_w * fm_raw_im})" + ) + + work_num += abs(fo - fm_scaled_amp) + work_den += fo + all_num += abs(fo - scale_k1 * fm_raw_amp) + all_den += fo + max_f_obs_scale_diff = max( + max_f_obs_scale_diff, abs(fo - float(orig["f_obs"])) + ) + work_by_hkl[key] = { + "hkl": list(entry["hkl"]), + "original_f_obs": float(orig["f_obs"]), + "original_sigf_obs": float(orig["sigf_obs"]), + "final_f_obs": fo, + "final_sigf_obs": sigfo, + "r_free_flag": WORK_FLAG_VALUE, + "f_model_amplitude": fm_raw_amp, + "f_model_scaled_with_k1_w_amplitude": fm_scaled_amp, + } + + free_by_hkl: dict[str, dict[str, Any]] = {} + free_num = 0.0 + free_den = 0.0 + + for entry in free_rows: + key = _hkl_key(entry.get("hkl")) + if key in free_by_hkl: + raise ValueError(f"Duplicate HKL {key} in actual_free") + if key in work_by_hkl: + raise ValueError( + f"HKL {key} appears in both actual_work and actual_free (non-disjoint)" + ) + if key not in final_by_hkl: + raise ValueError(f"actual_free HKL {key} not in final_effective") + orig = eligible_by_hkl[key] + if not orig["is_free"] or orig["r_free_flag"] != FREE_FLAG_VALUE: + raise ValueError( + f"actual_free HKL {key} has work flag {orig['r_free_flag']} in input MTZ" + ) + final_entry = final_by_hkl[key] + if final_entry["is_free"] is not True: + raise ValueError( + f"actual_free HKL {key} has is_free={final_entry['is_free']!r} in final_effective" + ) + fo = _verify_finite_positive(entry.get("f_obs"), f"actual_free[{key}].f_obs") + sigfo = _verify_finite_positive( + entry.get("sigf_obs"), f"actual_free[{key}].sigf_obs" + ) + if not math.isclose( + fo, float(final_entry["f_obs"]), rel_tol=1e-15, abs_tol=1e-15 + ) or not math.isclose( + sigfo, float(final_entry["sigf_obs"]), rel_tol=1e-15, abs_tol=1e-15 + ): + raise ValueError( + f"actual_free[{key}] F/SIGF ({fo}, {sigfo}) disagrees with " + f"final_effective[{key}] ({final_entry['f_obs']}, {final_entry['sigf_obs']})" + ) + fm_raw_re = _verify_finite( + entry.get("f_model_real"), f"actual_free[{key}].f_model_real" + ) + fm_raw_im = _verify_finite( + entry.get("f_model_imag"), f"actual_free[{key}].f_model_imag" + ) + fm_raw_amp = _verify_finite_nonnegative( + entry.get("f_model_amplitude"), f"actual_free[{key}].f_model_amplitude" + ) + fm_scaled_re = _verify_finite( + entry.get("f_model_scaled_with_k1_t_real"), + f"actual_free[{key}].f_model_scaled_with_k1_t_real", + ) + fm_scaled_im = _verify_finite( + entry.get("f_model_scaled_with_k1_t_imag"), + f"actual_free[{key}].f_model_scaled_with_k1_t_imag", + ) + fm_scaled_amp = _verify_finite_nonnegative( + entry.get("f_model_scaled_with_k1_t_amplitude"), + f"actual_free[{key}].f_model_scaled_with_k1_t_amplitude", + ) + calc_raw_amp = math.hypot(fm_raw_re, fm_raw_im) + calc_scaled_amp = math.hypot(fm_scaled_re, fm_scaled_im) + if not math.isclose(calc_raw_amp, fm_raw_amp, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"actual_free[{key}] raw amplitude {fm_raw_amp} != hypot {calc_raw_amp}" + ) + if not math.isclose( + calc_scaled_amp, fm_scaled_amp, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_free[{key}] scaled amplitude {fm_scaled_amp} != hypot " + f"{calc_scaled_amp}" + ) + if not math.isclose( + fm_scaled_amp, scale_k1_t * fm_raw_amp, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_free[{key}] f_model_scaled_with_k1_t amplitude {fm_scaled_amp} " + f"disagrees with scale_k1_t * |f_model_free| = {scale_k1_t * fm_raw_amp}" + ) + if not math.isclose( + fm_scaled_re, scale_k1_t * fm_raw_re, rel_tol=1e-12, abs_tol=1e-12 + ) or not math.isclose( + fm_scaled_im, scale_k1_t * fm_raw_im, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"actual_free[{key}] f_model_scaled_with_k1_t complex components " + f"({fm_scaled_re}, {fm_scaled_im}) disagree with " + f"scale_k1_t * f_model_free = " + f"({scale_k1_t * fm_raw_re}, {scale_k1_t * fm_raw_im})" + ) + + free_num += abs(fo - fm_scaled_amp) + free_den += fo + all_num += abs(fo - scale_k1 * fm_raw_amp) + all_den += fo + max_f_obs_scale_diff = max( + max_f_obs_scale_diff, abs(fo - float(orig["f_obs"])) + ) + free_by_hkl[key] = { + "hkl": list(entry["hkl"]), + "original_f_obs": float(orig["f_obs"]), + "original_sigf_obs": float(orig["sigf_obs"]), + "final_f_obs": fo, + "final_sigf_obs": sigfo, + "r_free_flag": FREE_FLAG_VALUE, + "f_model_amplitude": fm_raw_amp, + "f_model_scaled_with_k1_t_amplitude": fm_scaled_amp, + } + + if set(work_by_hkl) | set(free_by_hkl) != set(final_by_hkl): + raise ValueError( + "Union of actual_work and actual_free HKLs does not equal final_effective" + ) + if ( + reported_extracted_count != len(extracted_by_hkl) + or reported_final_count != len(final_by_hkl) + or reported_work_count != len(work_by_hkl) + or reported_free_count != len(free_by_hkl) + ): + raise ValueError( + "Reported scales_and_rfactors counts disagree with reflection arrays: " + f"reported=({reported_extracted_count}, {reported_final_count}, " + f"{reported_work_count}, {reported_free_count}), " + f"actual=({len(extracted_by_hkl)}, {len(final_by_hkl)}, " + f"{len(work_by_hkl)}, {len(free_by_hkl)})" + ) + if work_den <= 0.0 or free_den <= 0.0 or all_den <= 0.0: + raise ValueError("Work, free, and all denominators must be strictly positive") + + if not math.isclose(reported_work_num, work_num, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported work_numerator {reported_work_num} != recomputed {work_num}" + ) + if not math.isclose(reported_work_den, work_den, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported work_denominator {reported_work_den} != recomputed {work_den}" + ) + if not math.isclose(reported_free_num, free_num, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported free_numerator {reported_free_num} != recomputed {free_num}" + ) + if not math.isclose(reported_free_den, free_den, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported free_denominator {reported_free_den} != recomputed {free_den}" + ) + if "all_numerator" in scales: + reported_all_num = _verify_finite_nonnegative( + scales.get("all_numerator"), "scales_and_rfactors.all_numerator" + ) + if not math.isclose(reported_all_num, all_num, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported all_numerator {reported_all_num} != recomputed {all_num}" + ) + if "all_denominator" in scales: + reported_all_den = _verify_finite_positive( + scales.get("all_denominator"), "scales_and_rfactors.all_denominator" + ) + if not math.isclose(reported_all_den, all_den, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Reported all_denominator {reported_all_den} != recomputed {all_den}" + ) + + replayed_r_work = work_num / work_den + replayed_r_free = free_num / free_den + replayed_r_all = all_num / all_den + replayed_gap = replayed_r_free - replayed_r_work + + if not math.isclose(replayed_r_work, reported_r_work, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Replayed r_work {replayed_r_work:.15f} disagrees with native " + f"fmodel.r_work() {reported_r_work:.15f}" + ) + if not math.isclose(replayed_r_free, reported_r_free, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Replayed r_free {replayed_r_free:.15f} disagrees with native " + f"fmodel.r_free() {reported_r_free:.15f}" + ) + if not math.isclose(replayed_r_all, reported_r_all, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Replayed r_all {replayed_r_all:.15f} disagrees with native " + f"fmodel.r_all() {reported_r_all:.15f}" + ) + if not math.isclose(replayed_gap, reported_gap, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"Replayed r_free - r_work gap {replayed_gap:.15f} disagrees with " + f"reported {reported_gap:.15f}" + ) + + # 5b. Validate retained runtime unit cell against installed CCP4/iotbx float32 + # conversion of the admitted input cell, then replay stage resolution ranges (#1020). + expected_runtime_float32_cell = ccp4_mtz_float32_unit_cell( + validated_input_cell, + label="input_ledger.crystallographic_context.cell_a_b_c_alpha_beta_gamma", + ) + crystal_symmetry = phenix_payload.get("crystal_symmetry") + if not isinstance(crystal_symmetry, Mapping): + raise ValueError("phenix_payload missing crystal_symmetry section") + space_group_symbol = crystal_symmetry.get("space_group_symbol") + if not isinstance(space_group_symbol, str) or not space_group_symbol.strip(): + raise ValueError( + "phenix_payload.crystal_symmetry.space_group_symbol must be a non-empty string" + ) + raw_runtime_cell = crystal_symmetry.get("unit_cell") + if not isinstance(raw_runtime_cell, (list, tuple)) or len(raw_runtime_cell) != 6: + raise ValueError( + "phenix_payload.crystal_symmetry.unit_cell must be a 6-element numeric sequence, " + f"got {raw_runtime_cell!r}" + ) + for idx, val in enumerate(raw_runtime_cell): + _verify_finite_positive( + val, f"phenix_payload.crystal_symmetry.unit_cell[{idx}]" + ) + validated_runtime_cell = validate_unit_cell_parameters( + raw_runtime_cell, label="phenix_payload.crystal_symmetry.unit_cell" + ) + for idx, (param_name, rt_val, exp_val) in enumerate( + zip( + ("a", "b", "c", "alpha", "beta", "gamma"), + validated_runtime_cell, + expected_runtime_float32_cell, + ) + ): + if not math.isclose(rt_val, exp_val, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"phenix_payload.crystal_symmetry.unit_cell[{idx}] ({param_name}) " + f"{rt_val:.15f} disagrees with installed CCP4/iotbx float32 " + f"conversion {exp_val:.15f} of admitted input cell " + f"{validated_input_cell[idx]:.15f}" + ) + validated_runtime_space_group = validate_runtime_space_group( + crystal_symmetry, + input_context.get("symmetry"), + ) + + resolution_ranges = phenix_payload.get("resolution_ranges") + if not isinstance(resolution_ranges, Mapping) or set( + resolution_ranges + ) != set(EXPECTED_RESOLUTION_STAGES): + raise ValueError( + "phenix_payload.resolution_ranges must be a mapping with exact keys " + f"{list(EXPECTED_RESOLUTION_STAGES)!r}, got " + f"{list(resolution_ranges) if isinstance(resolution_ranges, Mapping) else resolution_ranges!r}" + ) + stage_hkls_by_name: dict[str, list[Any]] = { + "extracted_pre_fmodel": [ + entry["hkl"] for entry in extracted_by_hkl.values() + ], + "final_effective": [entry["hkl"] for entry in final_by_hkl.values()], + "actual_work": [entry["hkl"] for entry in work_by_hkl.values()], + "actual_free": [entry["hkl"] for entry in free_by_hkl.values()], + } + reconciled_resolution_ranges: dict[str, dict[str, float]] = {} + for stage_name in EXPECTED_RESOLUTION_STAGES: + stage_range = resolution_ranges.get(stage_name) + if not isinstance(stage_range, Mapping) or set(stage_range) != { + "d_max", + "d_min", + }: + raise ValueError( + f"resolution_ranges[{stage_name!r}] must be a mapping with exact keys " + f"['d_max', 'd_min'], got " + f"{list(stage_range) if isinstance(stage_range, Mapping) else stage_range!r}" + ) + reported_d_max = _verify_finite_positive( + stage_range.get("d_max"), f"resolution_ranges[{stage_name!r}].d_max" + ) + reported_d_min = _verify_finite_positive( + stage_range.get("d_min"), f"resolution_ranges[{stage_name!r}].d_min" + ) + if reported_d_max < reported_d_min: + raise ValueError( + f"resolution_ranges[{stage_name!r}] has inverted bounds: " + f"d_max={reported_d_max} < d_min={reported_d_min}" + ) + recomputed_range = compute_stage_resolution_range( + stage_hkls_by_name[stage_name], + validated_runtime_cell, + stage_label=stage_name, + ) + recomputed_d_max = recomputed_range["d_max"] + recomputed_d_min = recomputed_range["d_min"] + if not math.isclose( + reported_d_max, recomputed_d_max, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"resolution_ranges[{stage_name!r}].d_max {reported_d_max:.15f} " + f"disagrees with recomputed {recomputed_d_max:.15f}" + ) + if not math.isclose( + reported_d_min, recomputed_d_min, rel_tol=1e-12, abs_tol=1e-12 + ): + raise ValueError( + f"resolution_ranges[{stage_name!r}].d_min {reported_d_min:.15f} " + f"disagrees with recomputed {recomputed_d_min:.15f}" + ) + reconciled_resolution_ranges[stage_name] = { + "d_max": reported_d_max, + "d_min": reported_d_min, + "recomputed_d_max": recomputed_d_max, + "recomputed_d_min": recomputed_d_min, + "d_max_abs_diff": abs(recomputed_d_max - reported_d_max), + "d_min_abs_diff": abs(recomputed_d_min - reported_d_min), + } + + # 6. Validate fresh complex map-coefficient export metadata, JSON records, and MTZ files. + map_coeffs = phenix_payload.get("map_coefficients") + if not isinstance(map_coeffs, Mapping): + raise ValueError("phenix_payload missing map_coefficients section") + expected_maps = { + "2mFo-DFc": ("2FOFCWT", "PH2FOFCWT"), + "mFo-DFc": ("FOFCWT", "PHFOFCWT"), + } + map_summary: dict[str, Any] = {} + map_records_by_hkl: dict[str, dict[str, Mapping[str, Any]]] = {} + for map_key, (amp_label, phase_label) in expected_maps.items(): + mdata = map_coeffs.get(map_key) + if not isinstance(mdata, Mapping): + raise ValueError(f"map_coefficients missing {map_key!r}") + if ( + mdata.get("map_type") != map_key + or mdata.get("amplitude_label") != amp_label + or mdata.get("phase_label") != phase_label + ): + raise ValueError(f"Invalid labels for map_coefficients[{map_key!r}]") + params = mdata.get("parameters") + if not isinstance(params, Mapping): + raise ValueError(f"map_coefficients[{map_key!r}] missing parameters") + if ( + params.get("acentrics_scale") != 2.0 + or params.get("centrics_pre_scale") != 1.0 + or params.get("exclude_free_r_reflections") is not False + or params.get("fill_missing") is not False + or params.get("isotropize") is not True + or params.get("sharp") is not False + ): + raise ValueError( + f"map_coefficients[{map_key!r}] parameters violate preregistered " + f"contract: {params!r}" + ) + records = mdata.get("records") + if not isinstance(records, list) or len(records) != len(final_by_hkl): + raise ValueError( + f"map_coefficients[{map_key!r}] record count must equal " + f"final_effective count ({len(final_by_hkl)})" + ) + reported_n_refl = _verify_int_count( + mdata.get("n_reflections"), f"map_coefficients[{map_key!r}].n_reflections" + ) + reported_centric_count = _verify_nonnegative_int_count( + mdata.get("centric_count"), f"map_coefficients[{map_key!r}].centric_count" + ) + reported_acentric_count = _verify_nonnegative_int_count( + mdata.get("acentric_count"), + f"map_coefficients[{map_key!r}].acentric_count", + ) + if reported_n_refl != len(records): + raise ValueError( + f"map_coefficients[{map_key!r}].n_reflections {reported_n_refl} != " + f"len(records) {len(records)}" + ) + by_hkl: dict[str, Mapping[str, Any]] = {} + centric_count = 0 + for rec in records: + rkey = _hkl_key(rec.get("hkl")) + if rkey in by_hkl: + raise ValueError( + f"Duplicate HKL {rkey} in map_coefficients[{map_key!r}]" + ) + if rkey not in final_by_hkl: + raise ValueError( + f"Unexpected HKL {rkey} in map_coefficients[{map_key!r}]" + ) + re_val = _verify_finite(rec.get("real"), f"{map_key}[{rkey}].real") + im_val = _verify_finite(rec.get("imag"), f"{map_key}[{rkey}].imag") + amp_val = _verify_finite_nonnegative( + rec.get("amplitude"), f"{map_key}[{rkey}].amplitude" + ) + phase_val = _verify_finite( + rec.get("phase_deg"), f"{map_key}[{rkey}].phase_deg" + ) + if type(rec.get("is_centric")) is not bool: + raise ValueError(f"{map_key}[{rkey}].is_centric must be bool") + calc_amp = math.hypot(re_val, im_val) + if not math.isclose(calc_amp, amp_val, rel_tol=1e-12, abs_tol=1e-12): + raise ValueError( + f"{map_key}[{rkey}] amplitude {amp_val} != hypot(real, imag) {calc_amp}" + ) + if calc_amp > 1e-12: + calc_phase = math.degrees(math.atan2(im_val, re_val)) + if _circular_phase_diff_deg(calc_phase, phase_val) > 1e-9: + raise ValueError( + f"{map_key}[{rkey}] phase_deg {phase_val} disagrees with " + f"atan2(imag, real) {calc_phase}" + ) + if rec["is_centric"]: + centric_count += 1 + by_hkl[rkey] = rec + acentric_count = len(records) - centric_count + if ( + reported_centric_count != centric_count + or reported_acentric_count != acentric_count + ): + raise ValueError( + f"map_coefficients[{map_key!r}] centric/acentric counts " + f"({reported_centric_count}, {reported_acentric_count}) != " + f"({centric_count}, {acentric_count})" + ) + map_records_by_hkl[map_key] = by_hkl + map_summary[map_key] = { + "amplitude_label": amp_label, + "phase_label": phase_label, + "n_reflections": len(records), + "centric_count": centric_count, + "acentric_count": acentric_count, + "parameters": dict(params), + "coefficient_semantics": mdata.get("coefficient_semantics"), + } + + mtz_supplements = map_coeffs.get("mtz_supplements") + if not isinstance(mtz_supplements, Mapping): + raise ValueError("map_coefficients missing mtz_supplements") + expected_maps_columns = [ + "H", + "K", + "L", + "2FOFCWT", + "PH2FOFCWT", + "FOFCWT", + "PHFOFCWT", + ] + expected_obs_pair_columns = [ + "H", + "K", + "L", + "2FOFCWT", + "PH2FOFCWT", + ] + if mtz_supplements.get("phx_maps_columns") != expected_maps_columns: + raise ValueError( + f"Unexpected phx_maps.mtz columns: {mtz_supplements.get('phx_maps_columns')!r}" + ) + if mtz_supplements.get("obs_pair_only_columns") != expected_obs_pair_columns: + raise ValueError( + "Unexpected PHX_OBS_PAIR_ONLY.mtz columns: " + f"{mtz_supplements.get('obs_pair_only_columns')!r}" + ) + + if phx_maps_mtz_path is not None: + _sha256_regular_file(Path(phx_maps_mtz_path)) + loaded_maps_bytes = Path(phx_maps_mtz_path).read_bytes() + if phx_maps_mtz_bytes is not None and phx_maps_mtz_bytes != loaded_maps_bytes: + raise ValueError("phx_maps_mtz_path bytes disagree with phx_maps_mtz_bytes") + maps_bytes = loaded_maps_bytes + elif phx_maps_mtz_bytes is not None: + maps_bytes = bytes(phx_maps_mtz_bytes) + else: + raise ValueError( + "reconcile_phenix_rfactor_output requires phx_maps_mtz_path or phx_maps_mtz_bytes" + ) + + if phx_obs_pair_only_mtz_path is not None: + _sha256_regular_file(Path(phx_obs_pair_only_mtz_path)) + loaded_pair_bytes = Path(phx_obs_pair_only_mtz_path).read_bytes() + if ( + phx_obs_pair_only_mtz_bytes is not None + and phx_obs_pair_only_mtz_bytes != loaded_pair_bytes + ): + raise ValueError( + "phx_obs_pair_only_mtz_path bytes disagree with phx_obs_pair_only_mtz_bytes" + ) + obs_pair_bytes = loaded_pair_bytes + elif phx_obs_pair_only_mtz_bytes is not None: + obs_pair_bytes = bytes(phx_obs_pair_only_mtz_bytes) + else: + raise ValueError( + "reconcile_phenix_rfactor_output requires phx_obs_pair_only_mtz_path or " + "phx_obs_pair_only_mtz_bytes" + ) + + maps_sha = _sha256_bytes(maps_bytes) + obs_pair_sha = _sha256_bytes(obs_pair_bytes) + if mtz_supplements.get("phx_maps_sha256") != maps_sha: + raise ValueError( + f"phx_maps.mtz SHA-256 {maps_sha} disagrees with " + f"mtz_supplements.phx_maps_sha256 {mtz_supplements.get('phx_maps_sha256')!r}" + ) + if mtz_supplements.get("obs_pair_only_sha256") != obs_pair_sha: + raise ValueError( + f"PHX_OBS_PAIR_ONLY.mtz SHA-256 {obs_pair_sha} disagrees with " + f"mtz_supplements.obs_pair_only_sha256 {mtz_supplements.get('obs_pair_only_sha256')!r}" + ) + if cache_manifest_artifacts is not None: + if cache_manifest_artifacts.get("phx_maps_mtz", {}).get("sha256") != maps_sha: + raise ValueError( + "phx_maps.mtz SHA-256 disagrees with cache manifest artifacts" + ) + if ( + cache_manifest_artifacts.get("phx_obs_pair_only_mtz", {}).get("sha256") + != obs_pair_sha + ): + raise ValueError( + "PHX_OBS_PAIR_ONLY.mtz SHA-256 disagrees with cache manifest artifacts" + ) + + maps_mtz = inspect_map_mtz_bytes( + maps_bytes, + expected_columns=expected_maps_columns, + expected_column_types=["H", "H", "H", "F", "P", "F", "P"], + ) + obs_pair_mtz = inspect_map_mtz_bytes( + obs_pair_bytes, + expected_columns=expected_obs_pair_columns, + expected_column_types=["H", "H", "H", "F", "P"], + ) + if maps_mtz["crystallographic_context"] != obs_pair_mtz["crystallographic_context"]: + raise ValueError( + "PHX_OBS_PAIR_ONLY.mtz crystallographic context differs from phx_maps.mtz" + ) + maps_ctx = maps_mtz["crystallographic_context"] + input_cell = list(validated_input_cell) + maps_cell = list( + validate_unit_cell_parameters( + maps_ctx.get("cell_a_b_c_alpha_beta_gamma"), + label="map_mtz.crystallographic_context.cell_a_b_c_alpha_beta_gamma", + ) + ) + if len(maps_cell) != 6 or any( + not math.isclose(a, b, rel_tol=0.0, abs_tol=1e-3) + for a, b in zip(input_cell, maps_cell) + ): + raise ValueError( + f"Map MTZ cell {maps_cell} disagrees with input MTZ cell {input_cell}" + ) + input_symm = input_context["symmetry"] + maps_symm = maps_ctx["symmetry"] + for symm_key in ( + "operator_count", + "primitive_operator_count", + "lattice", + "space_group_number", + "space_group_symbol", + "operators_xyz", + ): + if maps_symm.get(symm_key) != input_symm.get(symm_key): + raise ValueError( + f"Map MTZ symmetry.{symm_key} {maps_symm.get(symm_key)!r} != " + f"input MTZ {input_symm.get(symm_key)!r}" + ) + validated_runtime_space_group = validate_runtime_space_group( + crystal_symmetry, + input_symm, + maps_symmetry=maps_symm, + ) + + if set(maps_mtz["rows_by_hkl"]) != set(final_by_hkl) or set( + obs_pair_mtz["rows_by_hkl"] + ) != set(final_by_hkl): + raise ValueError( + "Map MTZ HKL sets do not match final_effective HKL set" + ) + + for rkey in final_by_hkl: + rec_2fofc = map_records_by_hkl["2mFo-DFc"][rkey] + rec_fofc = map_records_by_hkl["mFo-DFc"][rkey] + m_row = maps_mtz["rows_by_hkl"][rkey] + p_row = obs_pair_mtz["rows_by_hkl"][rkey] + if ( + p_row["hkl"] != m_row["hkl"] + or p_row["words"]["2FOFCWT"] != m_row["words"]["2FOFCWT"] + or p_row["words"]["PH2FOFCWT"] != m_row["words"]["PH2FOFCWT"] + ): + raise ValueError( + f"PHX_OBS_PAIR_ONLY.mtz 2FOFCWT/PH2FOFCWT words differ from " + f"phx_maps.mtz at HKL {rkey}" + ) + amp_2fofc = float(rec_2fofc["amplitude"]) + ph_2fofc = float(rec_2fofc["phase_deg"]) + amp_fofc = float(rec_fofc["amplitude"]) + ph_fofc = float(rec_fofc["phase_deg"]) + if not math.isclose( + m_row["values"]["2FOFCWT"], amp_2fofc, rel_tol=1e-5, abs_tol=1e-5 + ): + raise ValueError( + f"phx_maps.mtz 2FOFCWT {m_row['values']['2FOFCWT']} disagrees with " + f"JSON amplitude {amp_2fofc} at HKL {rkey}" + ) + if amp_2fofc > 1e-6 and ( + _circular_phase_diff_deg(m_row["values"]["PH2FOFCWT"], ph_2fofc) > 5e-4 + ): + raise ValueError( + f"phx_maps.mtz PH2FOFCWT {m_row['values']['PH2FOFCWT']} disagrees with " + f"JSON phase_deg {ph_2fofc} at HKL {rkey}" + ) + if not math.isclose( + m_row["values"]["FOFCWT"], amp_fofc, rel_tol=1e-5, abs_tol=1e-5 + ): + raise ValueError( + f"phx_maps.mtz FOFCWT {m_row['values']['FOFCWT']} disagrees with " + f"JSON amplitude {amp_fofc} at HKL {rkey}" + ) + if amp_fofc > 1e-6 and ( + _circular_phase_diff_deg(m_row["values"]["PHFOFCWT"], ph_fofc) > 5e-4 + ): + raise ValueError( + f"phx_maps.mtz PHFOFCWT {m_row['values']['PHFOFCWT']} disagrees with " + f"JSON phase_deg {ph_fofc} at HKL {rkey}" + ) + + # 7. Compute exact stage-by-stage set differences without inventing per-reflection causes. + raw_hkl_set = set(raw_by_hkl) + eligible_hkl_set = set(eligible_by_hkl) + extracted_hkl_set = set(extracted_by_hkl) + final_hkl_set = set(final_by_hkl) + + raw_to_eligible_excluded = sorted( + (list(raw_by_hkl[k]["hkl"]) for k in (raw_hkl_set - eligible_hkl_set)), + key=tuple, + ) + eligible_to_extracted_excluded = sorted( + (list(eligible_by_hkl[k]["hkl"]) for k in (eligible_hkl_set - extracted_hkl_set)), + key=tuple, + ) + extracted_to_final_excluded = sorted( + (list(extracted_by_hkl[k]["hkl"]) for k in (extracted_hkl_set - final_hkl_set)), + key=tuple, + ) + + return { + "reconciliation_status": "verified", + "counts": { + "raw_mtz_total": len(raw_hkl_set), + "input_eligible_total": len(eligible_hkl_set), + "extracted_pre_fmodel_total": len(extracted_hkl_set), + "final_effective_total": len(final_hkl_set), + "actual_work_total": len(work_by_hkl), + "actual_free_total": len(free_by_hkl), + "excluded_raw_to_input_eligible": len(raw_to_eligible_excluded), + "excluded_input_eligible_to_extracted": len(eligible_to_extracted_excluded), + "excluded_extracted_to_final_effective": len(extracted_to_final_excluded), + }, + "set_differences": { + "raw_to_input_eligible_excluded_hkls": raw_to_eligible_excluded, + "input_eligible_to_extracted_excluded_hkls": eligible_to_extracted_excluded, + "extracted_to_final_effective_excluded_hkls": extracted_to_final_excluded, + }, + "rfactors": { + "r_work": reported_r_work, + "r_free": reported_r_free, + "r_all": reported_r_all, + "r_free_r_work_gap": reported_gap, + "replayed_r_work": replayed_r_work, + "replayed_r_free": replayed_r_free, + "replayed_r_all": replayed_r_all, + "replayed_r_free_r_work_gap": replayed_gap, + "r_work_abs_diff": abs(replayed_r_work - reported_r_work), + "r_free_abs_diff": abs(replayed_r_free - reported_r_free), + "r_all_abs_diff": abs(replayed_r_all - reported_r_all), + "work_numerator": work_num, + "work_denominator": work_den, + "free_numerator": free_num, + "free_denominator": free_den, + "all_numerator": all_num, + "all_denominator": all_den, + "scale_k1_w": scale_k1_w, + "scale_k1_t": scale_k1_t, + "scale_k1": scale_k1, + "max_abs_f_obs_rescaling_diff": max_f_obs_scale_diff, + }, + "resolution_ranges": reconciled_resolution_ranges, + "crystal_symmetry": { + "input_header_cell_tokens": list( + input_context["cell_a_b_c_alpha_beta_gamma"] + ), + "input_header_cell_float64": list(validated_input_cell), + "expected_ccp4_mtz_float32_cell": list(expected_runtime_float32_cell), + "effective_runtime_unit_cell": list(validated_runtime_cell), + "max_abs_runtime_vs_float32_diff": max( + abs(rt - exp) + for rt, exp in zip( + validated_runtime_cell, expected_runtime_float32_cell + ) + ), + "max_abs_runtime_vs_decimal_header_diff": max( + abs(rt - dec) + for rt, dec in zip(validated_runtime_cell, validated_input_cell) + ), + "space_group_symbol": validated_runtime_space_group[ + "space_group_symbol" + ], + "space_group_number": validated_runtime_space_group[ + "space_group_number" + ], + "raw_space_group_operators_xyz": validated_runtime_space_group[ + "raw_space_group_operators_xyz" + ], + "normalized_space_group_operators_xyz": validated_runtime_space_group[ + "normalized_space_group_operators_xyz" + ], + "normalized_lookup_symbol": validated_runtime_space_group[ + "normalized_lookup_symbol" + ], + "symmetry_normalization_policy": validated_runtime_space_group[ + "symmetry_normalization_policy" + ], + "precision_provenance": ( + "ccp4io/libccp4/ccp4/mtzdata.h:86 and cmtzlib.c:476,549-551 store " + "MTZ CELL/DCELL header parameters as IEEE-754 float32 and " + "iotbx/mtz/crystal.cpp:61-67 promotes them to float64; strict " + "1e-12 stage resolution replay uses the validated effective " + "runtime unit cell." + ), + }, + "xray_structure_invariance": dict(xrs_check), + "map_coefficients_summary": map_summary, + "mtz_supplements": dict(mtz_supplements), + } + + +def _verify_no_protstruct_overrides() -> None: + overrides = sorted( + name for name in os.environ if name.startswith("PROTSTRUCT_") + ) + if overrides: + raise ValueError( + "Canary measurement requires default repository toolchain resolution " + f"with no PROTSTRUCT_* environment overrides; found {overrides}" + ) + + +def run_phenix_rfactor_operation( + evidence_root: Path, + subject_id: str = "R4", + *, + repo_root: Path = REPO_ROOT, + archive_path: Path | None = None, + timeout: float = 900.0, + require_no_protstruct_overrides: bool = True, + runner: Callable[..., Any] | None = None, +) -> dict[str, Any]: + """Stage ``subject_id`` inputs and run the cached PHENIX first-block R-factor worker. + + Uses :func:`refinement_cache.cached_phenix_operation` via the default + toolchain resolver, binds the helper and installed PHENIX Python module + sources into the operation identity, verifies that staged inputs remain + byte-identical after execution, and reconciles the full-precision output. + """ + if require_no_protstruct_overrides: + _verify_no_protstruct_overrides() + + staged = stage_subject_inputs( + evidence_root, + subject_id=subject_id, + repo_root=repo_root, + archive_path=archive_path, + ) + fresh_root: Path = staged["evidence_root"] + original_pdb_path: Path = staged["original_pdb_path"] + original_mtz_path: Path = staged["original_mtz_path"] + projected_mtz_path: Path = staged["projected_mtz_path"] + input_ledger_path: Path = staged["input_ledger_path"] + input_ledger: dict[str, Any] = staged["input_ledger"] + + phenix_dir = fresh_root / "phenix" + phenix_dir.mkdir(parents=True, exist_ok=False) + preflight_build_json_path = ( + phenix_dir / "phenix_preflight_build_evidence.json" + ) + admission_failure_json_path = ( + phenix_dir / "phenix_build_admission_failure.json" + ) + operation_failure_json_path = ( + phenix_dir / "phenix_operation_failure.json" + ) + params_path = phenix_dir / "phenix_operation_parameters.json" + reconciled_json_path = phenix_dir / "phenix_reconciled_manifest.json" + cache_dir = phenix_dir / "cache" + + raw_build_evidence = toolchain.phenix_build_evidence() + build_info: Any = ( + dict(raw_build_evidence) + if isinstance(raw_build_evidence, Mapping) + else raw_build_evidence + ) + onesar_phenix_measure._atomic_write_json_checkpoint( + build_info, preflight_build_json_path + ) + preflight_build_sha256 = _sha256_regular_file(preflight_build_json_path) + + preflight_stage = "validate_measured_phenix_build" + try: + validated_preflight = validate_measured_phenix_build( + build_info, expected_version=toolchain.PHENIX_VERSION + ) + expected_resolved_prefix = validated_preflight["resolved_prefix"] + + preflight_stage = "resolve_phenix_vendor_source_files" + vendor_sources = resolve_phenix_vendor_source_files( + Path(expected_resolved_prefix) + ) + vendor_source_sha256 = { + key: _sha256_regular_file(path) + for key, path in vendor_sources.items() + } + except Exception as exc: + admission_failure_record = { + "schema_version": 1, + "status": "rejected", + "subject_id": subject_id, + "expected_phenix_version": toolchain.PHENIX_VERSION, + "preflight_build_evidence_json_path": str( + preflight_build_json_path + ), + "preflight_build_evidence_sha256": preflight_build_sha256, + "measured_build": copy.deepcopy(build_info), + "failure": { + "failed_stage": preflight_stage, + "exception_type": type(exc).__name__, + "exception_message": str(exc), + }, + "worker_launched": False, + } + onesar_phenix_measure._atomic_write_json_checkpoint( + admission_failure_record, admission_failure_json_path + ) + raise + + helper_script = (_SCRIPTS_DIR / "onesar_phenix_measure.py").resolve(strict=True) + _sha256_regular_file(helper_script) + + operation_parameters = { + "schema_version": 1, + "algorithm": "mmtbx.model_vs_data.first_block_public_api", + "subject_id": subject_id, + "expected_resolved_prefix": expected_resolved_prefix, + "expected_phenix_version": toolchain.PHENIX_VERSION, + "labels": list(OBSERVATION_LABELS[:2]), + "r_free_flags_label": OBSERVATION_LABELS[2], + "test_flag_value": FREE_FLAG_VALUE, + "r_free_flags_generate": False, + "ignore_r_free_flags": False, + "keep_going": False, + "scattering_table": "n_gaussian", + "twin_law": None, + "update_f_part1": True, + "apply_pdb_header_resolution_cut": False, + "map_coefficients": { + "acentrics_scale": 2.0, + "centrics_pre_scale": 1.0, + "exclude_free_r_reflections": False, + "fill_missing": False, + "isotropize": True, + "sharp": False, + "phases_prefix": "PH", + }, + } + onesar_phenix_measure.validate_parameters_contract(operation_parameters) + onesar_phenix_measure._atomic_write_json_checkpoint( + operation_parameters, params_path + ) + + input_files: dict[str, Path] = { + "model_pdb": original_pdb_path, + "projected_observations_mtz": projected_mtz_path, + "helper_script": helper_script, + "parameters_json": params_path, + **vendor_sources, + } + arguments: list[str | Path | refinement_cache.OutputTemplate] = [ + helper_script, + "--pdb", + original_pdb_path, + "--mtz", + projected_mtz_path, + "--params-json", + params_path, + "--out-json", + refinement_cache.OutputTemplate("{operation_dir}/phenix_rfactors.json"), + "--out-maps-mtz", + refinement_cache.OutputTemplate("{operation_dir}/phx_maps.mtz"), + "--out-obs-pair-mtz", + refinement_cache.OutputTemplate("{operation_dir}/PHX_OBS_PAIR_ONLY.mtz"), + "--out-diagnostics-json", + refinement_cache.OutputTemplate("{operation_dir}/phenix_diagnostics.json"), + "--out-partial-json", + refinement_cache.OutputTemplate( + "{operation_dir}/phenix_rfactors_partial_evidence.json" + ), + ] + required_outputs: dict[str, str] = { + "phenix_rfactors_json": "phenix_rfactors.json", + "phx_maps_mtz": "phx_maps.mtz", + "phx_obs_pair_only_mtz": "PHX_OBS_PAIR_ONLY.mtz", + "phenix_diagnostics_json": "phenix_diagnostics.json", + } + operation_name = f"onesar_phenix_first_block_rfactors_{subject_id}" + + def _retain_parent_operation_failure( + attempt_dir: Path | None, + exc: Exception, + *, + failed_stage: str, + ) -> None: + retained_artifacts: dict[str, dict[str, str]] = {} + diagnostics_summary: dict[str, Any] | None = None + partial_summary: dict[str, Any] | None = None + if attempt_dir is not None and attempt_dir.is_dir(): + candidate_files = { + "stdout_log": attempt_dir / "stdout.log", + "invocation_json": attempt_dir / "invocation.json", + "phenix_diagnostics_json": ( + attempt_dir / "phenix_diagnostics.json" + ), + "phenix_partial_evidence_json": ( + attempt_dir / "phenix_rfactors_partial_evidence.json" + ), + "phx_maps_mtz": attempt_dir / "phx_maps.mtz", + "phx_obs_pair_only_mtz": attempt_dir / "PHX_OBS_PAIR_ONLY.mtz", + "phenix_rfactors_json": attempt_dir / "phenix_rfactors.json", + "manifest_json": attempt_dir / "success.json", + } + for label, cand_path in candidate_files.items(): + if cand_path.is_file() and not cand_path.is_symlink(): + retained_artifacts[label] = { + "path": str(cand_path), + "sha256": _sha256_regular_file(cand_path), + } + diag_cand = candidate_files["phenix_diagnostics_json"] + if "phenix_diagnostics_json" in retained_artifacts: + try: + diag_doc = json.loads( + diag_cand.read_text(encoding="utf-8") + ) + if isinstance(diag_doc, Mapping): + diagnostics_summary = { + "status": diag_doc.get("status"), + "current_phase": diag_doc.get("current_phase"), + "completed_raw_stages": diag_doc.get( + "completed_raw_stages" + ), + "partial_evidence_status": diag_doc.get( + "partial_evidence_status" + ), + "failure": diag_doc.get("failure"), + } + except Exception: + diagnostics_summary = None + partial_cand = candidate_files["phenix_partial_evidence_json"] + if "phenix_partial_evidence_json" in retained_artifacts: + try: + partial_doc = json.loads( + partial_cand.read_text(encoding="utf-8") + ) + if isinstance(partial_doc, Mapping): + partial_summary = { + "artifact_role": partial_doc.get("artifact_role"), + "status": partial_doc.get("status"), + "current_phase": partial_doc.get("current_phase"), + "completed_stages": partial_doc.get( + "completed_stages" + ), + "rfactor_replay_verified": partial_doc.get( + "rfactor_replay_verified" + ), + "map_export_verified": partial_doc.get( + "map_export_verified" + ), + "failure": partial_doc.get("failure"), + } + except Exception: + partial_summary = None + + failure_manifest = { + "schema_version": 1, + "status": "failed", + "reusable_cache_output": False, + "canonical_eval_qds_eligible": False, + "subject_id": subject_id, + "failed_stage": failed_stage, + "attempt_directory": ( + str(attempt_dir) if attempt_dir is not None else None + ), + "returncode": getattr(exc, "returncode", None), + "exception_type": type(exc).__name__, + "exception_message": str(exc), + "preflight_build_evidence_json_path": str( + preflight_build_json_path + ), + "preflight_build_evidence_sha256": preflight_build_sha256, + "retained_attempt_artifacts": retained_artifacts, + "diagnostics_summary": diagnostics_summary, + "partial_evidence_summary": partial_summary, + } + onesar_phenix_measure._atomic_write_json_checkpoint( + failure_manifest, operation_failure_json_path + ) + + try: + try: + artifact_paths = refinement_cache.cached_phenix_operation( + cache_dir, + operation_name, + executable="phenix.python", + inputs=input_files, + arguments=arguments, + required_outputs=required_outputs, + timeout=timeout, + runner=runner, + ) + except Exception as exc: + log_p = getattr(exc, "log_path", None) + attempt_p = log_p.parent if isinstance(log_p, Path) else None + _retain_parent_operation_failure( + attempt_p, exc, failed_stage="cached_phenix_operation" + ) + raise + finally: + # Verify staged inputs were never mutated during execution. + if ( + _sha256_regular_file(original_pdb_path) + != staged["admission_record"]["staged_files"]["original_model_pdb_sha256"] + ): + raise ValueError("Staged original_model.pdb mutated during PHENIX operation") + if ( + _sha256_regular_file(original_mtz_path) + != staged["admission_record"]["staged_files"]["original_data_mtz_sha256"] + ): + raise ValueError("Staged original_data.mtz mutated during PHENIX operation") + if ( + _sha256_regular_file(projected_mtz_path) + != staged["admission_record"]["staged_files"][ + "projected_observations_mtz_sha256" + ] + ): + raise ValueError( + "Staged projected_observations.mtz mutated during PHENIX operation" + ) + if ( + _sha256_regular_file(input_ledger_path) + != staged["admission_record"]["staged_files"][ + "input_hkl_ledger_json_sha256" + ] + ): + raise ValueError( + "Staged input_hkl_ledger.json mutated during PHENIX operation" + ) + + manifest_path = artifact_paths["manifest"] + output_json_path = artifact_paths["phenix_rfactors_json"] + output_maps_mtz_path = artifact_paths["phx_maps_mtz"] + output_obs_pair_mtz_path = artifact_paths["phx_obs_pair_only_mtz"] + output_diagnostics_json_path = artifact_paths["phenix_diagnostics_json"] + stdout_log_path = artifact_paths["stdout"] + bundle_partial_json_path = ( + output_diagnostics_json_path.parent + / "phenix_rfactors_partial_evidence.json" + ) + + try: + manifest_record = json.loads(manifest_path.read_text(encoding="utf-8")) + if ( + not isinstance(manifest_record, Mapping) + or type(manifest_record.get("returncode")) is not int + or manifest_record["returncode"] != 0 + or manifest_record.get("reusable") is not True + or not output_json_path.is_file() + ): + raise RuntimeError( + f"PHENIX R-factor operation manifest reported invalid/non-reusable status: " + f"returncode={manifest_record.get('returncode')!r}, " + f"reusable={manifest_record.get('reusable')!r} at {manifest_path}" + ) + + invocation = manifest_record.get("invocation") + if not isinstance(invocation, Mapping): + raise ValueError("Cache manifest missing invocation record") + validated_manifest_build = validate_measured_phenix_build( + invocation.get("measured_build", {}), + expected_version=toolchain.PHENIX_VERSION, + ) + if ( + validated_manifest_build["build_info"] + != validated_preflight["build_info"] + or validated_manifest_build["resolved_prefix"] + != expected_resolved_prefix + ): + raise ValueError( + "Cache manifest measured_build drifted from preflight PHENIX build evidence" + ) + launcher_info = invocation.get("launcher") + if ( + not isinstance(launcher_info, Mapping) + or not isinstance(launcher_info.get("path"), str) + or not Path(launcher_info["path"]).is_relative_to( + Path(expected_resolved_prefix) + ) + ): + raise ValueError( + f"Cache manifest launcher {launcher_info!r} is not inside " + f"{expected_resolved_prefix}" + ) + invocation_inputs = invocation.get("inputs") + if not isinstance(invocation_inputs, Mapping): + raise ValueError("Cache manifest invocation missing inputs") + for mod_key, mod_path in vendor_sources.items(): + if invocation_inputs.get(mod_key) != { + "path": str(mod_path), + "sha256": vendor_source_sha256[mod_key], + }: + raise ValueError( + f"Cache manifest input {mod_key} does not match preflight vendor source" + ) + + manifest_artifacts = manifest_record.get("artifacts") + if not isinstance(manifest_artifacts, Mapping): + raise ValueError("Cache manifest missing artifacts mapping") + if ( + _sha256_regular_file(output_json_path) + != manifest_artifacts.get("phenix_rfactors_json", {}).get("sha256") + ): + raise ValueError( + "phenix_rfactors.json SHA-256 disagrees with cache manifest" + ) + + phenix_payload = json.loads( + output_json_path.read_text(encoding="utf-8") + ) + reconciliation = reconcile_phenix_rfactor_output( + phenix_payload, + input_ledger, + expected_vendor_source_sha256=vendor_source_sha256, + expected_vendor_module_paths=vendor_sources, + expected_resolved_prefix=expected_resolved_prefix, + expected_phenix_version=toolchain.PHENIX_VERSION, + measured_build=invocation["measured_build"], + phx_maps_mtz_path=output_maps_mtz_path, + phx_obs_pair_only_mtz_path=output_obs_pair_mtz_path, + phenix_diagnostics_path=output_diagnostics_json_path, + cache_manifest_artifacts=manifest_artifacts, + ) + except Exception as exc: + _retain_parent_operation_failure( + output_diagnostics_json_path.parent, + exc, + failed_stage="reconcile_phenix_rfactor_output", + ) + raise + + reconciled_record: dict[str, Any] = { + "schema_version": 1, + "subject": staged["admission_record"]["subject"], + "mtz": staged["admission_record"]["mtz"], + "staged_files": staged["admission_record"]["staged_files"], + "input_hkl_ledger_json_path": str(input_ledger_path), + "input_hkl_ledger_json_sha256": staged["admission_record"]["staged_files"][ + "input_hkl_ledger_json_sha256" + ], + "preflight_build_evidence_json_path": str(preflight_build_json_path), + "preflight_build_evidence_sha256": _sha256_regular_file( + preflight_build_json_path + ), + "cache_artifacts": { + key: str(path) for key, path in sorted(artifact_paths.items()) + }, + "manifest_path": str(manifest_path), + "manifest_sha256": _sha256_regular_file(manifest_path), + "stdout_log_path": str(stdout_log_path), + "phenix_rfactors_json_path": str(output_json_path), + "phenix_diagnostics_json_path": str(output_diagnostics_json_path), + "phenix_partial_evidence_json_path": ( + str(bundle_partial_json_path) + if bundle_partial_json_path.is_file() + else None + ), + "phenix_partial_evidence_sha256": ( + _sha256_regular_file(bundle_partial_json_path) + if bundle_partial_json_path.is_file() + else None + ), + "phx_maps_mtz_path": str(output_maps_mtz_path), + "phx_obs_pair_only_mtz_path": str(output_obs_pair_mtz_path), + "operation_manifest": manifest_record, + "reconciliation": reconciliation, + } + onesar_phenix_measure._atomic_write_json_checkpoint( + reconciled_record, reconciled_json_path + ) + return reconciled_record diff --git a/scripts/test_onesar_refmac_log.py b/scripts/test_onesar_refmac_log.py new file mode 100644 index 0000000..f2ef8fa --- /dev/null +++ b/scripts/test_onesar_refmac_log.py @@ -0,0 +1,1449 @@ +#!/usr/bin/env python3 +"""Hermetic unit tests for scripts/onesar_refmac_log.py. + +All fixtures in this test suite are self-contained in-memory strings with +explicit provenance labels (historical AMPLE REFMAC 5.8.0049 source excerpt +and synthetic test cases). No installed vendor tools, external files, or +network access are required. +""" + +from __future__ import annotations + +import json +import sys +import unittest +from pathlib import Path + +_SCRIPTS_DIR = Path(__file__).resolve().parent +if str(_SCRIPTS_DIR) not in sys.path: + sys.path.insert(0, str(_SCRIPTS_DIR)) + +from onesar_refmac_log import parse_refmac_log # noqa: E402 + + +# Provenance: Verbatim structural excerpt from the historical AMPLE test fixture +# (`ccp4-9/share/ample/testfiles/refmac.log`, SHA-256 +# 15593c60819187660ab7a267ed0232a67bb016262619de06bf56644378df2c2a, +# CCP4 6.4 / REFMAC 5.8.0049, run date 15/ 7/2014, input 1BYZ-cad.mtz, NCYC 30). +# Used strictly as parser-shape evidence, never as 1SAR or current-tool evidence. +HISTORICAL_AMPLE_EXCERPT_LOG = """\ + +
+ ### CCP4 6.4: Refmac_5.8.0049     version 5.8.0049 : 30/06/13##
+ User: unknown  Run date: 15/ 7/2014 Run time: 21:14:02 
+ $TEXT:Reference1: $$ Main reference $$ 
+   "REFMAC5 for the refinement of macromolecular crystal structures:"
+ $$
+ $SUMMARY :Reference1:  $$ Refmac: $$
+ :TEXT:Reference1: $$
+  Data line--- LABIN FP=FP SIGFP=SIGFP FREE=FREE
+  Data line--- NCYC 30
+  Data line--- END
+  Refinement cycles       :    30
+
+$TABLE: Cycle    1. Rfactor analysis, F distribution v resln  :
+$GRAPHS:Cycle    1.  v. resln :N:1,6,7,11,12:
+:Cycle    1.  and  v. resln :N:1,4,5,9,10:
+:Cycle    1. % observed v. resln :N:1,3:
+$$
+M(4SSQ/LL) NR_used %_obs M(Fo_used) M(Fc_used) Rf_used WR_used
+NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$
+$$
+ 0.032     293  99.36    39.1    31.2  0.54  0.77      19    24.6    36.2  0.76  1.48
+ 0.093     548 100.00    35.1    25.7  0.53  0.61      35    32.9    26.5  0.53  0.59
+ 0.154     675 100.00    28.2    23.8  0.51  0.62      35    23.7    21.4  0.58  0.70
+ 0.215     853 100.00    23.6    18.9  0.55  0.65      39    25.4    18.8  0.54  0.59
+ 0.276     920 100.00    18.0    14.4  0.55  0.60      48    16.2    13.9  0.47  0.55
+ 0.338    1031 100.00    14.7    11.9  0.51  0.54      70    13.6    11.4  0.51  0.53
+ 0.399    1106 100.00    13.4     9.9  0.55  0.55      68    13.1    10.1  0.45  0.45
+ 0.460    1193 100.00    12.3     9.5  0.52  0.54      68    10.4     9.3  0.45  0.47
+ 0.521    1279  99.85    10.5     8.3  0.55  0.55      72    10.8     8.7  0.46  0.48
+ 0.582    1356  99.72    10.0     8.0  0.53  0.52      75    11.8     7.8  0.58  0.58
+ 0.643    1364  99.18     9.8     7.6  0.55  0.55      82     9.2     7.2  0.52  0.53
+ 0.704    1473  98.72     9.1     6.8  0.55  0.54      70     9.7     6.5  0.56  0.54
+ 0.765    1546  97.89     8.9     6.3  0.56  0.56      79     8.0     5.8  0.55  0.53
+ 0.826    1484  92.33     7.9     5.9  0.54  0.53      82     8.0     5.9  0.55  0.52
+ 0.887    1434  87.55     7.5     5.5  0.55  0.53      85     7.6     5.1  0.57  0.54
+ 0.948    1468  82.59     6.5     4.9  0.54  0.52      50     5.7     5.3  0.45  0.43
+ 1.010    1351  75.18     5.4     4.1  0.57  0.54      73     4.7     4.5  0.64  0.60
+ 1.071    1200  67.16     4.4     3.5  0.57  0.56      70     4.2     3.8  0.53  0.50
+ 1.132    1143  58.99     3.9     3.0  0.53  0.53      51     3.5     3.0  0.57  0.60
+ 1.193     750  39.07     3.8     2.6  0.53  0.54      42     3.9     2.5  0.51  0.53
+$$
+
+ $TABLE: Cycle    1. Fom(-acentric, centric, overall v resln:
+ $GRAPHS:Cycle    1. M(Fom) v. resln :N:1,3,5,7,8:
+ $$
+ <4SSQ/LL> NREFa  FOMa  NREFc FOMc NREFall FOMall  SigmaA_Fc1 $$
+ $$
+  0.0321   293   0.567     0   0.000   293   0.567  0.646
+  0.0932   548   0.567     0   0.000   548   0.567  0.658
+  0.1542   675   0.572     0   0.000   675   0.572  0.667
+  0.2153   853   0.511     0   0.000   853   0.511  0.675
+  0.2764   920   0.442     0   0.000   920   0.442  0.681
+  0.3375  1031   0.422     0   0.000  1031   0.422  0.693
+  0.3986  1106   0.447     0   0.000  1106   0.447  0.729
+  0.4597  1193   0.483     0   0.000  1193   0.483  0.721
+  0.5208  1279   0.316     0   0.000  1279   0.316  0.524
+  0.5819  1356   0.246     0   0.000  1356   0.246  0.430
+  0.6430  1364   0.282     0   0.000  1364   0.282  0.529
+  0.7041  1473   0.307     0   0.000  1473   0.307  0.580
+  0.7652  1546   0.333     0   0.000  1546   0.333  0.574
+  0.8262  1484   0.270     0   0.000  1484   0.270  0.489
+  0.8873  1434   0.219     0   0.000  1434   0.219  0.410
+  0.9484  1468   0.304     0   0.000  1468   0.304  0.537
+  1.0095  1351   0.409     0   0.000  1351   0.409  0.642
+  1.0706  1200   0.256     0   0.000  1200   0.256  0.445
+  1.1317  1143   0.279     0   0.000  1143   0.279  0.478
+  1.1928   750   0.193     0   0.000   750   0.193  0.361
+ $$
+Resolution limits                    =     25.690     0.904
+Number of used reflections           =      22467
+Percentage observed                  =    85.8624
+Percentage of free reflections       =     5.1225
+Overall R factor                     =     0.5397
+Free R factor                        =     0.5317
+Overall weighted R factor            =     0.5603
+Free weighted R factor               =     0.5489
+Overall weighted R2 factor           =     0.7148
+Free weighted R2 factor              =     0.7169
+Overall figure of merit              =     0.3465
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5380
+Free R factor                        =     0.5313
+Overall figure of merit              =     0.3343
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5363
+Free R factor                        =     0.5315
+Overall figure of merit              =     0.3314
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5352
+Free R factor                        =     0.5321
+Overall figure of merit              =     0.3295
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5344
+Free R factor                        =     0.5327
+Overall figure of merit              =     0.3301
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5338
+Free R factor                        =     0.5330
+Overall figure of merit              =     0.3294
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5334
+Free R factor                        =     0.5333
+Overall figure of merit              =     0.3285
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5330
+Free R factor                        =     0.5336
+Overall figure of merit              =     0.3282
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5327
+Free R factor                        =     0.5339
+Overall figure of merit              =     0.3273
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5324
+Free R factor                        =     0.5343
+Overall figure of merit              =     0.3269
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5322
+Free R factor                        =     0.5344
+Overall figure of merit              =     0.3259
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5319
+Free R factor                        =     0.5345
+Overall figure of merit              =     0.3246
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5315
+Free R factor                        =     0.5345
+Overall figure of merit              =     0.3239
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5311
+Free R factor                        =     0.5345
+Overall figure of merit              =     0.3235
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5307
+Free R factor                        =     0.5346
+Overall figure of merit              =     0.3237
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5302
+Free R factor                        =     0.5342
+Overall figure of merit              =     0.3226
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5297
+Free R factor                        =     0.5338
+Overall figure of merit              =     0.3219
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5291
+Free R factor                        =     0.5331
+Overall figure of merit              =     0.3213
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5283
+Free R factor                        =     0.5326
+Overall figure of merit              =     0.3222
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5274
+Free R factor                        =     0.5321
+Overall figure of merit              =     0.3225
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5265
+Free R factor                        =     0.5316
+Overall figure of merit              =     0.3227
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5258
+Free R factor                        =     0.5308
+Overall figure of merit              =     0.3233
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5251
+Free R factor                        =     0.5301
+Overall figure of merit              =     0.3232
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5245
+Free R factor                        =     0.5295
+Overall figure of merit              =     0.3256
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5239
+Free R factor                        =     0.5291
+Overall figure of merit              =     0.3288
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5234
+Free R factor                        =     0.5287
+Overall figure of merit              =     0.3318
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5228
+Free R factor                        =     0.5287
+Overall figure of merit              =     0.3347
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5222
+Free R factor                        =     0.5286
+Overall figure of merit              =     0.3394
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5217
+Free R factor                        =     0.5286
+Overall figure of merit              =     0.3400
+-----------------------------------------------------------------------------
+Overall R factor                     =     0.5211
+Free R factor                        =     0.5288
+Overall figure of merit              =     0.3401
+-----------------------------------------------------------------------------
+
+$TABLE: Cycle   31. Rfactor analysis, F distribution v resln  :
+$GRAPHS:Cycle   31.  v. resln :N:1,6,7,11,12:
+:Cycle   31.  and  v. resln :N:1,4,5,9,10:
+:Cycle   31. % observed v. resln :N:1,3:
+$$
+M(4SSQ/LL) NR_used %_obs M(Fo_used) M(Fc_used) Rf_used WR_used
+NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$
+$$
+ 0.032     293  99.36    39.1    31.1  0.53  0.77      19    24.6    35.0  0.74  1.36
+ 0.093     548 100.00    35.1    26.5  0.52  0.61      35    32.9    26.5  0.58  0.64
+ 0.154     675 100.00    28.2    25.1  0.49  0.62      35    23.7    23.8  0.60  0.74
+ 0.215     853 100.00    23.6    19.6  0.52  0.62      39    25.4    20.0  0.50  0.58
+ 0.276     920 100.00    18.0    14.2  0.52  0.56      48    16.2    13.1  0.49  0.56
+ 0.338    1031 100.00    14.7    11.4  0.51  0.54      70    13.6    11.0  0.50  0.51
+ 0.399    1106 100.00    13.4    10.2  0.53  0.54      68    13.1    10.7  0.44  0.46
+ 0.460    1193 100.00    12.3    10.0  0.50  0.51      68    10.4     9.2  0.42  0.44
+ 0.521    1279  99.85    10.5     8.7  0.51  0.52      72    10.8     9.7  0.47  0.50
+ 0.582    1356  99.72    10.0     8.4  0.51  0.50      75    11.8     8.3  0.52  0.50
+ 0.643    1364  99.18     9.8     7.5  0.53  0.52      82     9.2     7.5  0.53  0.50
+ 0.704    1473  98.72     9.1     6.8  0.53  0.52      70     9.7     6.5  0.55  0.53
+ 0.765    1546  97.89     8.8     6.5  0.55  0.55      79     8.0     6.2  0.53  0.51
+ 0.826    1484  92.33     7.9     6.2  0.53  0.52      82     8.0     5.7  0.53  0.52
+ 0.887    1434  87.55     7.5     5.7  0.51  0.49      85     7.6     5.3  0.52  0.50
+ 0.948    1468  82.59     6.5     4.9  0.52  0.50      50     5.7     4.9  0.60  0.56
+ 1.010    1351  75.18     5.4     4.2  0.56  0.54      73     4.7     4.4  0.65  0.60
+ 1.071    1200  67.16     4.4     3.7  0.56  0.55      70     4.2     3.4  0.53  0.51
+ 1.132    1143  58.99     3.9     3.1  0.55  0.55      51     3.5     3.6  0.61  0.57
+ 1.193     750  39.07     3.8     2.7  0.53  0.53      42     3.9     2.9  0.55  0.58
+$$
+
+ $TABLE: Cycle   31. Fom(-acentric, centric, overall v resln:
+ $GRAPHS:Cycle   31. M(Fom) v. resln :N:1,3,5,7,8:
+ $$
+ <4SSQ/LL> NREFa  FOMa  NREFc FOMc NREFall FOMall  SigmaA_Fc1 $$
+ $$
+  0.0321   293   0.581     0   0.000   293   0.581  0.654
+  0.0932   548   0.596     0   0.000   548   0.596  0.672
+  0.1542   675   0.611     0   0.000   675   0.611  0.685
+  0.2153   853   0.540     0   0.000   853   0.540  0.696
+  0.2764   920   0.456     0   0.000   920   0.456  0.705
+  0.3375  1031   0.421     0   0.000  1031   0.421  0.718
+  0.3986  1106   0.471     0   0.000  1106   0.471  0.751
+  0.4597  1193   0.531     0   0.000  1193   0.531  0.740
+  0.5208  1279   0.344     0   0.000  1279   0.344  0.531
+  0.5819  1356   0.222     0   0.000  1356   0.222  0.364
+  0.6430  1364   0.165     0   0.000  1364   0.165  0.304
+  0.7041  1473   0.261     0   0.000  1473   0.261  0.470
+  0.7652  1546   0.370     0   0.000  1546   0.370  0.602
+  0.8262  1484   0.269     0   0.000  1484   0.269  0.463
+  0.8873  1434   0.235     0   0.000  1434   0.235  0.409
+  0.9484  1468   0.282     0   0.000  1468   0.282  0.496
+  1.0095  1351   0.353     0   0.000  1351   0.353  0.588
+  1.0706  1200   0.308     0   0.000  1200   0.308  0.501
+  1.1317  1143   0.225     0   0.000  1143   0.225  0.387
+  1.1928   750   0.136     0   0.000   750   0.136  0.246
+ $$
+Resolution limits                    =     25.690     0.904
+Number of used reflections           =      22467
+Percentage observed                  =    85.8624
+Percentage of free reflections       =     5.1225
+Overall R factor                     =     0.5207
+Free R factor                        =     0.5289
+Overall weighted R factor            =     0.5425
+Free weighted R factor               =     0.5425
+Overall weighted R2 factor           =     0.6996
+Free weighted R2 factor              =     0.6953
+Overall correlation coefficient      =     0.6076
+Free correlation coefficient         =     0.5684
+Cruickshanks DPI for coordinate error=     0.0241
+DPI based on free R factor           =     0.0244
+Overall figure of merit              =     0.3407
+ML based su of positional parameters =     0.0589
+ML based su of thermal parameters    =     1.0064
+-----------------------------------------------------------------------------
+
+$TABLE: Rfactor analysis, stats vs cycle  :
+$GRAPHS: vs cycle :N:1,2,3:
+:FOM vs cycle :N:1,4:
+:-LL vs cycle :N:1,5:
+:-LLfree vs cycle :N:1,6:
+:Geometry vs cycle:N:1,7,8,9,10,11:
+$$
+    Ncyc    Rfact    Rfree     FOM      -LL     -LLfree  rmsBOND  zBOND rmsANGL  zANGL rmsCHIRAL $$
+$$
+       0   0.5397   0.5317   0.347     66009.    3533.2   0.0193  0.987   3.189  1.772   0.092
+       1   0.5380   0.5313   0.334     66023.    3532.7   0.0108  0.528   2.391  1.304   0.081
+       2   0.5363   0.5315   0.331     66014.    3532.9   0.0088  0.422   2.037  1.111   0.056
+       3   0.5352   0.5321   0.329     65921.    3528.3   0.0075  0.359   1.844  1.008   0.036
+       4   0.5344   0.5327   0.330     65815.    3523.4   0.0064  0.311   1.713  0.940   0.023
+       5   0.5338   0.5330   0.329     65768.    3521.5   0.0057  0.274   1.614  0.887   0.018
+       6   0.5334   0.5333   0.329     65708.    3518.9   0.0051  0.245   1.532  0.843   0.019
+       7   0.5330   0.5336   0.328     65709.    3519.7   0.0046  0.223   1.462  0.805   0.021
+       8   0.5327   0.5339   0.327     65762.    3523.2   0.0042  0.206   1.400  0.771   0.024
+       9   0.5324   0.5343   0.327     65720.    3521.4   0.0040  0.192   1.345  0.741   0.026
+      10   0.5322   0.5344   0.326     65744.    3523.5   0.0037  0.182   1.296  0.713   0.028
+      11   0.5319   0.5345   0.325     65745.    3524.1   0.0036  0.173   1.250  0.688   0.029
+      12   0.5315   0.5345   0.324     65724.    3523.4   0.0034  0.166   1.206  0.662   0.029
+      13   0.5311   0.5345   0.324     65753.    3525.6   0.0033  0.159   1.164  0.639   0.029
+      14   0.5307   0.5346   0.324     65784.    3527.6   0.0031  0.154   1.126  0.617   0.030
+      15   0.5302   0.5342   0.323     65865.    3531.8   0.0031  0.151   1.091  0.597   0.030
+      16   0.5297   0.5338   0.322     65905.    3534.0   0.0030  0.150   1.059  0.579   0.030
+      17   0.5291   0.5331   0.321     65958.    3536.7   0.0030  0.150   1.028  0.562   0.029
+      18   0.5283   0.5326   0.322     65955.    3536.5   0.0029  0.149   1.000  0.545   0.029
+      19   0.5274   0.5321   0.323     65901.    3532.7   0.0029  0.149   0.972  0.529   0.029
+      20   0.5265   0.5316   0.323     65870.    3530.9   0.0029  0.150   0.946  0.515   0.029
+      21   0.5258   0.5308   0.323     65814.    3527.7   0.0029  0.151   0.921  0.500   0.028
+      22   0.5251   0.5301   0.323     65800.    3525.9   0.0029  0.152   0.897  0.487   0.028
+      23   0.5245   0.5295   0.326     65799.    3525.3   0.0029  0.153   0.874  0.474   0.028
+      24   0.5239   0.5291   0.329     65806.    3525.1   0.0029  0.154   0.851  0.462   0.027
+      25   0.5234   0.5287   0.332     65841.    3527.3   0.0029  0.155   0.829  0.450   0.027
+      26   0.5228   0.5287   0.335     65771.    3522.6   0.0029  0.156   0.809  0.438   0.026
+      27   0.5222   0.5286   0.339     65628.    3514.3   0.0029  0.156   0.789  0.427   0.026
+      28   0.5217   0.5286   0.340     65520.    3508.0   0.0029  0.156   0.769  0.417   0.025
+      29   0.5211   0.5288   0.340     65506.    3506.9   0.0029  0.156   0.751  0.407   0.025
+      30   0.5207   0.5289   0.341     65565.    3509.9   0.0028  0.155   0.733  0.397   0.024
+ $$
+ $TEXT:Result: $$ Final results $$
+                      Initial    Final
+           R factor    0.5397   0.5207
+             R free    0.5317   0.5289
+     Rms BondLength    0.0193   0.0028
+      Rms BondAngle    3.1891   0.7330
+     Rms ChirVolume    0.0916   0.0240
+ $$
+"""
+
+
+# Provenance: Purely synthetic single-cycle (NCYC 0) log fixture testing the
+# Cycle 1 / Ncyc 0 correspondence where detailed resolution table Cycle 1
+# corresponds to cycle-summary row Ncyc 0.
+SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG = """\
+ ### CCP4 9.0: Refmac_5.8.0430     version 5.8.0430 : 01/01/24##
+  Data line--- NCYC 0
+  Refinement cycles       :     0
+
+$TABLE: Cycle    1. Rfactor analysis, F distribution v resln  :
+$GRAPHS:Cycle    1.  v. resln :N:1,6,7,11,12:
+$$
+M(4SSQ/LL) NR_used %_obs M(Fo_used) M(Fc_used) Rf_used WR_used
+NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$
+$$
+ 0.050    3100  99.50    42.0    40.1  0.18  0.21     160    41.2    39.0  0.22  0.25
+ 0.150    3432  98.10    21.5    20.4  0.20  0.23     184    20.8    19.5  0.24  0.27
+$$
+
+ $TABLE: Cycle    1. Fom(-acentric, centric, overall v resln:
+ $GRAPHS:Cycle    1. M(Fom) v. resln :N:1,3,5,7,8:
+ $$
+ <4SSQ/LL> NREFa  FOMa  NREFc FOMc NREFall FOMall  SigmaA_Fc1 $$
+ $$
+  0.0500  3000   0.850   100   0.780  3100   0.848  0.910
+  0.1500  3332   0.810   100   0.740  3432   0.808  0.885
+ $$
+Resolution limits                    =     35.000     1.800
+Number of used reflections           =       6532
+Percentage observed                  =    98.7500
+Percentage of free reflections       =     5.0029
+Overall R factor                     =     0.1912
+Free R factor                        =     0.2315
+Overall weighted R factor            =     0.2210
+Free weighted R factor               =     0.2610
+Overall weighted R2 factor           =     0.2890
+Free weighted R2 factor              =     0.3310
+Overall figure of merit              =     0.8270
+-----------------------------------------------------------------------------
+
+$TABLE: Rfactor analysis, stats vs cycle  :
+$GRAPHS: vs cycle :N:1,2,3:
+$$
+    Ncyc    Rfact    Rfree     FOM      -LL     -LLfree  rmsBOND  zBOND rmsANGL  zANGL rmsCHIRAL $$
+$$
+       0   0.1912   0.2315   0.827     12345.     654.3   0.0081  0.410   1.120  0.550   0.045
+ $$
+ $TEXT:Result: $$ Final results $$
+                      Initial    Final
+           R factor    0.1912   0.1912
+             R free    0.2315   0.2315
+     Rms BondLength    0.0081   0.0081
+      Rms BondAngle    1.1200   1.1200
+     Rms ChirVolume    0.0450   0.0450
+ $$
+"""
+
+
+class TestOnesarRefmacLogParser(unittest.TestCase):
+    """Hermetic unit tests for parse_refmac_log."""
+
+    def test_historical_ample_excerpt_reconciles_all_cycles_and_bins(self) -> None:
+        parsed = parse_refmac_log(HISTORICAL_AMPLE_EXCERPT_LOG)
+        # Ensure output is completely JSON-serializable
+        roundtrip = json.loads(json.dumps(parsed))
+        self.assertEqual(roundtrip["reconciled_final_cycle"], parsed["reconciled_final_cycle"])
+
+        self.assertEqual(
+            parsed["source_metadata"]["reported_refmac_version"], "5.8.0049"
+        )
+        self.assertEqual(parsed["source_metadata"]["reported_suite"], "CCP4 6.4")
+        self.assertEqual(parsed["source_metadata"]["requested_ncyc"], 30)
+
+        self.assertEqual(
+            [b["id"] for b in parsed["table_blocks"]],
+            [
+                "initial_resolution_rfactor",
+                "initial_resolution_fom",
+                "final_resolution_rfactor",
+                "final_resolution_fom",
+                "cycle_summary",
+            ],
+        )
+
+        # Verify summary and detailed cycles
+        self.assertEqual(parsed["cycle_summary"]["row_count"], 31)
+        self.assertEqual(parsed["cycle_summary"]["summary_cycle_min"], 0)
+        self.assertEqual(parsed["cycle_summary"]["summary_cycle_max"], 30)
+        self.assertEqual(len(parsed["detailed_cycles"]), 31)
+        self.assertTrue(
+            parsed["cycle_r_values"]["all_ordered_detail_pairs_equal_summary_values"]
+        )
+        self.assertTrue(
+            parsed["consistency_checks"][
+                "initial_detail_summary_and_final_results_initial_values_equal"
+            ]
+        )
+        self.assertTrue(
+            parsed["consistency_checks"][
+                "final_detail_summary_and_final_results_final_values_equal"
+            ]
+        )
+        self.assertEqual(
+            parsed["cycle_r_values"]["detail_rfactor_table_cycle_labels"], [1, 31]
+        )
+
+        # Initial and final R pairs and precision
+        init_cyc = parsed["reconciled_initial_cycle"]
+        self.assertEqual(init_cyc["summary_ncyc"], 0)
+        self.assertEqual(init_cyc["detailed_cycle"], 1)
+        self.assertEqual(init_cyc["r_work_token"], "0.5397")
+        self.assertEqual(init_cyc["r_free_token"], "0.5317")
+        self.assertEqual(init_cyc["r_work_decimal_places"], 4)
+        self.assertEqual(init_cyc["r_free_decimal_places"], 4)
+
+        final_cyc = parsed["reconciled_final_cycle"]
+        self.assertEqual(final_cyc["summary_ncyc"], 30)
+        self.assertEqual(final_cyc["detailed_cycle"], 31)
+        self.assertEqual(final_cyc["resolution_table_cycle"], 31)
+        self.assertEqual(final_cyc["r_work_token"], "0.5207")
+        self.assertEqual(final_cyc["r_free_token"], "0.5289")
+        self.assertAlmostEqual(final_cyc["r_work"], 0.5207)
+        self.assertAlmostEqual(final_cyc["r_free"], 0.5289)
+        self.assertEqual(final_cyc["r_work_decimal_places"], 4)
+        self.assertEqual(final_cyc["r_free_decimal_places"], 4)
+        self.assertEqual(final_cyc["nr_used_sum"], 22467)
+        self.assertEqual(final_cyc["nr_free_sum"], 1213)
+        self.assertEqual(final_cyc["combined_reported_count"], 23680)
+
+        # Verify unweighted R is distinct from weighted R, R2, and FOM
+        final_detail = parsed["detailed_cycles"][-1]
+        self.assertEqual(final_detail["overall_r_token"], "0.5207")
+        self.assertEqual(final_detail["free_r_token"], "0.5289")
+        self.assertEqual(final_detail["overall_weighted_r_token"], "0.5425")
+        self.assertEqual(final_detail["free_weighted_r_token"], "0.5425")
+        self.assertEqual(final_detail["overall_weighted_r2_token"], "0.6996")
+        self.assertEqual(final_detail["free_weighted_r2_token"], "0.6953")
+        self.assertEqual(final_detail["overall_fom_token"], "0.3407")
+
+        # Verify per-bin counts and 2-decimal-place R tokens
+        final_bins = parsed["final_resolution_bins"]
+        self.assertEqual(final_bins["bin_count"], 20)
+        self.assertEqual(final_bins["nr_used_sum"], 22467)
+        self.assertEqual(final_bins["nr_free_sum"], 1213)
+        self.assertEqual(final_bins["rf_used_printed_decimal_places"], 2)
+        self.assertEqual(final_bins["rf_free_printed_decimal_places"], 2)
+        self.assertEqual(final_bins["bins"][0]["nr_used"], 293)
+        self.assertEqual(final_bins["bins"][0]["nr_free"], 19)
+        self.assertEqual(final_bins["bins"][0]["rf_used_token"], "0.53")
+        self.assertEqual(final_bins["bins"][0]["rf_free_token"], "0.74")
+        self.assertEqual(final_bins["bins"][0]["wr_free_token"], "1.36")
+        self.assertEqual(final_bins["bins"][-1]["nr_used"], 750)
+        self.assertEqual(final_bins["bins"][-1]["nr_free"], 42)
+        self.assertEqual(final_bins["bins"][-1]["rf_used_token"], "0.53")
+        self.assertEqual(final_bins["bins"][-1]["rf_free_token"], "0.55")
+
+    def test_single_cycle_cycle1_ncyc0_correspondence(self) -> None:
+        parsed = parse_refmac_log(SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG)
+        self.assertEqual(parsed["cycle_summary"]["row_count"], 1)
+        self.assertEqual(parsed["cycle_summary"]["summary_cycle_min"], 0)
+        self.assertEqual(parsed["cycle_summary"]["summary_cycle_max"], 0)
+        self.assertEqual(len(parsed["detailed_cycles"]), 1)
+        self.assertEqual(parsed["detailed_cycles"][0]["detailed_cycle"], 1)
+        self.assertEqual(parsed["detailed_cycles"][0]["summary_ncyc"], 0)
+
+        final_cyc = parsed["reconciled_final_cycle"]
+        self.assertEqual(final_cyc["summary_ncyc"], 0)
+        self.assertEqual(final_cyc["detailed_cycle"], 1)
+        self.assertEqual(final_cyc["resolution_table_cycle"], 1)
+        self.assertEqual(final_cyc["r_work_token"], "0.1912")
+        self.assertEqual(final_cyc["r_free_token"], "0.2315")
+        self.assertEqual(final_cyc["nr_used_sum"], 6532)
+        self.assertEqual(final_cyc["nr_free_sum"], 344)
+        self.assertEqual(final_cyc["combined_reported_count"], 6876)
+
+    def test_unweighted_r_above_one_accepted_and_precision_preserved(self) -> None:
+        log_text = (
+            SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace("0.1912", "1.12500")
+            .replace("0.2315", "1.34000")
+            .replace("0.18  0.21", "1.080 1.210")
+            .replace("0.22  0.25", "1.420 1.550")
+            .replace("0.20  0.23", "1.150 1.280")
+            .replace("0.24  0.27", "1.260 1.390")
+        )
+        parsed = parse_refmac_log(log_text)
+        final_cyc = parsed["reconciled_final_cycle"]
+        self.assertEqual(final_cyc["r_work_token"], "1.12500")
+        self.assertEqual(final_cyc["r_free_token"], "1.34000")
+        self.assertAlmostEqual(final_cyc["r_work"], 1.125)
+        self.assertAlmostEqual(final_cyc["r_free"], 1.34)
+        self.assertEqual(final_cyc["r_work_decimal_places"], 5)
+        self.assertEqual(final_cyc["r_free_decimal_places"], 5)
+        self.assertEqual(
+            parsed["final_resolution_bins"]["rf_used_printed_decimal_places"], 3
+        )
+        self.assertEqual(
+            parsed["final_resolution_bins"]["rf_free_printed_decimal_places"], 3
+        )
+
+    def test_competing_unrelated_tables_ignored_for_rfactor_extraction(self) -> None:
+        competing_before = """\
+$TABLE: Cycle   99. Anisotropic scaling v resln :
+$GRAPHS:Cycle   99. Scale v. resln :N:1,2,3:
+$$
+M(4SSQ/LL) Scale_k   B_aniso $$
+$$
+ 0.050      0.9999    0.8888
+ 0.150      0.7777    0.6666
+$$
+"""
+        competing_after = """\
+$TABLE: Restraint geometry stats vs cycle :
+$GRAPHS:Restraint rms vs cycle :N:1,2,3:
+$$
+Ncyc rmsBOND rmsANGL $$
+$$
+   0  0.9111  0.9222
+$$
+"""
+        augmented = (
+            competing_before
+            + SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG
+            + competing_after
+        )
+        parsed = parse_refmac_log(augmented)
+        self.assertEqual(
+            [b["table_kind"] for b in parsed["table_blocks"]],
+            ["other", "resolution_rfactor", "resolution_fom", "cycle_summary", "other"],
+        )
+        self.assertEqual(parsed["reconciled_final_cycle"]["r_work_token"], "0.1912")
+        self.assertEqual(parsed["reconciled_final_cycle"]["r_free_token"], "0.2315")
+        self.assertEqual(parsed["reconciled_final_cycle"]["nr_used_sum"], 6532)
+        self.assertEqual(parsed["reconciled_final_cycle"]["nr_free_sum"], 344)
+
+        # Log with only competing tables and no Rfactor table must fail
+        with self.assertRaisesRegex(ValueError, "Missing required"):
+            parse_refmac_log(competing_before + competing_after)
+
+    def test_standalone_header_end_double_dollar_accepted(self) -> None:
+        log_text = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$\n$$",
+            "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free\n$$\n$$",
+        )
+        parsed = parse_refmac_log(log_text)
+        roles = parsed["final_resolution_bins"]["double_dollar_roles"]
+        self.assertEqual(roles["data_start_line"], roles["header_end_line"] + 1)
+        self.assertEqual(parsed["final_resolution_bins"]["nr_used_sum"], 6532)
+
+    def test_malformed_delimiters_rejected(self) -> None:
+        # 1. Missing data_start $$ (only 3 $$ in resolution table)
+        missing_third_dd = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$\n$$",
+            "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$",
+        )
+        with self.assertRaisesRegex(ValueError, "data_start \\$\\$"):
+            parse_refmac_log(missing_third_dd)
+
+        # 2. Missing closing data_end $$ on cycle_summary table (reaches next $TEXT title with $$)
+        missing_fourth_dd = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "       0   0.1912   0.2315   0.827     12345.     654.3   0.0081  0.410   1.120  0.550   0.045\n $$",
+            "       0   0.1912   0.2315   0.827     12345.     654.3   0.0081  0.410   1.120  0.550   0.045",
+        )
+        with self.assertRaisesRegex(
+            ValueError, r"Multiple \$\$ delimiters on line .* in \$TABLE"
+        ):
+            parse_refmac_log(missing_fourth_dd)
+
+        # 2b. Missing closing data_end $$ before a non-$$ boundary (reaches next $TABLE)
+        missing_resln_fourth_dd = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            " 0.150    3432  98.10    21.5    20.4  0.20  0.23     184    20.8    19.5  0.24  0.27\n$$\n\n $TABLE:",
+            " 0.150    3432  98.10    21.5    20.4  0.20  0.23     184    20.8    19.5  0.24  0.27\n $TABLE:",
+        )
+        with self.assertRaisesRegex(ValueError, r"Unclosed data block in \$TABLE"):
+            parse_refmac_log(missing_resln_fourth_dd)
+
+        # 3. Multiple $$ on a single line inside $TABLE
+        double_dd_line = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "$$\nM(4SSQ/LL)",
+            "$$ $$\nM(4SSQ/LL)",
+        )
+        with self.assertRaisesRegex(ValueError, "Multiple \\$\\$ delimiters"):
+            parse_refmac_log(double_dd_line)
+
+        # 4. Trailing tokens after header_end $$
+        trailing_after_header_dd = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Rf_free   WR_free $$",
+            "Rf_free   WR_free $$ EXTRA_COL",
+        )
+        with self.assertRaisesRegex(ValueError, "Trailing tokens after header_end \\$\\$"):
+            parse_refmac_log(trailing_after_header_dd)
+
+        # 5. Unclosed $TEXT:Result: block
+        unclosed_result = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.rstrip()
+        assert unclosed_result.endswith("$$")
+        unclosed_result = unclosed_result[:-2]
+        with self.assertRaisesRegex(ValueError, "Unclosed \\$TEXT:Result:"):
+            parse_refmac_log(unclosed_result)
+
+        # 6. Stray $$ outside any $TABLE or $TEXT block
+        stray_dd = "$$\n" + SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG
+        with self.assertRaisesRegex(ValueError, "Stray or malformed loggraph delimiter"):
+            parse_refmac_log(stray_dd)
+
+    def test_conflicting_r_pairs_rejected(self) -> None:
+        # 1. Final detailed Overall R factor conflicts with cycle_summary Rfact
+        conflict_detail_work = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "Overall R factor                     =     0.5207",
+            "Overall R factor                     =     0.5208",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-work"):
+            parse_refmac_log(conflict_detail_work)
+
+        # 2. Final detailed Free R factor conflicts with cycle_summary Rfree
+        conflict_detail_free = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "Free R factor                        =     0.5289",
+            "Free R factor                        =     0.5290",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-free"):
+            parse_refmac_log(conflict_detail_free)
+
+        # 3. Final results R factor Final conflicts with cycle_summary
+        conflict_final_res_work = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "           R factor    0.5397   0.5207",
+            "           R factor    0.5397   0.5210",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-work"):
+            parse_refmac_log(conflict_final_res_work)
+
+        # 4. Final results R free Final conflicts with cycle_summary
+        conflict_final_res_free = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "             R free    0.5317   0.5289",
+            "             R free    0.5317   0.5299",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-free"):
+            parse_refmac_log(conflict_final_res_free)
+
+        # 5. Final results Initial R factor conflicts with initial cycle (Ncyc 0)
+        conflict_init_res_work = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "           R factor    0.5397   0.5207",
+            "           R factor    0.5399   0.5207",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-work"):
+            parse_refmac_log(conflict_init_res_work)
+
+        # 6. Intermediate cycle (Ncyc 15) mismatch between detail and summary
+        conflict_mid_cycle = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "      15   0.5302   0.5342",
+            "      15   0.5309   0.5342",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-work"):
+            parse_refmac_log(conflict_mid_cycle)
+
+        # 7. Unequal Decimal value at 5th decimal place (0.19121 vs 0.1912) rejected with no tolerance
+        conflict_fifth_dp = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Overall R factor                     =     0.1912",
+            "Overall R factor                     =     0.19121",
+        )
+        with self.assertRaisesRegex(ValueError, "Conflicting R-work"):
+            parse_refmac_log(conflict_fifth_dp)
+
+        # 8. Missing unweighted Overall R factor (only weighted R present)
+        missing_unweighted = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Overall R factor                     =     0.1912\n",
+            "",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "missing paired unweighted 'Overall R factor' / 'Free R factor'"
+        ):
+            parse_refmac_log(missing_unweighted)
+
+    def test_decimal_r_reconciliation_with_trailing_zeros_preserves_tokens(self) -> None:
+        # Equal Decimal values with different trailing zeros (0.19120 vs 0.1912 vs 0.191200) pass
+        trailing_zero_log = (
+            SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                "Overall R factor                     =     0.1912",
+                "Overall R factor                     =     0.19120",
+            )
+            .replace(
+                "Free R factor                        =     0.2315",
+                "Free R factor                        =     0.231500",
+            )
+            .replace(
+                "           R factor    0.1912   0.1912",
+                "           R factor    0.191200 0.1912000",
+            )
+            .replace(
+                "             R free    0.2315   0.2315",
+                "             R free    0.23150  0.2315",
+            )
+        )
+        parsed = parse_refmac_log(trailing_zero_log)
+        self.assertTrue(
+            parsed["cycle_r_values"]["all_ordered_detail_pairs_equal_summary_values"]
+        )
+        self.assertTrue(
+            parsed["consistency_checks"][
+                "initial_detail_summary_and_final_results_initial_values_equal"
+            ]
+        )
+        self.assertTrue(
+            parsed["consistency_checks"][
+                "final_detail_summary_and_final_results_final_values_equal"
+            ]
+        )
+
+        init_cyc = parsed["reconciled_initial_cycle"]
+        self.assertEqual(init_cyc["detailed_r_work_token"], "0.19120")
+        self.assertEqual(init_cyc["detailed_r_work_decimal_places"], 5)
+        self.assertEqual(init_cyc["summary_r_work_token"], "0.1912")
+        self.assertEqual(init_cyc["summary_r_work_decimal_places"], 4)
+        self.assertEqual(init_cyc["final_results_r_work_token"], "0.191200")
+        self.assertEqual(init_cyc["final_results_r_work_decimal_places"], 6)
+
+        final_cyc = parsed["reconciled_final_cycle"]
+        self.assertEqual(final_cyc["detailed_r_free_token"], "0.231500")
+        self.assertEqual(final_cyc["detailed_r_free_decimal_places"], 6)
+        self.assertEqual(final_cyc["summary_r_free_token"], "0.2315")
+        self.assertEqual(final_cyc["summary_r_free_decimal_places"], 4)
+        self.assertEqual(final_cyc["final_results_r_work_token"], "0.1912000")
+        self.assertEqual(final_cyc["final_results_r_work_decimal_places"], 7)
+
+    def test_truncated_rows_and_malformed_headers_rejected(self) -> None:
+        # 1. Truncated row in resolution R-factor table (11 tokens instead of 12)
+        truncated_bin_row = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            " 0.150    3432  98.10    21.5    20.4  0.20  0.23     184    20.8    19.5  0.24  0.27",
+            " 0.150    3432  98.10    21.5    20.4  0.20  0.23     184    20.8    19.5  0.24",
+        )
+        with self.assertRaisesRegex(ValueError, "Row width mismatch"):
+            parse_refmac_log(truncated_bin_row)
+
+        # 2. Truncated row in cycle-summary table (10 tokens instead of 11)
+        truncated_summary_row = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "       0   0.1912   0.2315   0.827     12345.     654.3   0.0081  0.410   1.120  0.550   0.045",
+            "       0   0.1912   0.2315   0.827     12345.     654.3   0.0081  0.410   1.120  0.550",
+        )
+        with self.assertRaisesRegex(ValueError, "Row width mismatch"):
+            parse_refmac_log(truncated_summary_row)
+
+        # 3. Truncated row in Final results block (missing Final value for known label 'R free')
+        truncated_final_results = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "             R free    0.2315   0.2315",
+            "             R free    0.2315",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Expected exactly 2 values \(Initial, Final\) for 'R free' in Final results block",
+        ):
+            parse_refmac_log(truncated_final_results)
+
+        # 4. Swapped/corrupted header in resolution R-factor table
+        corrupted_header = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Rf_used WR_used",
+            "WR_used Rf_used",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Unexpected resolution R-factor table header"
+        ):
+            parse_refmac_log(corrupted_header)
+
+    def test_final_results_known_and_other_label_cardinality_and_duplicates(self) -> None:
+        # Extra value on known 'R factor' row rejected
+        extra_rfactor_val = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "           R factor    0.1912   0.1912",
+            "           R factor    0.1912   0.1912   0.1912",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Expected exactly 2 values \(Initial, Final\) for 'R factor' in Final results block",
+        ):
+            parse_refmac_log(extra_rfactor_val)
+
+        # Extra value on known 'R free' row rejected
+        extra_rfree_val = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "             R free    0.2315   0.2315",
+            "             R free    0.2315   0.2315   0.2500",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Expected exactly 2 values \(Initial, Final\) for 'R free' in Final results block",
+        ):
+            parse_refmac_log(extra_rfree_val)
+
+        # Duplicate 'R factor' row rejected
+        dup_rfactor = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "           R factor    0.1912   0.1912",
+            "           R factor    0.1912   0.1912\n           R factor    0.1912   0.1912",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Duplicate 'R factor' row in Final results block"
+        ):
+            parse_refmac_log(dup_rfactor)
+
+        # Duplicate 'R free' row rejected
+        dup_rfree = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "             R free    0.2315   0.2315",
+            "             R free    0.2315   0.2315\n             R free    0.2315   0.2315",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Duplicate 'R free' row in Final results block"
+        ):
+            parse_refmac_log(dup_rfree)
+
+        # Legitimate other two-value label in Final results accepted
+        other_label_ok = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "     Rms ChirVolume    0.0450   0.0450",
+            "     Rms ChirVolume    0.0450   0.0450\n      Rms Planarity    0.0120   0.0080",
+        )
+        parsed_other = parse_refmac_log(other_label_ok)
+        self.assertEqual(parsed_other["final_results_block"]["rows"][-1]["label"], "Rms Planarity")
+
+        # Extra value on other label rejected
+        other_label_extra = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "     Rms ChirVolume    0.0450   0.0450",
+            "     Rms ChirVolume    0.0450   0.0450\n      Rms Planarity    0.0120   0.0080   0.0040",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Expected exactly 2 values \(Initial, Final\) for 'Rms Planarity' in Final results block",
+        ):
+            parse_refmac_log(other_label_extra)
+
+    def test_nan_infinity_negative_and_malformed_r_values_rejected_while_large_finite_accepted(
+        self,
+    ) -> None:
+        # Large finite ordinary and weighted R values (such as 99.99 or 100.0000) are accepted
+        large_finite_log = (
+            SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace("0.1912", "100.0000")
+            .replace("0.2315", "99.9900")
+            .replace("0.18  0.21", "99.99 100.00")
+            .replace("0.22  0.25", "100.00 120.50")
+            .replace("0.2210", "100.0000")
+            .replace("0.2610", "105.5000")
+        )
+        parsed_large = parse_refmac_log(large_finite_log)
+        self.assertEqual(
+            parsed_large["reconciled_final_cycle"]["r_work_token"], "100.0000"
+        )
+        self.assertEqual(
+            parsed_large["reconciled_final_cycle"]["r_free_token"], "99.9900"
+        )
+        self.assertAlmostEqual(
+            parsed_large["reconciled_final_cycle"]["r_work"], 100.0
+        )
+
+        # Non-finite, negative, overflow ('*'), and malformed R tokens are rejected
+        for bad_token in (
+            "NaN",
+            "nan",
+            "Inf",
+            "-inf",
+            "****",
+            "-0.1912",
+            "N/A",
+            ".1912",
+            "0.",
+        ):
+            with self.subTest(bad_bin_r=bad_token):
+                bad_bin_log = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "0.20  0.23",
+                    f"{bad_token}  0.23",
+                )
+                with self.assertRaisesRegex(ValueError, "R token"):
+                    parse_refmac_log(bad_bin_log)
+
+            with self.subTest(bad_detail_r=bad_token):
+                bad_detail_log = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "Overall R factor                     =     0.1912",
+                    f"Overall R factor                     =     {bad_token}",
+                )
+                with self.assertRaisesRegex(ValueError, "R token"):
+                    parse_refmac_log(bad_detail_log)
+
+            with self.subTest(bad_summary_r=bad_token):
+                bad_summary_log = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "       0   0.1912   0.2315",
+                    f"       0   {bad_token}   0.2315",
+                )
+                with self.assertRaisesRegex(ValueError, "R token"):
+                    parse_refmac_log(bad_summary_log)
+
+    def test_intermediate_resolution_rfactor_table_validated_and_retained(self) -> None:
+        intermediate_table = (
+            "$TABLE: Cycle    2. Rfactor analysis, F distribution v resln  :\n"
+            "$GRAPHS:Cycle    2.  v. resln :N:1,6,7,11,12:\n"
+            "$$\n"
+            "M(4SSQ/LL) NR_used %_obs M(Fo_used) M(Fc_used) Rf_used WR_used\n"
+            "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$\n"
+            "$$\n"
+            " 0.032   11000  99.36    39.1    31.2  0.53  0.76     600    24.6    36.2  0.75  1.45\n"
+            " 0.093   11467 100.00    35.1    25.7  0.52  0.60     613    32.9    26.5  0.52  0.58\n"
+            "$$\n"
+            "Number of used reflections           =      22467\n"
+        )
+        anchor = (
+            "Overall R factor                     =     0.5380\n"
+            "Free R factor                        =     0.5313\n"
+        )
+        with_intermediate = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            anchor, intermediate_table + anchor, 1
+        )
+        parsed = parse_refmac_log(with_intermediate)
+        tables = parsed["resolution_bin_values"]["tables"]
+        self.assertEqual(len(tables), 3)
+        self.assertEqual(
+            [t["phase"] for t in tables], ["initial", "intermediate", "final"]
+        )
+        self.assertEqual([t["detailed_cycle"] for t in tables], [1, 2, 31])
+        self.assertEqual(tables[1]["summary_ncyc"], 1)
+        self.assertEqual(tables[1]["nr_used_sum"], 22467)
+        self.assertEqual(tables[1]["nr_free_sum"], 1213)
+        self.assertEqual(parsed["initial_resolution_bins"]["detailed_cycle"], 1)
+        self.assertEqual(parsed["final_resolution_bins"]["detailed_cycle"], 31)
+
+        # 1. NaN R token inside intermediate resolution table rejected
+        bad_intermediate_r = with_intermediate.replace(
+            "0.53  0.76     600", "NaN   0.76     600", 1
+        )
+        with self.assertRaisesRegex(ValueError, "R token"):
+            parse_refmac_log(bad_intermediate_r)
+
+        # 2. Negative and fractional count inside intermediate resolution table rejected
+        neg_intermediate_count = with_intermediate.replace(
+            " 0.032   11000", " 0.032  -11000", 1
+        )
+        with self.assertRaisesRegex(ValueError, "NR_used"):
+            parse_refmac_log(neg_intermediate_count)
+
+        frac_intermediate_count = with_intermediate.replace(
+            " 0.032   11000", " 0.032 11000.5", 1
+        )
+        with self.assertRaisesRegex(ValueError, "NR_used"):
+            parse_refmac_log(frac_intermediate_count)
+
+        # 3. Same-cycle detailed Number of used reflections mismatch against intermediate table sum rejected
+        mismatch_intermediate_used = with_intermediate.replace(
+            "Number of used reflections           =      22467\nOverall R factor                     =     0.5380",
+            "Number of used reflections           =      22468\nOverall R factor                     =     0.5380",
+            1,
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            "Detailed Cycle 2 Number of used reflections .* != Cycle 2 resolution table NR_used sum",
+        ):
+            parse_refmac_log(mismatch_intermediate_used)
+
+    def test_cgmat_cycle_markers_validated_and_retained(self) -> None:
+        # Optional absence in HISTORICAL_AMPLE_EXCERPT_LOG
+        parsed_no_markers = parse_refmac_log(HISTORICAL_AMPLE_EXCERPT_LOG)
+        self.assertEqual(parsed_no_markers["cgmat_cycle_markers"], [])
+        self.assertIsNone(parsed_no_markers["detailed_cycles"][0]["cgmat_cycle_number"])
+
+        # Native pattern: CGMAT cycle number 1..30 before detailed cycles 1..30, no marker 31 before detailed 31
+        lines = HISTORICAL_AMPLE_EXCERPT_LOG.splitlines()
+        out_lines: list[str] = []
+        overall_r_seen = 0
+        for line in lines:
+            if line.startswith("Resolution limits") and overall_r_seen == 0:
+                out_lines.append("     CGMAT cycle number =      1")
+            elif line.startswith("Overall R factor"):
+                overall_r_seen += 1
+                if 2 <= overall_r_seen <= 30:
+                    out_lines.append(
+                        f"     CGMAT cycle number =     {overall_r_seen:2d}"
+                    )
+            out_lines.append(line)
+        log_with_30_markers = "\n".join(out_lines) + "\n"
+        parsed_markers = parse_refmac_log(log_with_30_markers)
+        self.assertEqual(len(parsed_markers["cgmat_cycle_markers"]), 30)
+        self.assertEqual(
+            [m["cycle_number"] for m in parsed_markers["cgmat_cycle_markers"]],
+            list(range(1, 31)),
+        )
+        self.assertEqual(
+            [m["detailed_cycle"] for m in parsed_markers["cgmat_cycle_markers"]],
+            list(range(1, 31)),
+        )
+        self.assertEqual(parsed_markers["detailed_cycles"][0]["cgmat_cycle_number"], 1)
+        self.assertEqual(
+            parsed_markers["detailed_cycles"][29]["cgmat_cycle_number"], 30
+        )
+        self.assertIsNone(parsed_markers["detailed_cycles"][30]["cgmat_cycle_number"])
+
+        # Duplicate CGMAT marker rejected
+        dup_marker_log = log_with_30_markers.replace(
+            "     CGMAT cycle number =      2",
+            "     CGMAT cycle number =      1",
+            1,
+        )
+        with self.assertRaisesRegex(ValueError, "CGMAT cycle number"):
+            parse_refmac_log(dup_marker_log)
+
+        # Out-of-order / misassociated CGMAT marker rejected
+        misassociated_marker = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Resolution limits",
+            "     CGMAT cycle number =      2\nResolution limits",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "CGMAT cycle number 2 .* does not match associated detailed Cycle 1"
+        ):
+            parse_refmac_log(misassociated_marker)
+
+        # Non-positive / non-finite CGMAT marker rejected
+        for bad_cgmat in ("0", "-1", "1.5", "NaN", "1 2"):
+            with self.subTest(bad_cgmat=bad_cgmat):
+                bad_cgmat_log = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "Resolution limits",
+                    f"     CGMAT cycle number =      {bad_cgmat}\nResolution limits",
+                )
+                with self.assertRaisesRegex(ValueError, "CGMAT cycle number"):
+                    parse_refmac_log(bad_cgmat_log)
+
+    def test_optional_correlation_dpi_and_ml_su_fields_validated(self) -> None:
+        parsed = parse_refmac_log(HISTORICAL_AMPLE_EXCERPT_LOG)
+        init_detail = parsed["detailed_cycles"][0]
+        final_detail = parsed["detailed_cycles"][-1]
+
+        # Absent on Cycle 1 in excerpt
+        self.assertIsNone(init_detail["overall_correlation_coefficient"])
+        self.assertIsNone(init_detail["free_correlation_coefficient"])
+        self.assertIsNone(init_detail["cruickshanks_dpi"])
+        self.assertIsNone(init_detail["dpi_free_r"])
+        self.assertIsNone(init_detail["ml_su_positional"])
+        self.assertIsNone(init_detail["ml_su_thermal"])
+
+        # Present on Cycle 31 with tokens, values, and source line numbers
+        self.assertEqual(
+            final_detail["overall_correlation_coefficient_token"], "0.6076"
+        )
+        self.assertAlmostEqual(
+            final_detail["overall_correlation_coefficient"], 0.6076
+        )
+        self.assertIsNotNone(final_detail["overall_correlation_coefficient_line"])
+        self.assertEqual(final_detail["free_correlation_coefficient_token"], "0.5684")
+        self.assertEqual(final_detail["cruickshanks_dpi_token"], "0.0241")
+        self.assertEqual(final_detail["dpi_free_r_token"], "0.0244")
+        self.assertEqual(final_detail["ml_su_positional_token"], "0.0589")
+        self.assertEqual(final_detail["ml_su_thermal_token"], "1.0064")
+
+        # Non-finite or wrong-cardinality optional metric values rejected
+        for label, orig_val in (
+            ("Overall correlation coefficient", "0.6076"),
+            ("Free correlation coefficient", "0.5684"),
+            ("Cruickshanks DPI for coordinate error", "0.0241"),
+            ("DPI based on free R factor", "0.0244"),
+            ("ML based su of positional parameters", "0.0589"),
+            ("ML based su of thermal parameters", "1.0064"),
+        ):
+            for bad_rhs in ("NaN", "Inf", "****", f"{orig_val} {orig_val}", ""):
+                with self.subTest(label=label, bad_rhs=bad_rhs):
+                    mutated = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+                        orig_val, bad_rhs, 1
+                    )
+                    with self.assertRaises(ValueError):
+                        parse_refmac_log(mutated)
+
+    def test_fom_resolution_tables_cycle_order_and_row_count_identity(self) -> None:
+        # FOM population may differ from R-table NR_used / NR_free as long as NREFall == NREFa + NREFc
+        different_fom_pop = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "  0.0500  3000   0.850   100   0.780  3100   0.848  0.910",
+            "  0.0500   400   0.850    50   0.780   450   0.848  0.910",
+        )
+        parsed = parse_refmac_log(different_fom_pop)
+        self.assertEqual(parsed["reconciled_final_cycle"]["nr_used_sum"], 6532)
+
+        # Within-row NREFall != NREFa + NREFc rejected
+        bad_fom_sum = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "  0.0500  3000   0.850   100   0.780  3100   0.848  0.910",
+            "  0.0500  3000   0.850   100   0.780  3101   0.848  0.910",
+        )
+        with self.assertRaisesRegex(
+            ValueError, r"FOM table row NREFall \(3101\) != NREFa \(3000\) \+ NREFc \(100\)"
+        ):
+            parse_refmac_log(bad_fom_sum)
+
+        # Duplicate / non-increasing FOM cycle number rejected
+        dup_fom_cycle = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            " $TABLE: Cycle   31. Fom(-acentric, centric, overall v resln:\n"
+            " $GRAPHS:Cycle   31. M(Fom) v. resln :N:1,3,5,7,8:",
+            " $TABLE: Cycle    1. Fom(-acentric, centric, overall v resln:\n"
+            " $GRAPHS:Cycle    1. M(Fom) v. resln :N:1,3,5,7,8:",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Non-increasing or duplicate resolution FOM table Cycle 1"
+        ):
+            parse_refmac_log(dup_fom_cycle)
+
+        # Misaligned FOM table cycle number rejected
+        misaligned_fom_cycle = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            " $TABLE: Cycle   31. Fom(-acentric, centric, overall v resln:\n"
+            " $GRAPHS:Cycle   31. M(Fom) v. resln :N:1,3,5,7,8:",
+            " $TABLE: Cycle   30. Fom(-acentric, centric, overall v resln:\n"
+            " $GRAPHS:Cycle   30. M(Fom) v. resln :N:1,3,5,7,8:",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Resolution FOM table Cycle 30 .* is misaligned"
+        ):
+            parse_refmac_log(misaligned_fom_cycle)
+
+    def test_percentage_tokens_domain_and_precision(self) -> None:
+        # 0 and 100 boundaries accepted; 0.0000 free percentage with positive NR_free accepted
+        boundary_pct_log = (
+            SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                "Percentage observed                  =    98.7500",
+                "Percentage observed                  =   100.0000",
+            )
+            .replace(
+                "Percentage of free reflections       =     5.0029",
+                "Percentage of free reflections       =     0.0000",
+            )
+            .replace("99.50", "0.00")
+            .replace("98.10", "100.00")
+        )
+        parsed = parse_refmac_log(boundary_pct_log)
+        detail = parsed["detailed_cycles"][0]
+        self.assertEqual(detail["percentage_observed_token"], "100.0000")
+        self.assertAlmostEqual(detail["percentage_observed"], 100.0)
+        self.assertEqual(detail["percentage_observed_decimal_places"], 4)
+        self.assertEqual(detail["percentage_free_token"], "0.0000")
+        self.assertAlmostEqual(detail["percentage_free"], 0.0)
+        self.assertEqual(detail["percentage_free_decimal_places"], 4)
+        self.assertGreater(parsed["final_resolution_bins"]["nr_free_sum"], 0)
+
+        # Scientific-notation percentages compute effective decimal places after exponent
+        exp_pct_log = (
+            SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                "Percentage observed                  =    98.7500",
+                "Percentage observed                  =   8.58624e1",
+            )
+            .replace(
+                "Percentage of free reflections       =     5.0029",
+                "Percentage of free reflections       =   5.12250e0",
+            )
+            .replace("99.50", "1e2", 1)
+        )
+        parsed_exp = parse_refmac_log(exp_pct_log)
+        exp_detail = parsed_exp["detailed_cycles"][0]
+        self.assertEqual(exp_detail["percentage_observed_token"], "8.58624e1")
+        self.assertAlmostEqual(exp_detail["percentage_observed"], 85.8624)
+        self.assertEqual(exp_detail["percentage_observed_decimal_places"], 4)
+        self.assertEqual(exp_detail["percentage_free_token"], "5.12250e0")
+        self.assertAlmostEqual(exp_detail["percentage_free"], 5.1225)
+        self.assertEqual(exp_detail["percentage_free_decimal_places"], 5)
+        self.assertEqual(
+            parsed_exp["final_resolution_bins"]["bins"][0]["pct_obs_token"], "1e2"
+        )
+        self.assertAlmostEqual(
+            parsed_exp["final_resolution_bins"]["bins"][0]["pct_obs"], 100.0
+        )
+        self.assertEqual(
+            parsed_exp["final_resolution_bins"]["bins"][0]["pct_obs_decimal_places"],
+            0,
+        )
+
+        # Out-of-domain (< 0, > 100) and non-finite percentages rejected
+        for bad_pct in ("-0.01", "100.01", "NaN", "Inf", "****"):
+            with self.subTest(bad_pct_obs=bad_pct):
+                bad_obs = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "Percentage observed                  =    98.7500",
+                    f"Percentage observed                  =    {bad_pct}",
+                )
+                with self.assertRaisesRegex(ValueError, "Percentage observed"):
+                    parse_refmac_log(bad_obs)
+
+            with self.subTest(bad_pct_free=bad_pct):
+                bad_free = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "Percentage of free reflections       =     5.0029",
+                    f"Percentage of free reflections       =     {bad_pct}",
+                )
+                with self.assertRaisesRegex(
+                    ValueError, "Percentage of free reflections"
+                ):
+                    parse_refmac_log(bad_free)
+
+            with self.subTest(bad_bin_pct=bad_pct):
+                bad_bin = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+                    "99.50", bad_pct, 1
+                )
+                with self.assertRaisesRegex(ValueError, "%_obs"):
+                    parse_refmac_log(bad_bin)
+
+    def test_underscore_and_non_native_numeric_literals_rejected(self) -> None:
+        # Underscores in percentage, generic float, integer count, and R fields must fail
+        for target, replacement, err_re in (
+            (
+                "Percentage observed                  =    85.8624",
+                "Percentage observed                  =   8_5.8624",
+                "Malformed numeric token '8_5.8624'",
+            ),
+            (
+                " 0.032     293  99.36",
+                " 0.032     293 9_9.36",
+                "Malformed numeric token '9_9.36'",
+            ),
+            (
+                "Overall correlation coefficient      =     0.6076",
+                "Overall correlation coefficient      =    0_0.6076",
+                "Malformed numeric token '0_0.6076'",
+            ),
+            (
+                "Overall weighted R factor            =     0.5425",
+                "Overall weighted R factor            =    0_0.5425",
+                "Malformed numeric token '0_0.5425'",
+            ),
+            (
+                "      30   0.5207   0.5289   0.341     65565.",
+                "      30   0.5207   0.5289   0.341    65_565.",
+                "Malformed numeric token '65_565.'",
+            ),
+            (
+                "Number of used reflections           =      22467",
+                "Number of used reflections           =     22_467",
+                "Invalid non-negative integer count token '22_467'",
+            ),
+            (
+                "Overall R factor                     =     0.5207",
+                "Overall R factor                     =    0_0.5207",
+                "Invalid or nonfinite/sentinel R token '0_0.5207'",
+            ),
+        ):
+            with self.subTest(replacement=replacement):
+                mutated = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+                    target, replacement, 1
+                )
+                with self.assertRaisesRegex(ValueError, err_re):
+                    parse_refmac_log(mutated)
+
+    def test_graph_declaration_cycle_labels_validated_against_table_title(self) -> None:
+        # 1. Final resolution R-factor $GRAPHS Cycle 31 -> Cycle 30 contradicts table title Cycle 31
+        contradict_r_graph = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "$GRAPHS:Cycle   31.  v. resln :N:1,6,7,11,12:",
+            "$GRAPHS:Cycle   30.  v. resln :N:1,6,7,11,12:",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Conflicting \$GRAPHS Cycle 30 .* vs resolution R-factor table title Cycle 31",
+        ):
+            parse_refmac_log(contradict_r_graph)
+
+        # 2. Final resolution FOM $GRAPHS Cycle 31 -> Cycle 30 contradicts table title Cycle 31
+        contradict_fom_graph = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            " $GRAPHS:Cycle   31. M(Fom) v. resln :N:1,3,5,7,8:",
+            " $GRAPHS:Cycle   30. M(Fom) v. resln :N:1,3,5,7,8:",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Conflicting \$GRAPHS Cycle 30 .* vs resolution FOM table title Cycle 31",
+        ):
+            parse_refmac_log(contradict_fom_graph)
+
+        # 3. Continuation line :Cycle 31. -> :Cycle 30. contradicts table title Cycle 31
+        contradict_cont_graph = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            ":Cycle   31.  and  v. resln :N:1,4,5,9,10:",
+            ":Cycle   30.  and  v. resln :N:1,4,5,9,10:",
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            r"Conflicting \$GRAPHS Cycle 30 .* vs resolution R-factor table title Cycle 31",
+        ):
+            parse_refmac_log(contradict_cont_graph)
+
+        # 4. Malformed explicit Cycle labels in $GRAPHS or continuation lines rejected
+        for bad_graph_line in (
+            ":Cycle   31  and  v. resln :N:1,4,5,9,10:",
+            ":cycle   31.  and  v. resln :N:1,4,5,9,10:",
+            ":Cycle  NaN.  and  v. resln :N:1,4,5,9,10:",
+            ":Cycle    0.  and  v. resln :N:1,4,5,9,10:",
+        ):
+            with self.subTest(bad_graph_line=bad_graph_line):
+                mutated = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+                    ":Cycle   31.  and  v. resln :N:1,4,5,9,10:",
+                    bad_graph_line,
+                )
+                with self.assertRaisesRegex(ValueError, r"\$GRAPHS"):
+                    parse_refmac_log(mutated)
+
+        # 5. Valid graph declaration without an explicit Cycle label is accepted
+        no_cycle_in_graph = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "$GRAPHS:Cycle    1.  v. resln :N:1,6,7,11,12:",
+            "$GRAPHS: v. resln :N:1,6,7,11,12:",
+        )
+        parsed_no_graph_cycle = parse_refmac_log(no_cycle_in_graph)
+        self.assertEqual(
+            parsed_no_graph_cycle["reconciled_final_cycle"]["resolution_table_cycle"],
+            1,
+        )
+
+    def test_fractional_negative_and_contradictory_counts_rejected(self) -> None:
+        # 1. Fractional NR_used
+        frac_nr_used = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            " 0.050    3100",
+            " 0.050  3100.5",
+        )
+        with self.assertRaisesRegex(ValueError, "NR_used"):
+            parse_refmac_log(frac_nr_used)
+
+        # 2. Fractional NR_free
+        frac_nr_free = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "     160    41.2",
+            "   160.0    41.2",
+        )
+        with self.assertRaisesRegex(ValueError, "NR_free"):
+            parse_refmac_log(frac_nr_free)
+
+        # 3. Negative NR_used
+        neg_nr_used = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            " 0.050    3100",
+            " 0.050   -3100",
+        )
+        with self.assertRaisesRegex(ValueError, "NR_used"):
+            parse_refmac_log(neg_nr_used)
+
+        # 4. Negative NR_free
+        neg_nr_free = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "     160    41.2",
+            "      -1    41.2",
+        )
+        with self.assertRaisesRegex(ValueError, "NR_free"):
+            parse_refmac_log(neg_nr_free)
+
+        # 5. Fractional summary Ncyc
+        frac_ncyc = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "       0   0.1912   0.2315",
+            "     0.0   0.1912   0.2315",
+        )
+        with self.assertRaisesRegex(ValueError, "summary Ncyc"):
+            parse_refmac_log(frac_ncyc)
+
+        # 6. Contradictory Number of used reflections vs sum(NR_used)
+        contradictory_used = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "Number of used reflections           =       6532",
+            "Number of used reflections           =       6533",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Number of used reflections .* != .* NR_used sum"
+        ):
+            parse_refmac_log(contradictory_used)
+
+    def test_ambiguous_final_cycle_rejected(self) -> None:
+        # 1. Wrong final resolution table cycle label (Cycle 30 instead of Cycle 31)
+        wrong_final_table_cycle = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "$TABLE: Cycle   31. Rfactor analysis, F distribution v resln  :\n"
+            "$GRAPHS:Cycle   31.  v. resln :N:1,6,7,11,12:\n"
+            ":Cycle   31.  and  v. resln :N:1,4,5,9,10:\n"
+            ":Cycle   31. % observed v. resln :N:1,3:",
+            "$TABLE: Cycle   30. Rfactor analysis, F distribution v resln  :\n"
+            "$GRAPHS:Cycle   30.  v. resln :N:1,6,7,11,12:\n"
+            ":Cycle   30.  and  v. resln :N:1,4,5,9,10:\n"
+            ":Cycle   30. % observed v. resln :N:1,3:",
+        )
+        with self.assertRaisesRegex(ValueError, "Resolution R-factor table Cycle 30"):
+            parse_refmac_log(wrong_final_table_cycle)
+
+        # 2. Missing final resolution R-factor table in multi-cycle log
+        parts = HISTORICAL_AMPLE_EXCERPT_LOG.split(
+            "$TABLE: Cycle   31. Rfactor analysis, F distribution v resln  :"
+        )
+        self.assertEqual(len(parts), 2)
+        only_initial_resln_table = (
+            parts[0]
+            + "$TABLE: Cycle   31. Unrelated stats v resln :"
+            + parts[1].replace(
+                "M(4SSQ/LL) NR_used %_obs M(Fo_used) M(Fc_used) Rf_used WR_used\n"
+                "NR_free M(Fo_free) M(Fc_free) Rf_free   WR_free $$",
+                "C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 C11 C12 $$",
+                1,
+            )
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Ambiguous or missing final resolution R-factor table"
+        ):
+            parse_refmac_log(only_initial_resln_table)
+
+        # 3. Extra detailed Overall/Free R pair not in cycle_summary
+        extra_detail_pair = HISTORICAL_AMPLE_EXCERPT_LOG.replace(
+            "$TABLE: Rfactor analysis, stats vs cycle  :",
+            "Overall R factor = 0.5207\nFree R factor = 0.5289\n"
+            "-----------------------------------------------------------------------------\n"
+            "$TABLE: Rfactor analysis, stats vs cycle  :",
+        )
+        with self.assertRaisesRegex(ValueError, "Cycle count mismatch"):
+            parse_refmac_log(extra_detail_pair)
+
+        # 4. Contradictory declared NCYC vs summary max Ncyc
+        contradictory_ncyc = SYNTHETIC_SINGLE_CYCLE_NCYC0_LOG.replace(
+            "  Data line--- NCYC 0\n  Refinement cycles       :     0",
+            "  Data line--- NCYC 5\n  Refinement cycles       :     5",
+        )
+        with self.assertRaisesRegex(
+            ValueError, "Cycle summary max Ncyc .* contradicts declared NCYC"
+        ):
+            parse_refmac_log(contradictory_ncyc)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/scripts/test_onesar_rerun.py b/scripts/test_onesar_rerun.py
new file mode 100644
index 0000000..7218692
--- /dev/null
+++ b/scripts/test_onesar_rerun.py
@@ -0,0 +1,5806 @@
+#!/usr/bin/env python3
+"""Hermetic unit tests for 1SAR input admission, MTZ projection, and PHENIX R-factor reconciliation.
+
+Runs under the repository Python environment with zero PHENIX/CCP4/Gemmi
+execution, zero vendor library imports, and zero network access.
+"""
+
+from __future__ import annotations
+
+import ast
+import copy
+import hashlib
+import io
+import json
+import math
+import os
+import struct
+import subprocess
+import sys
+import tempfile
+import time
+import types
+import unittest
+import warnings
+import zipfile
+from pathlib import Path
+from typing import Any
+from unittest import mock
+
+_SCRIPTS_DIR = Path(__file__).resolve().parent
+if str(_SCRIPTS_DIR) not in sys.path:
+    sys.path.insert(0, str(_SCRIPTS_DIR))
+
+import onesar_phenix_measure  # noqa: E402
+import onesar_rerun  # noqa: E402
+import refinement_cache  # noqa: E402
+import toolchain  # noqa: E402
+
+
+REPO_ROOT = _SCRIPTS_DIR.parent
+COMMITTED_ARCHIVE = REPO_ROOT / onesar_rerun.ARCHIVE_REL_PATH
+
+
+def _card(text: str) -> bytes:
+    return text.ljust(80).encode("ascii")
+
+
+def _build_synthetic_mtz(
+    rows: list[tuple[float, ...]],
+    *,
+    columns: list[tuple[str, str, str]] | None = None,
+    datasets: list[tuple[int, str]] | None = None,
+    cell: tuple[float, float, float, float, float, float] = (
+        64.897,
+        78.323,
+        38.792,
+        90.0,
+        90.0,
+        90.0,
+    ),
+    syminf: str = "SYMINF   4 4 P 19 'P 21 21 21' PG222",
+    symm_cards: tuple[str, ...] = (
+        "SYMM X,Y,Z",
+        "SYMM -X+1/2,-Y,Z+1/2",
+        "SYMM -X,Y+1/2,-Z+1/2",
+        "SYMM X+1/2,-Y+1/2,-Z",
+    ),
+    valm: str = "VALM NAN",
+) -> bytes:
+    """Construct a synthetic little-endian MTZ byte stream for pure unit testing."""
+    if columns is None:
+        columns = [
+            ("H", "H", "1"),
+            ("K", "H", "1"),
+            ("L", "H", "1"),
+            ("F-obs", "F", "1"),
+            ("SIGF-obs", "Q", "1"),
+            ("R-free-flags", "I", "1"),
+        ]
+    if datasets is None:
+        datasets = [(1, "Original-experimental-data-mapped-to-asu")]
+    ncols = len(columns)
+    nrows = len(rows)
+    flat_values: list[float] = []
+    for row in rows:
+        if len(row) != ncols:
+            raise ValueError(f"Row width {len(row)} != ncols {ncols}")
+        flat_values.extend(float(v) for v in row)
+    data_bytes = struct.pack(f"<{len(flat_values)}f", *flat_values)
+    header_offset = (80 + len(data_bytes)) // 4 + 1
+
+    cards: list[bytes] = [
+        _card("VERS MTZ:V1.1"),
+        _card("TITLE synthetic test mtz"),
+        _card(f"NCOL {ncols:8d} {nrows:12d} {0:8d}"),
+        _card(
+            f"CELL {cell[0]:9.4f} {cell[1]:9.4f} {cell[2]:9.4f} "
+            f"{cell[3]:8.4f} {cell[4]:8.4f} {cell[5]:8.4f}"
+        ),
+        _card("SORT    1   2   3   0   0"),
+        _card(syminf),
+    ]
+    for symm in symm_cards:
+        cards.append(_card(symm))
+    cards.append(_card("RESO 0.00200000 0.25000000"))
+    cards.append(_card(valm))
+    cards.append(_card(f"NDIF {len(datasets):8d}"))
+    for d_id, d_name in datasets:
+        cards.append(_card(f"PROJECT {d_id:7d} project"))
+        cards.append(_card(f"CRYSTAL {d_id:7d} crystal"))
+        cards.append(_card(f"DATASET {d_id:7d} {d_name}"))
+        cards.append(
+            _card(
+                f"DCELL {d_id:9d} {cell[0]:9.4f} {cell[1]:9.4f} {cell[2]:9.4f} "
+                f"{cell[3]:8.4f} {cell[4]:8.4f} {cell[5]:8.4f}"
+            )
+        )
+        cards.append(_card(f"DWAVEL {d_id:8d}    1.00000"))
+    for name, ctype, d_id in columns:
+        cards.append(_card(f"COLUMN {name:<30s} {ctype} 0.0 100.0 {d_id}"))
+    cards.append(_card("END"))
+
+    preamble = (
+        b"MTZ "
+        + struct.pack(" tuple[bytes, bytes]:
+    """Build synthetic phx_maps.mtz and PHX_OBS_PAIR_ONLY.mtz byte streams."""
+    fofc_by_hkl = {
+        tuple(rec["hkl"]): rec for rec in records_fofc
+    }
+    maps_rows: list[tuple[float, ...]] = []
+    pair_rows: list[tuple[float, ...]] = []
+    for rec2 in records_2fofc:
+        h, k, l = rec2["hkl"]
+        rec1 = fofc_by_hkl[(h, k, l)]
+        # Emulate iotbx.mtz phase storage in [0, 360) degrees float32
+        ph2 = float(rec2["phase_deg"]) % 360.0
+        ph1 = float(rec1["phase_deg"]) % 360.0
+        maps_rows.append(
+            (
+                float(h),
+                float(k),
+                float(l),
+                float(rec2["amplitude"]),
+                ph2,
+                float(rec1["amplitude"]),
+                ph1,
+            )
+        )
+        pair_rows.append(
+            (
+                float(h),
+                float(k),
+                float(l),
+                float(rec2["amplitude"]),
+                ph2,
+            )
+        )
+    maps_bytes = _build_synthetic_mtz(
+        maps_rows,
+        columns=[
+            ("H", "H", "1"),
+            ("K", "H", "1"),
+            ("L", "H", "1"),
+            ("2FOFCWT", "F", "1"),
+            ("PH2FOFCWT", "P", "1"),
+            ("FOFCWT", "F", "1"),
+            ("PHFOFCWT", "P", "1"),
+        ],
+        cell=cell,
+    )
+    pair_bytes = _build_synthetic_mtz(
+        pair_rows,
+        columns=[
+            ("H", "H", "1"),
+            ("K", "H", "1"),
+            ("L", "H", "1"),
+            ("2FOFCWT", "F", "1"),
+            ("PH2FOFCWT", "P", "1"),
+        ],
+        cell=cell,
+    )
+    return maps_bytes, pair_bytes
+
+
+def _make_reconciliation_fixture() -> tuple[
+    dict[str, Any], dict[str, Any], dict[str, str], bytes, bytes
+]:
+    """Create a pure-math synthetic input ledger, helper payload, and map MTZs."""
+    nan = float("nan")
+    mtz_bytes = _build_synthetic_mtz(
+        [
+            # Eligible work rows (flag=0), including one zero-F and one negative-F row
+            (1, 0, 0, 100.0, 2.0, 0.0),
+            (0, 1, 0, 200.0, 3.0, 0.0),
+            (0, 0, 1, 0.0, 1.5, 0.0),
+            (1, 1, 0, -5.0, 1.2, 0.0),
+            (1, 1, 1, 150.0, 2.5, 0.0),
+            # Eligible free rows (flag=1)
+            (2, 0, 0, 80.0, 1.8, 1.0),
+            (0, 2, 0, 120.0, 2.2, 1.0),
+            # Ineligible all-NaN padding row (F, SIGF, flag all NaN)
+            (0, 0, 2, nan, nan, nan),
+        ]
+    )
+    input_ledger = onesar_rerun.build_input_hkl_ledger(mtz_bytes)
+
+    # Suppose extract_xtal_data regularizes out the zero-F (0,0,1) and negative-F (1,1,0)
+    # rows, leaving 5 extracted reflections; and f_model outlier removal drops (1,1,1),
+    # leaving 2 work reflections [(1,0,0), (0,1,0)] and 2 free reflections [(2,0,0), (0,2,0)].
+    scale_k1_w = 0.5
+    scale_k1_t = 0.6
+    scale_k1 = 0.54
+
+    # Work: (1,0,0) fo=100, |fm_raw|=180 -> |fm_scaled_w|=90; diff=10
+    #       (0,1,0) fo=200, |fm_raw|=420 -> |fm_scaled_w|=210; diff=10
+    # work_num = 20, work_den = 300, r_work = 20 / 300
+    # Free: (2,0,0) fo=80,  |fm_raw|=100 -> |fm_scaled_t|=60; diff=20
+    #       (0,2,0) fo=120, |fm_raw|=250 -> |fm_scaled_t|=150; diff=30
+    # free_num = 50, free_den = 200, r_free = 50 / 200 = 0.25
+    # All (using scale_k1 = 0.54):
+    #   (1,0,0): |100 - 0.54*180| = |100 - 97.2| = 2.8
+    #   (0,1,0): |200 - 0.54*420| = |200 - 226.8| = 26.8
+    #   (2,0,0): |80  - 0.54*100| = |80  - 54.0| = 26.0
+    #   (0,2,0): |120 - 0.54*250| = |120 - 135.0| = 15.0
+    # all_num = 70.6, all_den = 500.0, r_all = 70.6 / 500.0 = 0.1412
+    r_work = 20.0 / 300.0
+    r_free = 50.0 / 200.0
+    all_num = 70.6
+    all_den = 500.0
+    r_all = all_num / all_den
+
+    vendor_shas = {
+        key: hashlib.sha256(key.encode("utf-8")).hexdigest()
+        for key in onesar_rerun.PHENIX_VENDOR_SOURCE_RELPATHS
+    }
+    vendor_modules = {
+        key: {
+            "module_name": key,
+            "configured_path": f"/opt/phenix/lib/python3.9/site-packages/{rel}",
+            "resolved_path": f"/opt/phenix/lib/python3.9/site-packages/{rel}",
+            "sha256": vendor_shas[key],
+        }
+        for key, rel in onesar_rerun.PHENIX_VENDOR_SOURCE_RELPATHS.items()
+    }
+
+    scatterer_records = [
+        {
+            "label": "PDB=\" N   VAL A   1 \"",
+            "scattering_type": "N",
+            "site_frac": [0.1, 0.2, 0.3],
+            "site_cart": [6.4897, 15.6646, 11.6376],
+            "occupancy": 1.0,
+            "u_iso": 0.25,
+            "u_star": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+            "use_u_iso": True,
+            "use_u_aniso": False,
+        },
+        {
+            "label": "PDB=\" CA  VAL A   1 \"",
+            "scattering_type": "C",
+            "site_frac": [0.12, 0.22, 0.32],
+            "site_cart": [7.78764, 17.23106, 12.41344],
+            "occupancy": 1.0,
+            "u_iso": 0.28,
+            "u_star": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+            "use_u_iso": True,
+            "use_u_aniso": False,
+        },
+    ]
+    snap = onesar_phenix_measure.snapshot_scatterer_records(scatterer_records)
+    xrs_invariance = onesar_phenix_measure.compare_scatterer_snapshots(
+        snap, copy.deepcopy(snap), copy.deepcopy(snap)
+    )
+
+    final_hkls = [
+        ([1, 0, 0], 100.0, 2.0, False),
+        ([0, 1, 0], 200.0, 3.0, False),
+        ([2, 0, 0], 80.0, 1.8, True),
+        ([0, 2, 0], 120.0, 2.2, True),
+    ]
+    map_records_2fofc = [
+        {
+            "hkl": [1, 0, 0],
+            "real": 10.0,
+            "imag": 0.0,
+            "amplitude": 10.0,
+            "phase_deg": 0.0,
+            "is_centric": True,
+        },
+        {
+            "hkl": [0, 1, 0],
+            "real": -12.0,
+            "imag": 0.0,
+            "amplitude": 12.0,
+            "phase_deg": 180.0,
+            "is_centric": True,
+        },
+        {
+            "hkl": [2, 0, 0],
+            "real": 0.0,
+            "imag": 8.0,
+            "amplitude": 8.0,
+            "phase_deg": 90.0,
+            "is_centric": False,
+        },
+        {
+            "hkl": [0, 2, 0],
+            "real": 0.0,
+            "imag": -6.0,
+            "amplitude": 6.0,
+            "phase_deg": -90.0,
+            "is_centric": False,
+        },
+    ]
+    map_records_fofc = [
+        {
+            "hkl": [1, 0, 0],
+            "real": 2.0,
+            "imag": 0.0,
+            "amplitude": 2.0,
+            "phase_deg": 0.0,
+            "is_centric": True,
+        },
+        {
+            "hkl": [0, 1, 0],
+            "real": -1.5,
+            "imag": 0.0,
+            "amplitude": 1.5,
+            "phase_deg": 180.0,
+            "is_centric": True,
+        },
+        {
+            "hkl": [2, 0, 0],
+            "real": 0.0,
+            "imag": 1.25,
+            "amplitude": 1.25,
+            "phase_deg": 90.0,
+            "is_centric": False,
+        },
+        {
+            "hkl": [0, 2, 0],
+            "real": 0.0,
+            "imag": -0.75,
+            "amplitude": 0.75,
+            "phase_deg": -90.0,
+            "is_centric": False,
+        },
+    ]
+    maps_mtz_bytes, obs_pair_mtz_bytes = _build_synthetic_map_mtz_pair(
+        map_records_2fofc, map_records_fofc
+    )
+    map_params = {
+        "acentrics_scale": 2.0,
+        "centrics_pre_scale": 1.0,
+        "exclude_free_r_reflections": False,
+        "fill_missing": False,
+        "isotropize": True,
+        "sharp": False,
+    }
+
+    runtime_cell = onesar_rerun.ccp4_mtz_float32_unit_cell(
+        input_ledger["crystallographic_context"]["cell_a_b_c_alpha_beta_gamma"]
+    )
+    ca_rt, cb_rt, cc_rt = runtime_cell[0], runtime_cell[1], runtime_cell[2]
+
+    payload = {
+        "schema_version": 1,
+        "algorithm": "mmtbx.model_vs_data.first_block_public_api",
+        "expected_resolved_prefix": "/opt/phenix",
+        "expected_phenix_version": toolchain.PHENIX_VERSION,
+        "runtime_environment": {
+            "python_version": "3.9.18",
+            "sys_executable": "/opt/phenix/bin/python3.9",
+            "resolved_sys_executable": "/opt/phenix/bin/python3.9",
+            "sys_prefix": "/opt/phenix",
+            "resolved_sys_prefix": "/opt/phenix",
+            "vendor_modules": vendor_modules,
+        },
+        "crystal_symmetry": {
+            "unit_cell": list(runtime_cell),
+            "space_group_symbol": "P 21 21 21 (No. 19)",
+            "space_group_number": 19,
+            "space_group_operators_xyz": [
+                "x,y,z",
+                "-x+1/2,-y,z+1/2",
+                "-x,y+1/2,-z+1/2",
+                "x+1/2,-y+1/2,-z",
+            ],
+        },
+        "extraction": {
+            "labels": ["F-obs", "SIGF-obs"],
+            "r_free_flags_label": "R-free-flags",
+            "test_flag_value": 1,
+            "r_free_flags_generate": False,
+            "ignore_r_free_flags": False,
+            "keep_going": False,
+            "err": [],
+            "log_text": "extract_xtal_data synthetic log\n",
+            "applied_pdb_header_resolution_cut": False,
+        },
+        "diagnostics": {
+            "extract_xtal_data_log": "extract_xtal_data synthetic log\n",
+            "model_manager_log": "model.manager synthetic log\n",
+            "fmodel_update_all_scales_log": "update_all_scales synthetic log\n",
+            "failure": None,
+        },
+        "resolution_ranges": {
+            "extracted_pre_fmodel": {
+                "d_max": cb_rt,
+                "d_min": 1.0
+                / math.sqrt(
+                    (1.0 / ca_rt) ** 2
+                    + (1.0 / cb_rt) ** 2
+                    + (1.0 / cc_rt) ** 2
+                ),
+            },
+            "final_effective": {"d_max": cb_rt, "d_min": ca_rt / 2.0},
+            "actual_work": {"d_max": cb_rt, "d_min": ca_rt},
+            "actual_free": {"d_max": cb_rt / 2.0, "d_min": ca_rt / 2.0},
+        },
+        "xray_structure_invariance": xrs_invariance,
+        "scales_and_rfactors": {
+            "r_work": r_work,
+            "r_free": r_free,
+            "r_all": r_all,
+            "r_free_r_work_gap": r_free - r_work,
+            "scale_k1_w": scale_k1_w,
+            "scale_k1_t": scale_k1_t,
+            "scale_k1": scale_k1,
+            "work_numerator": 20.0,
+            "work_denominator": 300.0,
+            "free_numerator": 50.0,
+            "free_denominator": 200.0,
+            "all_numerator": all_num,
+            "all_denominator": all_den,
+            "work_count": 2,
+            "free_count": 2,
+            "final_effective_count": 4,
+            "extracted_pre_fmodel_count": 5,
+        },
+        "extracted_pre_fmodel": [
+            {"hkl": [1, 0, 0], "f_obs": 100.0, "sigf_obs": 2.0, "is_free": False},
+            {"hkl": [0, 1, 0], "f_obs": 200.0, "sigf_obs": 3.0, "is_free": False},
+            {"hkl": [1, 1, 1], "f_obs": 150.0, "sigf_obs": 2.5, "is_free": False},
+            {"hkl": [2, 0, 0], "f_obs": 80.0, "sigf_obs": 1.8, "is_free": True},
+            {"hkl": [0, 2, 0], "f_obs": 120.0, "sigf_obs": 2.2, "is_free": True},
+        ],
+        "final_effective": [
+            {"hkl": hkl, "f_obs": fo, "sigf_obs": sigfo, "is_free": is_free}
+            for hkl, fo, sigfo, is_free in final_hkls
+        ],
+        "actual_work": [
+            {
+                "hkl": [1, 0, 0],
+                "f_obs": 100.0,
+                "sigf_obs": 2.0,
+                "f_model_real": 108.0,
+                "f_model_imag": 144.0,
+                "f_model_amplitude": 180.0,
+                "f_model_scaled_with_k1_w_real": 54.0,
+                "f_model_scaled_with_k1_w_imag": 72.0,
+                "f_model_scaled_with_k1_w_amplitude": 90.0,
+            },
+            {
+                "hkl": [0, 1, 0],
+                "f_obs": 200.0,
+                "sigf_obs": 3.0,
+                "f_model_real": 252.0,
+                "f_model_imag": 336.0,
+                "f_model_amplitude": 420.0,
+                "f_model_scaled_with_k1_w_real": 126.0,
+                "f_model_scaled_with_k1_w_imag": 168.0,
+                "f_model_scaled_with_k1_w_amplitude": 210.0,
+            },
+        ],
+        "actual_free": [
+            {
+                "hkl": [2, 0, 0],
+                "f_obs": 80.0,
+                "sigf_obs": 1.8,
+                "f_model_real": 60.0,
+                "f_model_imag": 80.0,
+                "f_model_amplitude": 100.0,
+                "f_model_scaled_with_k1_t_real": 36.0,
+                "f_model_scaled_with_k1_t_imag": 48.0,
+                "f_model_scaled_with_k1_t_amplitude": 60.0,
+            },
+            {
+                "hkl": [0, 2, 0],
+                "f_obs": 120.0,
+                "sigf_obs": 2.2,
+                "f_model_real": 150.0,
+                "f_model_imag": 200.0,
+                "f_model_amplitude": 250.0,
+                "f_model_scaled_with_k1_t_real": 90.0,
+                "f_model_scaled_with_k1_t_imag": 120.0,
+                "f_model_scaled_with_k1_t_amplitude": 150.0,
+            },
+        ],
+        "map_coefficients": {
+            "2mFo-DFc": {
+                "map_type": "2mFo-DFc",
+                "amplitude_label": "2FOFCWT",
+                "phase_label": "PH2FOFCWT",
+                "parameters": dict(map_params),
+                "coefficient_semantics": "combine_includes_scales",
+                "n_reflections": 4,
+                "centric_count": 2,
+                "acentric_count": 2,
+                "records": copy.deepcopy(map_records_2fofc),
+            },
+            "mFo-DFc": {
+                "map_type": "mFo-DFc",
+                "amplitude_label": "FOFCWT",
+                "phase_label": "PHFOFCWT",
+                "parameters": dict(map_params),
+                "coefficient_semantics": "combine_includes_scales",
+                "n_reflections": 4,
+                "centric_count": 2,
+                "acentric_count": 2,
+                "records": copy.deepcopy(map_records_fofc),
+            },
+            "mtz_supplements": {
+                "phx_maps_sha256": hashlib.sha256(maps_mtz_bytes).hexdigest(),
+                "phx_maps_columns": [
+                    "H",
+                    "K",
+                    "L",
+                    "2FOFCWT",
+                    "PH2FOFCWT",
+                    "FOFCWT",
+                    "PHFOFCWT",
+                ],
+                "obs_pair_only_sha256": hashlib.sha256(obs_pair_mtz_bytes).hexdigest(),
+                "obs_pair_only_columns": [
+                    "H",
+                    "K",
+                    "L",
+                    "2FOFCWT",
+                    "PH2FOFCWT",
+                ],
+            },
+        },
+    }
+    return input_ledger, payload, vendor_shas, maps_mtz_bytes, obs_pair_mtz_bytes
+
+
+class TestOnesarSubjectAndArchiveAdmission(unittest.TestCase):
+    def test_allowed_subjects_and_forbidden_r7(self) -> None:
+        expected_members = {
+            "start": ("data/1sar.pdb", "start"),
+            "R4": ("data/1sar_final.pdb", "final"),
+            "R5": ("data/1sar_round5.pdb", "intermediate"),
+            "R6": ("data/1sar_round6.pdb", "intermediate"),
+        }
+        for subj, (member, stage) in expected_members.items():
+            spec = onesar_rerun.resolve_subject_spec(subj)
+            self.assertEqual(spec["subject_id"], subj)
+            self.assertEqual(spec["member"], member)
+            self.assertEqual(spec["stage"], stage)
+            self.assertEqual(len(spec["sha256"]), 64)
+
+        for forbidden in (
+            "R7",
+            "round7",
+            "1sar_round7",
+            "data/1sar_round7.pdb",
+            "deposited",
+            "1sar_deposited",
+            "R1",
+            "",
+        ):
+            with self.assertRaises(ValueError):
+                onesar_rerun.resolve_subject_spec(forbidden)
+
+    def test_committed_archive_members_match_preregistered_hashes(self) -> None:
+        self.assertTrue(COMMITTED_ARCHIVE.is_file())
+        for subj in ("start", "R4", "R5", "R6"):
+            spec = onesar_rerun.resolve_subject_spec(subj)
+            data = onesar_rerun.read_verified_archive_member(
+                COMMITTED_ARCHIVE,
+                spec["member"],
+                expected_archive_sha256=onesar_rerun.ARCHIVE_SHA256,
+                expected_member_sha256=spec["sha256"],
+            )
+            self.assertEqual(hashlib.sha256(data).hexdigest(), spec["sha256"])
+
+        mtz_bytes = onesar_rerun.read_verified_archive_member(
+            COMMITTED_ARCHIVE,
+            onesar_rerun.MTZ_MEMBER,
+            expected_archive_sha256=onesar_rerun.ARCHIVE_SHA256,
+            expected_member_sha256=onesar_rerun.MTZ_SHA256,
+        )
+        self.assertEqual(
+            hashlib.sha256(mtz_bytes).hexdigest(), onesar_rerun.MTZ_SHA256
+        )
+
+    def test_archive_member_admission_rejects_bad_hashes_duplicates_symlinks_and_paths(
+        self,
+    ) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            tmp_dir = Path(tmp)
+            zip_path = tmp_dir / "test.zip"
+            payload = b"HEADER    TEST\nEND\n"
+            payload_sha = hashlib.sha256(payload).hexdigest()
+
+            with zipfile.ZipFile(zip_path, "w") as zf:
+                zf.writestr("data/model.pdb", payload)
+            zip_sha = hashlib.sha256(zip_path.read_bytes()).hexdigest()
+
+            # Valid read succeeds.
+            self.assertEqual(
+                onesar_rerun.read_verified_archive_member(
+                    zip_path,
+                    "data/model.pdb",
+                    expected_archive_sha256=zip_sha,
+                    expected_member_sha256=payload_sha,
+                ),
+                payload,
+            )
+
+            # Wrong archive hash fails.
+            with self.assertRaisesRegex(ValueError, "Archive SHA-256 mismatch"):
+                onesar_rerun.read_verified_archive_member(
+                    zip_path,
+                    "data/model.pdb",
+                    expected_archive_sha256="0" * 64,
+                    expected_member_sha256=payload_sha,
+                )
+
+            # Wrong member hash fails.
+            with self.assertRaisesRegex(ValueError, "SHA-256 mismatch"):
+                onesar_rerun.read_verified_archive_member(
+                    zip_path,
+                    "data/model.pdb",
+                    expected_archive_sha256=zip_sha,
+                    expected_member_sha256="f" * 64,
+                )
+
+            # Unsafe member names fail before touching the archive.
+            for bad_name in ("/etc/passwd", "../secret.pdb", "data\\model.pdb", "data//model.pdb"):
+                with self.assertRaisesRegex(ValueError, "Unsafe archive member"):
+                    onesar_rerun.read_verified_archive_member(
+                        zip_path,
+                        bad_name,
+                        expected_archive_sha256=zip_sha,
+                        expected_member_sha256=payload_sha,
+                    )
+
+            # Symlink to archive fails.
+            link_zip = tmp_dir / "link.zip"
+            link_zip.symlink_to(zip_path)
+            with self.assertRaisesRegex(ValueError, "Regular non-symlink file"):
+                onesar_rerun.read_verified_archive_member(
+                    link_zip,
+                    "data/model.pdb",
+                    expected_archive_sha256=zip_sha,
+                    expected_member_sha256=payload_sha,
+                )
+
+            # Duplicate member entries fail (suppress zipfile's own UserWarning on write).
+            dup_zip = tmp_dir / "dup.zip"
+            with warnings.catch_warnings():
+                warnings.simplefilter("ignore", UserWarning)
+                with zipfile.ZipFile(dup_zip, "w") as zf:
+                    zf.writestr("data/model.pdb", payload)
+                    zf.writestr("data/model.pdb", payload)
+            dup_sha = hashlib.sha256(dup_zip.read_bytes()).hexdigest()
+            with self.assertRaisesRegex(ValueError, "duplicate entries"):
+                onesar_rerun.read_verified_archive_member(
+                    dup_zip,
+                    "data/model.pdb",
+                    expected_archive_sha256=dup_sha,
+                    expected_member_sha256=payload_sha,
+                )
+
+            # Symlink ZIP member entry fails.
+            sym_zip = tmp_dir / "sym_member.zip"
+            info = zipfile.ZipInfo("data/model.pdb")
+            info.external_attr = (0o120777) << 16
+            with zipfile.ZipFile(sym_zip, "w") as zf:
+                zf.writestr(info, payload)
+            sym_sha = hashlib.sha256(sym_zip.read_bytes()).hexdigest()
+            with self.assertRaisesRegex(ValueError, "is a symlink"):
+                onesar_rerun.read_verified_archive_member(
+                    sym_zip,
+                    "data/model.pdb",
+                    expected_archive_sha256=sym_sha,
+                    expected_member_sha256=payload_sha,
+                )
+
+
+class TestFreshEvidenceRootBoundary(unittest.TestCase):
+    def test_fresh_evidence_root_rejects_existing_symlinks_and_outside_repo(
+        self,
+    ) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            fake_repo = Path(tmp).resolve(strict=True) / "repo"
+            fake_repo.mkdir()
+
+            fresh = onesar_rerun.prepare_fresh_evidence_root(
+                fake_repo / "runs" / "canary_01", repo_root=fake_repo
+            )
+            self.assertTrue(fresh.is_dir())
+
+            # Reusing an existing directory must fail with FileExistsError.
+            with self.assertRaises(FileExistsError):
+                onesar_rerun.prepare_fresh_evidence_root(fresh, repo_root=fake_repo)
+
+            # Repo root itself is rejected.
+            with self.assertRaisesRegex(ValueError, "strict subdirectory"):
+                onesar_rerun.prepare_fresh_evidence_root(fake_repo, repo_root=fake_repo)
+
+            # Path outside repo root is rejected.
+            outside = Path(tmp).resolve(strict=True) / "outside_dir"
+            with self.assertRaisesRegex(ValueError, "strict subdirectory"):
+                onesar_rerun.prepare_fresh_evidence_root(outside, repo_root=fake_repo)
+
+            # Symlink ancestor inside repo is rejected.
+            real_sub = fake_repo / "real_sub"
+            real_sub.mkdir()
+            sym_sub = fake_repo / "sym_sub"
+            sym_sub.symlink_to(real_sub)
+            with self.assertRaisesRegex(ValueError, "must not be a symlink"):
+                onesar_rerun.prepare_fresh_evidence_root(
+                    sym_sub / "child", repo_root=fake_repo
+                )
+
+
+class TestMtzProjectionAndHklLedger(unittest.TestCase):
+    def test_committed_1sar_mtz_projection_and_ledger_counts(self) -> None:
+        mtz_bytes = onesar_rerun.read_verified_archive_member(
+            COMMITTED_ARCHIVE,
+            onesar_rerun.MTZ_MEMBER,
+            expected_archive_sha256=onesar_rerun.ARCHIVE_SHA256,
+            expected_member_sha256=onesar_rerun.MTZ_SHA256,
+        )
+        projected_bytes, proj_summary = onesar_rerun.project_observation_mtz(mtz_bytes)
+        self.assertEqual(proj_summary["input_ncols"], 16)
+        self.assertEqual(proj_summary["projected_ncols"], 6)
+        self.assertEqual(proj_summary["nrows"], 7248)
+        self.assertEqual(
+            proj_summary["projected_columns"],
+            ["H", "K", "L", "F-obs", "SIGF-obs", "R-free-flags"],
+        )
+        self.assertEqual(proj_summary["retained_dataset_ids"], ["1"])
+        self.assertEqual(
+            set(proj_summary["crystallographic_context"]),
+            {
+                "cell_a_b_c_alpha_beta_gamma",
+                "resolution_limits_inverse_a2",
+                "symmetry",
+            },
+        )
+        self.assertEqual(
+            proj_summary["retention_audit"]["audit_status"], "preserved"
+        )
+
+        ledger = onesar_rerun.build_input_hkl_ledger(projected_bytes)
+        self.assertEqual(
+            ledger["crystallographic_context"],
+            proj_summary["crystallographic_context"],
+        )
+        counts = ledger["counts"]
+        self.assertEqual(counts["raw_total"], 7248)
+        self.assertEqual(counts["raw_work"], 6505)
+        self.assertEqual(counts["raw_free"], 723)
+        self.assertEqual(counts["raw_unassigned"], 20)
+        self.assertEqual(counts["eligible_total"], 7228)
+        self.assertEqual(counts["eligible_work"], 6505)
+        self.assertEqual(counts["eligible_free"], 723)
+        self.assertEqual(counts["eligible_zero_f"], 11)
+        self.assertEqual(counts["eligible_negative_f"], 0)
+        self.assertEqual(counts["ineligible_total"], 20)
+        self.assertEqual(counts["ineligible_nonfinite_f"], 20)
+        self.assertEqual(counts["ineligible_nonpositive_or_nonfinite_sigf"], 0)
+        self.assertEqual(counts["ineligible_unassigned_flag"], 20)
+        self.assertEqual(len(ledger["ineligible_rows"]), 20)
+        for row in ledger["ineligible_rows"]:
+            self.assertIsNone(row["f_obs"])
+            self.assertIsNone(row["sigf_obs"])
+            self.assertIsNone(row["r_free_flag"])
+            self.assertIsNone(row["is_free"])
+            self.assertEqual(row["partition"], "unassigned")
+            self.assertEqual(len(row["f_obs_word_hex"]), 8)
+            self.assertEqual(len(row["sigf_obs_word_hex"]), 8)
+            self.assertEqual(len(row["r_free_flag_word_hex"]), 8)
+
+    def test_synthetic_mtz_eligibility_zero_negative_nan_and_failure_modes(
+        self,
+    ) -> None:
+        nan = float("nan")
+        # Include archived columns in synthetic MTZ to verify projection strips them.
+        cols = [
+            ("H", "H", "1"),
+            ("K", "H", "1"),
+            ("L", "H", "1"),
+            ("F-obs", "F", "1"),
+            ("SIGF-obs", "Q", "1"),
+            ("R-free-flags", "I", "1"),
+            ("F-model", "F", "2"),
+            ("2FOFCWT", "F", "3"),
+        ]
+        datasets = [
+            (1, "Original-experimental-data-mapped-to-asu"),
+            (2, "Model-structure-factors"),
+            (3, "Electron-density"),
+        ]
+        mtz_bytes = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 50.0, 2.0, 0.0, 48.0, 51.0),
+                (0, 1, 0, 0.0, 1.0, 0.0, 1.0, 0.5),
+                (0, 0, 1, -3.5, 0.8, 1.0, 2.0, 0.2),
+                (1, 1, 0, nan, nan, 0.0, 10.0, 10.0),
+                (1, 0, 1, 25.0, 0.0, 1.0, 20.0, 22.0),
+                (0, 1, 1, 30.0, -1.0, 0.0, 28.0, 29.0),
+                (1, 1, 1, nan, nan, nan, nan, nan),
+            ],
+            columns=cols,
+            datasets=datasets,
+        )
+        proj_bytes, proj_summary = onesar_rerun.project_observation_mtz(mtz_bytes)
+        self.assertEqual(proj_summary["input_ncols"], 8)
+        self.assertEqual(proj_summary["projected_ncols"], 6)
+        self.assertEqual(proj_summary["retained_dataset_ids"], ["1"])
+
+        ledger = onesar_rerun.build_input_hkl_ledger(proj_bytes)
+        counts = ledger["counts"]
+        self.assertEqual(counts["raw_total"], 7)
+        self.assertEqual(counts["raw_work"], 4)
+        self.assertEqual(counts["raw_free"], 2)
+        self.assertEqual(counts["raw_unassigned"], 1)
+        self.assertEqual(counts["eligible_total"], 3)
+        self.assertEqual(counts["eligible_work"], 2)
+        self.assertEqual(counts["eligible_free"], 1)
+        self.assertEqual(counts["eligible_zero_f"], 1)
+        self.assertEqual(counts["eligible_negative_f"], 1)
+        self.assertEqual(counts["ineligible_total"], 4)
+        self.assertEqual(counts["ineligible_nonfinite_f"], 2)
+        self.assertEqual(counts["ineligible_nonpositive_or_nonfinite_sigf"], 2)
+        self.assertEqual(counts["ineligible_unassigned_flag"], 1)
+        self.assertTrue(ledger["eligible_by_hkl"]["0,1,0"]["is_zero_f"])
+        self.assertTrue(ledger["eligible_by_hkl"]["0,0,1"]["is_negative_f"])
+        unassigned_rows = [
+            r for r in ledger["ineligible_rows"] if r["hkl"] == [1, 1, 1]
+        ]
+        self.assertEqual(len(unassigned_rows), 1)
+        self.assertIsNone(unassigned_rows[0]["r_free_flag"])
+        self.assertIsNone(unassigned_rows[0]["is_free"])
+        self.assertEqual(unassigned_rows[0]["partition"], "unassigned")
+
+        # Duplicate HKL fails.
+        dup_mtz = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 50.0, 2.0, 0.0),
+                (1, 0, 0, 60.0, 2.5, 1.0),
+            ]
+        )
+        with self.assertRaisesRegex(ValueError, "Duplicate Miller index"):
+            onesar_rerun.build_input_hkl_ledger(dup_mtz)
+
+        # NaN R-free flag on an eligible reflection fails.
+        nan_flag_mtz = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 50.0, 2.0, nan),
+                (0, 1, 0, 60.0, 2.5, 1.0),
+            ]
+        )
+        with self.assertRaisesRegex(ValueError, "Invalid R-free flag value"):
+            onesar_rerun.build_input_hkl_ledger(nan_flag_mtz)
+
+        # Undeclared integer flag (e.g. 2) on an eligible or ineligible reflection fails.
+        bad_flag_mtz = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 50.0, 2.0, 0.0),
+                (0, 1, 0, 60.0, 2.5, 2.0),
+            ]
+        )
+        with self.assertRaisesRegex(ValueError, "Invalid R-free flag value"):
+            onesar_rerun.build_input_hkl_ledger(bad_flag_mtz)
+
+        bad_ineligible_flag_mtz = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 50.0, 2.0, 0.0),
+                (0, 1, 0, 60.0, 2.5, 1.0),
+                (0, 0, 1, nan, nan, 2.0),
+            ]
+        )
+        with self.assertRaisesRegex(ValueError, "Invalid R-free flag value"):
+            onesar_rerun.build_input_hkl_ledger(bad_ineligible_flag_mtz)
+
+        # Non-integral Miller index fails.
+        frac_hkl_mtz = _build_synthetic_mtz(
+            [
+                (1.5, 0, 0, 50.0, 2.0, 0.0),
+                (0, 1, 0, 60.0, 2.5, 1.0),
+            ]
+        )
+        with self.assertRaisesRegex(ValueError, "Non-integral Miller index"):
+            onesar_rerun.build_input_hkl_ledger(frac_hkl_mtz)
+
+
+class TestPhenixReconciliationAndPureHelpers(unittest.TestCase):
+    def _reconcile(
+        self,
+        payload: dict[str, Any],
+        ledger: dict[str, Any],
+        vendor_shas: dict[str, str],
+        maps_bytes: bytes,
+        pair_bytes: bytes,
+        **kwargs: Any,
+    ) -> dict[str, Any]:
+        return onesar_rerun.reconcile_phenix_rfactor_output(
+            payload,
+            ledger,
+            expected_vendor_source_sha256=vendor_shas,
+            phx_maps_mtz_bytes=maps_bytes,
+            phx_obs_pair_only_mtz_bytes=pair_bytes,
+            **kwargs,
+        )
+
+    def test_reconcile_valid_payload_and_stage_exclusions(self) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+        result = self._reconcile(
+            payload, ledger, vendor_shas, maps_bytes, pair_bytes
+        )
+        self.assertEqual(result["reconciliation_status"], "verified")
+        self.assertEqual(result["counts"]["raw_mtz_total"], 8)
+        self.assertEqual(result["counts"]["input_eligible_total"], 7)
+        self.assertEqual(result["counts"]["extracted_pre_fmodel_total"], 5)
+        self.assertEqual(result["counts"]["final_effective_total"], 4)
+        self.assertEqual(result["counts"]["actual_work_total"], 2)
+        self.assertEqual(result["counts"]["actual_free_total"], 2)
+        self.assertEqual(
+            result["set_differences"]["raw_to_input_eligible_excluded_hkls"],
+            [[0, 0, 2]],
+        )
+        self.assertEqual(
+            result["set_differences"]["input_eligible_to_extracted_excluded_hkls"],
+            [[0, 0, 1], [1, 1, 0]],
+        )
+        self.assertEqual(
+            result["set_differences"]["extracted_to_final_effective_excluded_hkls"],
+            [[1, 1, 1]],
+        )
+        self.assertAlmostEqual(
+            result["rfactors"]["replayed_r_work"], 20.0 / 300.0, places=15
+        )
+        self.assertAlmostEqual(
+            result["rfactors"]["replayed_r_free"], 0.25, places=15
+        )
+        self.assertAlmostEqual(
+            result["rfactors"]["replayed_r_all"], 70.6 / 500.0, places=15
+        )
+        self.assertEqual(
+            set(result["resolution_ranges"]),
+            {
+                "extracted_pre_fmodel",
+                "final_effective",
+                "actual_work",
+                "actual_free",
+            },
+        )
+        ca_f32, cb_f32 = (
+            struct.unpack(" None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+        # Substitute work scale_k1_w (0.5) in place of free scale_k1_t (0.6) on free set.
+        bad = copy.deepcopy(payload)
+        bad["actual_free"][0]["f_model_scaled_with_k1_t_real"] = 30.0
+        bad["actual_free"][0]["f_model_scaled_with_k1_t_imag"] = 40.0
+        bad["actual_free"][0]["f_model_scaled_with_k1_t_amplitude"] = 50.0
+        with self.assertRaisesRegex(
+            ValueError, "disagrees with scale_k1_t \\* \\|f_model_free\\|"
+        ):
+            self._reconcile(bad, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Complex sign reversal or imaginary sign flip that preserves amplitude and
+        # scalar R_work / R_free must be rejected for both actual_work and actual_free (#1021).
+        work_sign_flip = copy.deepcopy(payload)
+        work_sign_flip["actual_work"][0]["f_model_scaled_with_k1_w_real"] = (
+            -54.0
+        )
+        work_sign_flip["actual_work"][0]["f_model_scaled_with_k1_w_imag"] = (
+            -72.0
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            "actual_work\\[1,0,0\\] f_model_scaled_with_k1_w complex components .* disagree with scale_k1_w \\* f_model_work",
+        ):
+            self._reconcile(
+                work_sign_flip, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        work_imag_flip = copy.deepcopy(payload)
+        work_imag_flip["actual_work"][1]["f_model_scaled_with_k1_w_imag"] = (
+            -168.0
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            "actual_work\\[0,1,0\\] f_model_scaled_with_k1_w complex components .* disagree with scale_k1_w \\* f_model_work",
+        ):
+            self._reconcile(
+                work_imag_flip, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        free_sign_flip = copy.deepcopy(payload)
+        free_sign_flip["actual_free"][0]["f_model_scaled_with_k1_t_real"] = (
+            -36.0
+        )
+        free_sign_flip["actual_free"][0]["f_model_scaled_with_k1_t_imag"] = (
+            -48.0
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            "actual_free\\[2,0,0\\] f_model_scaled_with_k1_t complex components .* disagree with scale_k1_t \\* f_model_free",
+        ):
+            self._reconcile(
+                free_sign_flip, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        free_imag_flip = copy.deepcopy(payload)
+        free_imag_flip["actual_free"][1]["f_model_scaled_with_k1_t_imag"] = (
+            -120.0
+        )
+        with self.assertRaisesRegex(
+            ValueError,
+            "actual_free\\[0,2,0\\] f_model_scaled_with_k1_t complex components .* disagree with scale_k1_t \\* f_model_free",
+        ):
+            self._reconcile(
+                free_imag_flip, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+    def test_reconcile_rejects_partition_flag_and_set_violations(self) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+
+        # Overlapping work and free HKL fails.
+        overlap = copy.deepcopy(payload)
+        overlap["actual_free"].append(
+            {
+                "hkl": [1, 0, 0],
+                "f_obs": 100.0,
+                "sigf_obs": 2.0,
+                "f_model_real": 100.0,
+                "f_model_imag": 0.0,
+                "f_model_amplitude": 100.0,
+                "f_model_scaled_with_k1_t_real": 60.0,
+                "f_model_scaled_with_k1_t_imag": 0.0,
+                "f_model_scaled_with_k1_t_amplitude": 60.0,
+            }
+        )
+        with self.assertRaisesRegex(ValueError, "non-disjoint"):
+            self._reconcile(overlap, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Swapped 0/1 flag assignment fails.
+        swapped = copy.deepcopy(payload)
+        swapped["extracted_pre_fmodel"][0]["is_free"] = True
+        with self.assertRaisesRegex(ValueError, "Flag mismatch at extracted HKL"):
+            self._reconcile(swapped, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Unknown HKL not in raw MTZ fails.
+        unknown = copy.deepcopy(payload)
+        unknown["extracted_pre_fmodel"].append(
+            {"hkl": [9, 9, 9], "f_obs": 50.0, "sigf_obs": 1.0, "is_free": False}
+        )
+        with self.assertRaisesRegex(ValueError, "absent from input MTZ raw ledger"):
+            self._reconcile(unknown, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Second-block PDB header cutoff flag fails.
+        second_block = copy.deepcopy(payload)
+        second_block["extraction"]["applied_pdb_header_resolution_cut"] = True
+        with self.assertRaisesRegex(
+            ValueError, "Second-block PDB header resolution cutoff"
+        ):
+            self._reconcile(
+                second_block, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+    def test_reconcile_rejects_scatterer_mutation_map_contract_and_prefix_mix(
+        self,
+    ) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+
+        # Scatterer coordinate or occupancy drift fails even when top-level diff is spoofed.
+        mut_xrs = copy.deepcopy(payload)
+        mut_xrs["xray_structure_invariance"]["max_abs_occupancy_diff"] = 1e-4
+        with self.assertRaisesRegex(
+            ValueError, "Non-zero max_abs_occupancy_diff"
+        ):
+            self._reconcile(mut_xrs, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Mutate a scatterer record in after_fmodel without updating its sha256 -> fails digest check.
+        mut_snap = copy.deepcopy(payload)
+        mut_snap["xray_structure_invariance"]["snapshots"]["after_fmodel"][
+            "records"
+        ][0]["site_cart"][0] += 0.01
+        with self.assertRaisesRegex(
+            ValueError, "after_fmodel snapshot sha256 mismatch"
+        ):
+            self._reconcile(mut_snap, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Mutate a scatterer record AND recompute after_fmodel["sha256"] while spoofing
+        # top-level after_fmodel_sha256 and max_abs_site_cart_diff=0 -> still caught!
+        spoofed_snap = copy.deepcopy(payload)
+        bad_records = copy.deepcopy(
+            spoofed_snap["xray_structure_invariance"]["snapshots"]["after_fmodel"][
+                "records"
+            ]
+        )
+        bad_records[0]["occupancy"] = 0.75
+        recomputed_bad = onesar_phenix_measure.snapshot_scatterer_records(
+            bad_records
+        )
+        spoofed_snap["xray_structure_invariance"]["snapshots"]["after_fmodel"] = (
+            recomputed_bad
+        )
+        with self.assertRaisesRegex(ValueError, "Scatterer snapshot mutated"):
+            self._reconcile(
+                spoofed_snap, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Missing snapshots fails.
+        no_snaps = copy.deepcopy(payload)
+        del no_snaps["xray_structure_invariance"]["snapshots"]
+        with self.assertRaisesRegex(
+            ValueError,
+            "xray_structure_invariance must contain snapshots",
+        ):
+            self._reconcile(no_snaps, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Map coefficient fill_missing=True fails.
+        bad_map = copy.deepcopy(payload)
+        bad_map["map_coefficients"]["2mFo-DFc"]["parameters"]["fill_missing"] = True
+        with self.assertRaisesRegex(
+            ValueError, "parameters violate preregistered contract"
+        ):
+            self._reconcile(bad_map, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+        # Mixed vendor sys.prefix fails.
+        bad_prefix = copy.deepcopy(payload)
+        bad_prefix["runtime_environment"]["vendor_modules"][
+            "vendor_mmtbx_model_vs_data"
+        ]["resolved_path"] = "/other/install/mmtbx/model_vs_data.py"
+        with self.assertRaisesRegex(ValueError, "escapes resolved_sys_prefix"):
+            self._reconcile(
+                bad_prefix, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Same source hashes under a different installation prefix fails (#1013).
+        other_prefix = copy.deepcopy(payload)
+        other_prefix["expected_resolved_prefix"] = "/other/phenix"
+        other_prefix["runtime_environment"]["sys_prefix"] = "/other/phenix"
+        other_prefix["runtime_environment"]["resolved_sys_prefix"] = "/other/phenix"
+        other_prefix["runtime_environment"]["sys_executable"] = (
+            "/other/phenix/bin/python3.9"
+        )
+        other_prefix["runtime_environment"]["resolved_sys_executable"] = (
+            "/other/phenix/bin/python3.9"
+        )
+        for mod_key, rel in onesar_rerun.PHENIX_VENDOR_SOURCE_RELPATHS.items():
+            other_prefix["runtime_environment"]["vendor_modules"][mod_key][
+                "resolved_path"
+            ] = f"/other/phenix/lib/python3.9/site-packages/{rel}"
+        with self.assertRaisesRegex(
+            ValueError, "resolved_sys_prefix .* != expected_resolved_prefix"
+        ):
+            self._reconcile(
+                other_prefix,
+                ledger,
+                vendor_shas,
+                maps_bytes,
+                pair_bytes,
+                expected_resolved_prefix="/opt/phenix",
+            )
+
+        # Wrong PHENIX version in payload fails (#1013).
+        bad_ver = copy.deepcopy(payload)
+        bad_ver["expected_phenix_version"] = "2.0-5935"
+        with self.assertRaisesRegex(
+            ValueError, "expected_phenix_version '2.0-5935' !="
+        ):
+            self._reconcile(bad_ver, ledger, vendor_shas, maps_bytes, pair_bytes)
+
+    def test_full_precision_counts_sums_and_r_all_reconciliation_1015(self) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+
+        # Per-HKL f_obs mismatch between final_effective and actual_work fails.
+        bad_final_fo = copy.deepcopy(payload)
+        bad_final_fo["final_effective"][0]["f_obs"] = 100.5
+        with self.assertRaisesRegex(
+            ValueError, "actual_work\\[1,0,0\\] F/SIGF .* disagrees with final_effective"
+        ):
+            self._reconcile(
+                bad_final_fo, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Per-HKL sigf_obs mismatch between final_effective and actual_free fails.
+        bad_final_sigf = copy.deepcopy(payload)
+        bad_final_sigf["final_effective"][2]["sigf_obs"] = 9.9
+        with self.assertRaisesRegex(
+            ValueError, "actual_free\\[2,0,0\\] F/SIGF .* disagrees with final_effective"
+        ):
+            self._reconcile(
+                bad_final_sigf, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Mutated work_count / free_count / final_effective_count / extracted_pre_fmodel_count fails.
+        for count_key in (
+            "work_count",
+            "free_count",
+            "final_effective_count",
+            "extracted_pre_fmodel_count",
+        ):
+            bad_cnt = copy.deepcopy(payload)
+            bad_cnt["scales_and_rfactors"][count_key] += 1
+            with self.assertRaisesRegex(
+                ValueError, "Reported scales_and_rfactors counts disagree"
+            ):
+                self._reconcile(
+                    bad_cnt, ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+            bool_cnt = copy.deepcopy(payload)
+            bool_cnt["scales_and_rfactors"][count_key] = True
+            with self.assertRaisesRegex(ValueError, "must be a positive int"):
+                self._reconcile(
+                    bool_cnt, ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+
+        # Mutated numerator/denominator sums fail.
+        for sum_key, err_pat in (
+            ("work_numerator", "Reported work_numerator"),
+            ("work_denominator", "Reported work_denominator"),
+            ("free_numerator", "Reported free_numerator"),
+            ("free_denominator", "Reported free_denominator"),
+            ("all_numerator", "Reported all_numerator"),
+            ("all_denominator", "Reported all_denominator"),
+        ):
+            bad_sum = copy.deepcopy(payload)
+            bad_sum["scales_and_rfactors"][sum_key] += 1.0
+            with self.assertRaisesRegex(ValueError, err_pat):
+                self._reconcile(
+                    bad_sum, ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+
+        # Mutated r_all (e.g. 0.14 instead of 70.6 / 500.0 = 0.1412) fails.
+        bad_r_all = copy.deepcopy(payload)
+        bad_r_all["scales_and_rfactors"]["r_all"] = 0.14
+        with self.assertRaisesRegex(
+            ValueError, "Replayed r_all .* disagrees with native fmodel.r_all"
+        ):
+            self._reconcile(
+                bad_r_all, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+    def test_resolution_ranges_reconciliation_and_cell_metric_1020(
+        self,
+    ) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+
+        # 0. Actual parser-produced ledger regression (#1008 / #1020):
+        # mtz_crystallographic_context emits exact decimal string tokens (e.g. '64.897', '9E+1').
+        parser_tokens = ledger["crystallographic_context"][
+            "cell_a_b_c_alpha_beta_gamma"
+        ]
+        self.assertEqual(
+            parser_tokens,
+            ["64.897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"],
+        )
+        self.assertTrue(all(type(tok) is str for tok in parser_tokens))
+        self.assertEqual(
+            onesar_rerun.validate_unit_cell_parameters(parser_tokens),
+            (64.897, 78.323, 38.792, 90.0, 90.0, 90.0),
+        )
+        expected_f32_cell = struct.unpack(
+            "<6f", struct.pack("<6f", 64.897, 78.323, 38.792, 90.0, 90.0, 90.0)
+        )
+        self.assertEqual(
+            onesar_rerun.ccp4_mtz_float32_unit_cell(parser_tokens),
+            expected_f32_cell,
+        )
+        baseline_res = self._reconcile(
+            payload, ledger, vendor_shas, maps_bytes, pair_bytes
+        )
+        self.assertEqual(baseline_res["reconciliation_status"], "verified")
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["input_header_cell_tokens"],
+            ["64.897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"],
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["input_header_cell_float64"],
+            [64.897, 78.323, 38.792, 90.0, 90.0, 90.0],
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["effective_runtime_unit_cell"],
+            list(expected_f32_cell),
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["max_abs_runtime_vs_float32_diff"],
+            0.0,
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["space_group_symbol"],
+            "P 21 21 21 (No. 19)",
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["space_group_number"], 19
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["raw_space_group_operators_xyz"],
+            [
+                "x,y,z",
+                "-x+1/2,-y,z+1/2",
+                "-x,y+1/2,-z+1/2",
+                "x+1/2,-y+1/2,-z",
+            ],
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"][
+                "normalized_space_group_operators_xyz"
+            ],
+            [
+                "-X+1/2,-Y,Z+1/2",
+                "-X,Y+1/2,-Z+1/2",
+                "X+1/2,-Y+1/2,-Z",
+                "X,Y,Z",
+            ],
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["normalized_lookup_symbol"],
+            "P212121",
+        )
+        self.assertEqual(
+            baseline_res["crystal_symmetry"]["symmetry_normalization_policy"],
+            onesar_rerun.SYMMETRY_OPERATOR_NORMALIZATION_POLICY,
+        )
+        # Reconciliation must preserve the original string tokens in the ledger untouched.
+        self.assertEqual(
+            ledger["crystallographic_context"]["cell_a_b_c_alpha_beta_gamma"],
+            ["64.897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"],
+        )
+        self.assertTrue(
+            all(
+                type(tok) is str
+                for tok in ledger["crystallographic_context"][
+                    "cell_a_b_c_alpha_beta_gamma"
+                ]
+            )
+        )
+
+        # Unrounded decimal-header resolution ranges or unrounded runtime unit_cell are rejected:
+        unrounded_ranges_payload = copy.deepcopy(payload)
+        unrounded_ranges_payload["resolution_ranges"]["extracted_pre_fmodel"][
+            "d_max"
+        ] = 78.323
+        with self.assertRaisesRegex(
+            ValueError,
+            "resolution_ranges\\['extracted_pre_fmodel'\\]\\.d_max 78\\.322999999999993 disagrees with recomputed 78\\.322998046875000",
+        ):
+            self._reconcile(
+                unrounded_ranges_payload,
+                ledger,
+                vendor_shas,
+                maps_bytes,
+                pair_bytes,
+            )
+
+        unrounded_runtime_cell_payload = copy.deepcopy(payload)
+        unrounded_runtime_cell_payload["crystal_symmetry"]["unit_cell"] = [
+            64.897,
+            78.323,
+            38.792,
+            90.0,
+            90.0,
+            90.0,
+        ]
+        with self.assertRaisesRegex(
+            ValueError,
+            "phenix_payload\\.crystal_symmetry\\.unit_cell\\[0\\] \\(a\\) .* disagrees with installed CCP4/iotbx float32 conversion",
+        ):
+            self._reconcile(
+                unrounded_runtime_cell_payload,
+                ledger,
+                vendor_shas,
+                maps_bytes,
+                pair_bytes,
+            )
+
+        for idx_symm, (bad_symm_fn, symm_err) in enumerate((
+            (
+                lambda p: p.pop("crystal_symmetry"),
+                "phenix_payload missing crystal_symmetry section",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": ""}
+                ),
+                "space_group_symbol must be a non-empty string",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"unit_cell": [64.897, 78.323, 38.792]}
+                ),
+                "unit_cell must be a 6-element numeric sequence",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "unit_cell": [
+                            "64.89700317382812",
+                            78.322998046875,
+                            38.79199981689453,
+                            90.0,
+                            90.0,
+                            90.0,
+                        ]
+                    }
+                ),
+                "must be a finite number",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "unit_cell": [
+                            65.0,
+                            78.322998046875,
+                            38.79199981689453,
+                            90.0,
+                            90.0,
+                            90.0,
+                        ]
+                    }
+                ),
+                "disagrees with installed CCP4/iotbx float32 conversion",
+            ),
+            # Missing / invalid / inconsistent runtime space_group_number (#1025):
+            (
+                lambda p: p["crystal_symmetry"].pop("space_group_number"),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": None}
+                ),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": True}
+                ),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": "19"}
+                ),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": 0}
+                ),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": 231}
+                ),
+                "space_group_number must be an int in 1..230",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_number": 1}
+                ),
+                "space_group_number 1 disagrees with input_ledger space_group_number 19",
+            ),
+            # Missing / invalid / unsupported-spelling / inconsistent runtime space_group_operators_xyz (#1025):
+            (
+                lambda p: p["crystal_symmetry"].pop(
+                    "space_group_operators_xyz"
+                ),
+                "space_group_operators_xyz must be a non-empty sequence",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_operators_xyz": None}
+                ),
+                "space_group_operators_xyz must be a non-empty sequence",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_operators_xyz": []}
+                ),
+                "space_group_operators_xyz must be a non-empty sequence",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_operators_xyz": ["x,y,z"]}
+                ),
+                "space_group_operators_xyz .* disagrees with input_ledger symmetry\\.operators_xyz",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "x,y,z",
+                            "X,Y,Z",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "contains duplicate normalized symmetry operators",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "x,y,z",
+                            "-x+1/4,-y,z+1/2",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "space_group_operators_xyz .* disagrees with input_ledger symmetry\\.operators_xyz",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "x,y,z",
+                            "-x+0.5,-y,z+0.5",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "uses unsupported or malformed symmetry operator spelling",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "x,y,z",
+                            "1/2-x,-y,1/2+z",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "uses unsupported or malformed symmetry operator spelling",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "x,y,z",
+                            "-x+2/4,-y,z+1/2",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "has non-canonical or unreduced translation",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "+x,y,z",
+                            "-x+1/2,-y,z+1/2",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "uses unsupported or malformed symmetry operator spelling",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {
+                        "space_group_operators_xyz": [
+                            "a,b,c",
+                            "-x+1/2,-y,z+1/2",
+                            "-x,y+1/2,-z+1/2",
+                            "x+1/2,-y+1/2,-z",
+                        ]
+                    }
+                ),
+                "uses unsupported or malformed symmetry operator spelling",
+            ),
+            # Non-empty wrong-group or inconsistent display string declarations (#1025):
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "P 1 (No. 1)"}
+                ),
+                "declares space-group number 1, which disagrees with space_group_number 19",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "P 21 21 21 (No. 1)"}
+                ),
+                "declares space-group number 1, which disagrees with space_group_number 19",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "P 1 (No. 19)"}
+                ),
+                "normalized symbol 'P1'\\) disagrees with input_ledger space_group_symbol 'P212121'",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "C 21 21 21 (No. 19)"}
+                ),
+                "normalized symbol 'C212121'\\) disagrees with input_ledger space_group_symbol 'P212121'",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "P 21 21 21"}
+                ),
+                "must match installed cctbx\\.sgtbx\\.space_group_info\\.symbol_and_number\\(\\) format",
+            ),
+            (
+                lambda p: p["crystal_symmetry"].update(
+                    {"space_group_symbol": "arbitrary_nonempty_string"}
+                ),
+                "must match installed cctbx\\.sgtbx\\.space_group_info\\.symbol_and_number\\(\\) format",
+            ),
+        )):
+            with self.subTest(idx=idx_symm, symm_err=symm_err):
+                bad_symm_payload = copy.deepcopy(payload)
+                bad_symm_fn(bad_symm_payload)
+                with self.assertRaisesRegex(ValueError, symm_err):
+                    self._reconcile(
+                        bad_symm_payload,
+                        ledger,
+                        vendor_shas,
+                        maps_bytes,
+                        pair_bytes,
+                    )
+
+        # Also verify the real committed 1SAR MTZ parser-produced ledger cell tokens normalize cleanly:
+        real_mtz_bytes = onesar_rerun.read_verified_archive_member(
+            COMMITTED_ARCHIVE,
+            onesar_rerun.MTZ_MEMBER,
+            expected_archive_sha256=onesar_rerun.ARCHIVE_SHA256,
+            expected_member_sha256=onesar_rerun.MTZ_SHA256,
+        )
+        real_proj_bytes, _ = onesar_rerun.project_observation_mtz(real_mtz_bytes)
+        real_ledger = onesar_rerun.build_input_hkl_ledger(real_proj_bytes)
+        real_cell_tokens = real_ledger["crystallographic_context"][
+            "cell_a_b_c_alpha_beta_gamma"
+        ]
+        self.assertTrue(all(type(tok) is str for tok in real_cell_tokens))
+        self.assertEqual(
+            onesar_rerun.validate_unit_cell_parameters(real_cell_tokens),
+            (64.897, 78.323, 38.792, 90.0, 90.0, 90.0),
+        )
+
+        # 1. Orthogonal cell closed-form check: (10, 20, 30, 90, 90, 90).
+        orth_cell = (10.0, 20.0, 30.0, 90.0, 90.0, 90.0)
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 0, 0], orth_cell),
+            10.0,
+            places=14,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([0, 1, 0], orth_cell),
+            20.0,
+            places=14,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([0, 0, 1], orth_cell),
+            30.0,
+            places=14,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 2, 3], orth_cell),
+            10.0 / math.sqrt(3.0),
+            places=14,
+        )
+
+        # 2. Non-orthogonal hexagonal cell closed-form check: (10, 10, 20, 90, 90, 120).
+        # Exact formula: 1/d^2 = (4/3)*(h^2 + h*k + k^2)/a^2 + l^2/c^2.
+        hex_cell = (10.0, 10.0, 20.0, 90.0, 90.0, 120.0)
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 0, 0], hex_cell),
+            5.0 * math.sqrt(3.0),
+            places=13,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 1, 0], hex_cell),
+            5.0,
+            places=13,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, -1, 0], hex_cell),
+            5.0 * math.sqrt(3.0),
+            places=13,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 1, 2], hex_cell),
+            2.0 * math.sqrt(5.0),
+            places=13,
+        )
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([2, 0, 0], hex_cell),
+            2.5 * math.sqrt(3.0),
+            places=13,
+        )
+
+        # Full reconcile_phenix_rfactor_output on a non-orthogonal hexagonal cell:
+        # fixture stage HKLs:
+        #   extracted_pre_fmodel: (1,0,0)->5*sqrt(3), (0,1,0)->5*sqrt(3),
+        #     (1,1,1)->1/sqrt(12/300 + 1/400)=20/sqrt(17),
+        #     (2,0,0)->2.5*sqrt(3), (0,2,0)->2.5*sqrt(3)
+        #   final_effective: (1,0,0), (0,1,0), (2,0,0), (0,2,0)
+        #   actual_work: (1,0,0), (0,1,0) -> d = 5*sqrt(3)
+        #   actual_free: (2,0,0), (0,2,0) -> d = 2.5*sqrt(3)
+        hex_mtz_bytes = _build_synthetic_mtz(
+            [
+                (1, 0, 0, 100.0, 2.0, 0.0),
+                (0, 1, 0, 200.0, 3.0, 0.0),
+                (0, 0, 1, 0.0, 1.5, 0.0),
+                (1, 1, 0, -5.0, 1.2, 0.0),
+                (1, 1, 1, 150.0, 2.5, 0.0),
+                (2, 0, 0, 80.0, 1.8, 1.0),
+                (0, 2, 0, 120.0, 2.2, 1.0),
+                (0, 0, 2, float("nan"), float("nan"), float("nan")),
+            ],
+            cell=hex_cell,
+        )
+        hex_ledger = onesar_rerun.build_input_hkl_ledger(hex_mtz_bytes)
+        hex_maps_bytes, hex_pair_bytes = _build_synthetic_map_mtz_pair(
+            payload["map_coefficients"]["2mFo-DFc"]["records"],
+            payload["map_coefficients"]["mFo-DFc"]["records"],
+            cell=hex_cell,
+        )
+        hex_payload = copy.deepcopy(payload)
+        hex_payload["crystal_symmetry"]["unit_cell"] = list(
+            onesar_rerun.ccp4_mtz_float32_unit_cell(hex_cell)
+        )
+        hex_payload["map_coefficients"]["mtz_supplements"][
+            "phx_maps_sha256"
+        ] = hashlib.sha256(hex_maps_bytes).hexdigest()
+        hex_payload["map_coefficients"]["mtz_supplements"][
+            "obs_pair_only_sha256"
+        ] = hashlib.sha256(hex_pair_bytes).hexdigest()
+        hex_d_100 = 5.0 * math.sqrt(3.0)
+        hex_d_200 = 2.5 * math.sqrt(3.0)
+        hex_d_111 = 20.0 / math.sqrt(17.0)
+        hex_payload["resolution_ranges"] = {
+            "extracted_pre_fmodel": {"d_max": hex_d_100, "d_min": hex_d_200},
+            "final_effective": {"d_max": hex_d_100, "d_min": hex_d_200},
+            "actual_work": {"d_max": hex_d_100, "d_min": hex_d_100},
+            "actual_free": {"d_max": hex_d_200, "d_min": hex_d_200},
+        }
+        self.assertAlmostEqual(
+            onesar_rerun.compute_hkl_d_spacing([1, 1, 1], hex_cell),
+            hex_d_111,
+            places=13,
+        )
+        hex_res = self._reconcile(
+            hex_payload,
+            hex_ledger,
+            vendor_shas,
+            hex_maps_bytes,
+            hex_pair_bytes,
+        )
+        self.assertEqual(hex_res["reconciliation_status"], "verified")
+        self.assertAlmostEqual(
+            hex_res["resolution_ranges"]["actual_work"]["recomputed_d_max"],
+            hex_d_100,
+            places=12,
+        )
+
+        # Orthogonal approximation on the hexagonal cell (10.0 instead of 5*sqrt(3)) must fail!
+        hex_orth_approx = copy.deepcopy(hex_payload)
+        hex_orth_approx["resolution_ranges"]["actual_work"] = {
+            "d_max": 10.0,
+            "d_min": 10.0,
+        }
+        with self.assertRaisesRegex(
+            ValueError,
+            "resolution_ranges\\['actual_work'\\]\\.d_max .* disagrees with recomputed",
+        ):
+            self._reconcile(
+                hex_orth_approx,
+                hex_ledger,
+                vendor_shas,
+                hex_maps_bytes,
+                hex_pair_bytes,
+            )
+
+        # 3. General triclinic cell verified against independent 3D Cartesian cross-product basis:
+        tri_cell = (10.0, 12.0, 15.0, 80.0, 85.0, 70.0)
+        ta, tb, tc, talpha, tbeta, tgamma = tri_cell
+        ca_r = math.cos(math.radians(talpha))
+        cb_r = math.cos(math.radians(tbeta))
+        cg_r = math.cos(math.radians(tgamma))
+        sg_r = math.sin(math.radians(tgamma))
+        vec_a = (ta, 0.0, 0.0)
+        vec_b = (tb * cg_r, tb * sg_r, 0.0)
+        cx = tc * cb_r
+        cy = tc * (ca_r - cb_r * cg_r) / sg_r
+        cz = math.sqrt(tc * tc - cx * cx - cy * cy)
+        vec_c = (cx, cy, cz)
+
+        def _cross(
+            u: tuple[float, float, float], v: tuple[float, float, float]
+        ) -> tuple[float, float, float]:
+            return (
+                u[1] * v[2] - u[2] * v[1],
+                u[2] * v[0] - u[0] * v[2],
+                u[0] * v[1] - u[1] * v[0],
+            )
+
+        def _dot(
+            u: tuple[float, float, float], v: tuple[float, float, float]
+        ) -> float:
+            return u[0] * v[0] + u[1] * v[1] + u[2] * v[2]
+
+        vol = _dot(vec_a, _cross(vec_b, vec_c))
+        b_cross_c = _cross(vec_b, vec_c)
+        c_cross_a = _cross(vec_c, vec_a)
+        a_cross_b = _cross(vec_a, vec_b)
+        astar = (b_cross_c[0] / vol, b_cross_c[1] / vol, b_cross_c[2] / vol)
+        bstar = (c_cross_a[0] / vol, c_cross_a[1] / vol, c_cross_a[2] / vol)
+        cstar = (a_cross_b[0] / vol, a_cross_b[1] / vol, a_cross_b[2] / vol)
+
+        tri_hkls = [
+            [1, 0, 0],
+            [0, 1, 0],
+            [0, 0, 1],
+            [1, 2, -1],
+            [-2, 1, 3],
+            [3, -2, -4],
+        ]
+        indep_ds: list[float] = []
+        for h, k, l in tri_hkls:
+            sx = h * astar[0] + k * bstar[0] + l * cstar[0]
+            sy = h * astar[1] + k * bstar[1] + l * cstar[1]
+            sz = h * astar[2] + k * bstar[2] + l * cstar[2]
+            d_indep = 1.0 / math.sqrt(sx * sx + sy * sy + sz * sz)
+            indep_ds.append(d_indep)
+            self.assertAlmostEqual(
+                onesar_rerun.compute_hkl_d_spacing([h, k, l], tri_cell),
+                d_indep,
+                places=13,
+            )
+        tri_stage_range = onesar_rerun.compute_stage_resolution_range(
+            tri_hkls, tri_cell
+        )
+        self.assertAlmostEqual(
+            tri_stage_range["d_max"], max(indep_ds), places=13
+        )
+        self.assertAlmostEqual(
+            tri_stage_range["d_min"], min(indep_ds), places=13
+        )
+
+        # 4. Missing, malformed, swapped, nonfinite, boolean, cross-stage copied, or fabricated ranges fail:
+        missing_rr = copy.deepcopy(payload)
+        del missing_rr["resolution_ranges"]
+        with self.assertRaisesRegex(
+            ValueError, "phenix_payload\\.resolution_ranges must be a mapping"
+        ):
+            self._reconcile(
+                missing_rr, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        missing_stage = copy.deepcopy(payload)
+        del missing_stage["resolution_ranges"]["actual_free"]
+        with self.assertRaisesRegex(
+            ValueError, "phenix_payload\\.resolution_ranges must be a mapping"
+        ):
+            self._reconcile(
+                missing_stage, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        extra_stage = copy.deepcopy(payload)
+        extra_stage["resolution_ranges"]["raw_mtz"] = {
+            "d_max": 78.323,
+            "d_min": 64.897,
+        }
+        with self.assertRaisesRegex(
+            ValueError, "phenix_payload\\.resolution_ranges must be a mapping"
+        ):
+            self._reconcile(
+                extra_stage, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        missing_dmin = copy.deepcopy(payload)
+        del missing_dmin["resolution_ranges"]["actual_work"]["d_min"]
+        with self.assertRaisesRegex(
+            ValueError,
+            "resolution_ranges\\['actual_work'\\] must be a mapping with exact keys",
+        ):
+            self._reconcile(
+                missing_dmin, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        swapped_bounds = copy.deepcopy(payload)
+        swapped_bounds["resolution_ranges"]["actual_work"] = {
+            "d_max": 64.897,
+            "d_min": 78.323,
+        }
+        with self.assertRaisesRegex(
+            ValueError, "has inverted bounds: d_max=64.897 < d_min=78.323"
+        ):
+            self._reconcile(
+                swapped_bounds, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        for bad_val, err_pat in (
+            (float("nan"), "must be a finite number"),
+            (float("inf"), "must be a finite number"),
+            (0.0, "must be positive"),
+            (-5.0, "must be positive"),
+            (True, "must be a finite number"),
+        ):
+            bad_d = copy.deepcopy(payload)
+            bad_d["resolution_ranges"]["actual_work"]["d_min"] = bad_val
+            with self.assertRaisesRegex(ValueError, err_pat):
+                self._reconcile(
+                    bad_d, ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+
+        # Copying final_effective onto extracted_pre_fmodel, actual_work, or actual_free fails:
+        for stage_target, field_pat in (
+            ("extracted_pre_fmodel", "d_min"),
+            ("actual_work", "d_min"),
+            ("actual_free", "d_max"),
+        ):
+            copied_stage = copy.deepcopy(payload)
+            copied_stage["resolution_ranges"][stage_target] = copy.deepcopy(
+                payload["resolution_ranges"]["final_effective"]
+            )
+            with self.assertRaisesRegex(
+                ValueError,
+                f"resolution_ranges\\['{stage_target}'\\]\\.{field_pat} .* disagrees with recomputed",
+            ):
+                self._reconcile(
+                    copied_stage, ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+
+        # Fabricated 1.8 A range on the low-resolution fixture stage HKL set fails:
+        fabricated_18 = copy.deepcopy(payload)
+        fabricated_18["resolution_ranges"]["final_effective"]["d_min"] = 1.8
+        with self.assertRaisesRegex(
+            ValueError,
+            "resolution_ranges\\['final_effective'\\]\\.d_min 1\\.800000000000000 disagrees with recomputed",
+        ):
+            self._reconcile(
+                fabricated_18, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Numeric scalar/measurement checks remain strict (do not accept decimal strings):
+        str_d = copy.deepcopy(payload)
+        str_d["resolution_ranges"]["actual_work"]["d_min"] = "64.897"
+        with self.assertRaisesRegex(ValueError, "must be a finite number"):
+            self._reconcile(
+                str_d, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+        str_rw = copy.deepcopy(payload)
+        str_rw["scales_and_rfactors"]["r_work"] = "0.06666666666666667"
+        with self.assertRaisesRegex(ValueError, "must be a finite number"):
+            self._reconcile(
+                str_rw, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # 5. Origin (0, 0, 0) HKL and invalid/singular/malformed-token unit cells raise ValueError:
+        with self.assertRaisesRegex(
+            ValueError, "Miller index \\(0, 0, 0\\) has undefined d-spacing"
+        ):
+            onesar_rerun.compute_hkl_d_spacing([0, 0, 0], orth_cell)
+        with self.assertRaisesRegex(
+            ValueError, "Miller index \\(0, 0, 0\\) has undefined d-spacing"
+        ):
+            onesar_rerun.compute_stage_resolution_range([[0, 0, 0]], orth_cell)
+
+        for bad_cell, cell_err in (
+            ([10.0, 20.0, 30.0], "must be a 6-element sequence"),
+            ((0.0, 20.0, 30.0, 90.0, 90.0, 90.0), "must be positive"),
+            ((-10.0, 20.0, 30.0, 90.0, 90.0, 90.0), "must be positive"),
+            ((True, 20.0, 30.0, 90.0, 90.0, 90.0), "must be a finite number"),
+            ((10.0, 20.0, 30.0, 0.0, 90.0, 90.0), "must be positive"),
+            (
+                (10.0, 20.0, 30.0, 180.0, 90.0, 90.0),
+                "must be in \\(0, 180\\) degrees",
+            ),
+            (
+                (10.0, 20.0, 30.0, 90.0, 120.0, 150.0),
+                "violate crystallographic cone inequalities",
+            ),
+            (
+                (10.0, 20.0, 30.0, 40.0, 50.0, 100.0),
+                "violate crystallographic cone inequalities",
+            ),
+            (
+                ("", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a valid decimal unit-cell token",
+            ),
+            (
+                (" 64.897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a valid decimal unit-cell token",
+            ),
+            (
+                ("64.897 ", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a valid decimal unit-cell token",
+            ),
+            (
+                ("64_897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a valid decimal unit-cell token",
+            ),
+            (
+                ("64.897a", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a valid decimal unit-cell token",
+            ),
+            (
+                ("NaN", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a finite number",
+            ),
+            (
+                ("inf", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a finite number",
+            ),
+            (
+                ("-Infinity", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a finite number",
+            ),
+            (
+                ("1e1000", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be a finite number",
+            ),
+            (
+                ("0", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be positive",
+            ),
+            (
+                ("-64.897", "78.323", "38.792", "9E+1", "9E+1", "9E+1"),
+                "must be positive",
+            ),
+            (
+                ("64.897", "78.323", "38.792", "1.8E+2", "9E+1", "9E+1"),
+                "must be in \\(0, 180\\) degrees",
+            ),
+        ):
+            with self.assertRaisesRegex(ValueError, cell_err):
+                onesar_rerun.validate_unit_cell_parameters(bad_cell)
+            bad_ledger = copy.deepcopy(ledger)
+            bad_ledger["crystallographic_context"][
+                "cell_a_b_c_alpha_beta_gamma"
+            ] = list(bad_cell)
+            with self.assertRaisesRegex(ValueError, cell_err):
+                self._reconcile(
+                    payload, bad_ledger, vendor_shas, maps_bytes, pair_bytes
+                )
+
+    def test_map_coefficients_and_mtz_supplements_reconciliation_1016(self) -> None:
+        ledger, payload, vendor_shas, maps_bytes, pair_bytes = (
+            _make_reconciliation_fixture()
+        )
+
+        # Mutated JSON map amplitude or phase fails.
+        bad_json_amp = copy.deepcopy(payload)
+        bad_json_amp["map_coefficients"]["2mFo-DFc"]["records"][0][
+            "amplitude"
+        ] = 11.0
+        with self.assertRaisesRegex(
+            ValueError, "amplitude 11.0 != hypot\\(real, imag\\)"
+        ):
+            self._reconcile(
+                bad_json_amp, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        bad_json_phase = copy.deepcopy(payload)
+        bad_json_phase["map_coefficients"]["2mFo-DFc"]["records"][2][
+            "phase_deg"
+        ] = 45.0
+        with self.assertRaisesRegex(
+            ValueError, "phase_deg 45.0 disagrees with atan2"
+        ):
+            self._reconcile(
+                bad_json_phase, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Mutated centric_count fails.
+        bad_centric = copy.deepcopy(payload)
+        bad_centric["map_coefficients"]["2mFo-DFc"]["centric_count"] = 3
+        bad_centric["map_coefficients"]["2mFo-DFc"]["acentric_count"] = 1
+        with self.assertRaisesRegex(
+            ValueError, "centric/acentric counts .* !="
+        ):
+            self._reconcile(
+                bad_centric, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Wrong MTZ SHA-256 fails.
+        bad_sha = copy.deepcopy(payload)
+        bad_sha["map_coefficients"]["mtz_supplements"]["phx_maps_sha256"] = (
+            "0" * 64
+        )
+        with self.assertRaisesRegex(
+            ValueError, "phx_maps.mtz SHA-256 .* disagrees"
+        ):
+            self._reconcile(
+                bad_sha, ledger, vendor_shas, maps_bytes, pair_bytes
+            )
+
+        # Wrong column type in phx_maps.mtz (e.g. Q instead of P) fails.
+        bad_type_maps = _build_synthetic_mtz(
+            [(1, 0, 0, 10.0, 0.0, 2.0, 0.0)],
+            columns=[
+                ("H", "H", "1"),
+                ("K", "H", "1"),
+                ("L", "H", "1"),
+                ("2FOFCWT", "F", "1"),
+                ("PH2FOFCWT", "Q", "1"),
+                ("FOFCWT", "F", "1"),
+                ("PHFOFCWT", "P", "1"),
+            ],
+        )
+        bad_type_payload = copy.deepcopy(payload)
+        bad_type_payload["map_coefficients"]["mtz_supplements"][
+            "phx_maps_sha256"
+        ] = hashlib.sha256(bad_type_maps).hexdigest()
+        with self.assertRaisesRegex(
+            ValueError, "Map MTZ column types .* do not match expected"
+        ):
+            self._reconcile(
+                bad_type_payload,
+                ledger,
+                vendor_shas,
+                bad_type_maps,
+                pair_bytes,
+            )
+
+        # Mutated float32 amplitude in phx_maps.mtz fails.
+        mut_recs_2fofc = copy.deepcopy(
+            payload["map_coefficients"]["2mFo-DFc"]["records"]
+        )
+        mut_recs_2fofc[0]["amplitude"] = 99.0
+        bad_amp_maps, _ = _build_synthetic_map_mtz_pair(
+            mut_recs_2fofc, payload["map_coefficients"]["mFo-DFc"]["records"]
+        )
+        bad_amp_payload = copy.deepcopy(payload)
+        bad_amp_payload["map_coefficients"]["mtz_supplements"][
+            "phx_maps_sha256"
+        ] = hashlib.sha256(bad_amp_maps).hexdigest()
+        # Keep PHX_OBS_PAIR_ONLY matching bad_amp_maps so it reaches the JSON amplitude comparison:
+        _, bad_amp_pair = _build_synthetic_map_mtz_pair(
+            mut_recs_2fofc, payload["map_coefficients"]["mFo-DFc"]["records"]
+        )
+        bad_amp_payload["map_coefficients"]["mtz_supplements"][
+            "obs_pair_only_sha256"
+        ] = hashlib.sha256(bad_amp_pair).hexdigest()
+        with self.assertRaisesRegex(
+            ValueError, "phx_maps.mtz 2FOFCWT 99.0 disagrees with JSON amplitude 10.0"
+        ):
+            self._reconcile(
+                bad_amp_payload,
+                ledger,
+                vendor_shas,
+                bad_amp_maps,
+                bad_amp_pair,
+            )
+
+        # Mismatched 2FOFCWT word in PHX_OBS_PAIR_ONLY.mtz vs phx_maps.mtz fails.
+        mismatch_pair_payload = copy.deepcopy(payload)
+        mismatch_pair_payload["map_coefficients"]["mtz_supplements"][
+            "obs_pair_only_sha256"
+        ] = hashlib.sha256(bad_amp_pair).hexdigest()
+        with self.assertRaisesRegex(
+            ValueError,
+            "PHX_OBS_PAIR_ONLY.mtz 2FOFCWT/PH2FOFCWT words differ from phx_maps.mtz",
+        ):
+            self._reconcile(
+                mismatch_pair_payload,
+                ledger,
+                vendor_shas,
+                maps_bytes,
+                bad_amp_pair,
+            )
+
+    def test_helper_pure_functions_and_python39_static_contract(self) -> None:
+        # Pure scatterer snapshot comparison catches mutations.
+        base_rec = [
+            {
+                "label": "N",
+                "scattering_type": "N",
+                "site_frac": [0.1, 0.2, 0.3],
+                "site_cart": [6.4, 15.6, 11.6],
+                "occupancy": 1.0,
+                "u_iso": 0.25,
+                "u_star": [0.0, 0.0, 0.0, 0.0, 0.0, 0.0],
+                "use_u_iso": True,
+                "use_u_aniso": False,
+            }
+        ]
+        snap_before = onesar_phenix_measure.snapshot_scatterer_records(base_rec)
+        snap_same = onesar_phenix_measure.snapshot_scatterer_records(base_rec)
+        inv = onesar_phenix_measure.compare_scatterer_snapshots(
+            snap_before, snap_same, snap_same
+        )
+        self.assertEqual(inv["max_abs_site_cart_diff"], 0.0)
+
+        mut_rec = copy.deepcopy(base_rec)
+        mut_rec[0]["site_cart"][0] += 0.001
+        snap_mut = onesar_phenix_measure.snapshot_scatterer_records(mut_rec)
+        with self.assertRaisesRegex(ValueError, "Scatterer snapshot mutated"):
+            onesar_phenix_measure.compare_scatterer_snapshots(
+                snap_before, snap_mut, snap_same
+            )
+
+        # build_raw_scatterer_snapshot retains empty and non-finite records without
+        # validating finiteness or computing a canonical digest (#1014, Task 128):
+        raw_empty_snap = onesar_phenix_measure.build_raw_scatterer_snapshot([])
+        self.assertEqual(
+            raw_empty_snap,
+            {"n_scatterers": 0, "sha256": None, "records": []},
+        )
+        with self.subTest(
+            case="strict_snapshot_rejects_empty_scatterer_records"
+        ):
+            with self.assertRaisesRegex(
+                ValueError, "Scatterer snapshot cannot be empty"
+            ):
+                onesar_phenix_measure.snapshot_scatterer_records([])
+
+        nf_occ_bits = struct.unpack(
+            ">d", struct.pack(">Q", 0x7FF8000000000123)
+        )[0]
+        nf_rec = copy.deepcopy(base_rec)
+        nf_rec[0]["occupancy"] = nf_occ_bits
+        raw_nf_snap = onesar_phenix_measure.build_raw_scatterer_snapshot(nf_rec)
+        self.assertEqual(raw_nf_snap["n_scatterers"], 1)
+        self.assertIsNone(raw_nf_snap["sha256"])
+        encoded_raw_nf_snap = (
+            onesar_phenix_measure.encode_raw_evidence_nonfinite(
+                raw_nf_snap,
+                field_path="xray_structure_snapshots_raw.after_model",
+            )
+        )
+        self.assertEqual(
+            encoded_raw_nf_snap["records"][0]["occupancy"][
+                "ieee754_binary64_hex"
+            ],
+            "0x7ff8000000000123",
+        )
+        with self.subTest(
+            case="strict_snapshot_rejects_nonfinite_scatterer_record"
+        ):
+            with self.assertRaisesRegex(
+                ValueError, "Invalid scatterer record at index 0"
+            ):
+                onesar_phenix_measure.snapshot_scatterer_records(nf_rec)
+
+        # Symmetry operator normalization policy (#1025, Task 128):
+        self.assertEqual(
+            onesar_rerun.normalize_symmetry_operator_xyz(
+                " -x + 1/2,  -y,  z + 1/2 "
+            ),
+            "-X+1/2,-Y,Z+1/2",
+        )
+        for bad_op_case, bad_op_str, bad_op_pat in (
+            (
+                "symm_prefixed",
+                "SYMM   -x + 1/2,  -y,  z + 1/2 ",
+                "unsupported or malformed",
+            ),
+            (
+                "translation_first",
+                "1/2-X, -Y, 1/2+Z",
+                "unsupported or malformed",
+            ),
+            (
+                "leading_plus_translation_first",
+                "+1/2+X, +1/2-Y, -Z",
+                "unsupported or malformed",
+            ),
+            (
+                "wrong_component_count",
+                "X,Y",
+                "must have 3 comma-separated XYZ components",
+            ),
+            (
+                "decimal_translation",
+                "X+0.5,Y,Z",
+                "unsupported or malformed",
+            ),
+        ):
+            with self.subTest(case=f"normalize_symmetry_rejects_{bad_op_case}"):
+                with self.assertRaisesRegex(ValueError, bad_op_pat):
+                    onesar_rerun.normalize_symmetry_operator_xyz(bad_op_str)
+
+        # Strict-JSON IEEE-754 nonfinite tagger preserves finite values and tags NaN/+Inf/-Inf:
+        nf_locs: list[dict[str, Any]] = []
+        encoded_nf = onesar_phenix_measure.encode_raw_evidence_nonfinite(
+            {
+                "finite": 12.5,
+                "flag": True,
+                "count": 3,
+                "items": [float("nan"), float("inf"), float("-inf")],
+            },
+            nonfinite_locations=nf_locs,
+        )
+        self.assertEqual(encoded_nf["finite"], 12.5)
+        self.assertIs(encoded_nf["flag"], True)
+        self.assertEqual(encoded_nf["count"], 3)
+        self.assertEqual(len(nf_locs), 3)
+        self.assertEqual(
+            [item["kind"] for item in nf_locs], ["nan", "+inf", "-inf"]
+        )
+        self.assertEqual(
+            [item["field_path"] for item in nf_locs],
+            ["items[0]", "items[1]", "items[2]"],
+        )
+        self.assertEqual(
+            nf_locs[1]["ieee754_binary64_hex"], "0x7ff0000000000000"
+        )
+        self.assertEqual(
+            nf_locs[2]["ieee754_binary64_hex"], "0xfff0000000000000"
+        )
+        # Re-encoding an already tagged structure is idempotent:
+        nf_locs_second: list[dict[str, Any]] = []
+        encoded_nf_second = (
+            onesar_phenix_measure.encode_raw_evidence_nonfinite(
+                encoded_nf, nonfinite_locations=nf_locs_second
+            )
+        )
+        self.assertEqual(encoded_nf_second, encoded_nf)
+        self.assertEqual(nf_locs_second, nf_locs)
+
+        # Static AST verification of scripts/onesar_phenix_measure.py:
+        # - No top-level vendor imports
+        # - No Python 3.10+/3.11+ zip(strict=...) or hashlib.file_digest
+        # - No call to second-block get_r_rfree_sigma
+        helper_path = _SCRIPTS_DIR / "onesar_phenix_measure.py"
+        tree = ast.parse(helper_path.read_text(encoding="utf-8"))
+        forbidden_top_imports = {"cctbx", "iotbx", "mmtbx", "scitbx", "libtbx"}
+        for node in tree.body:
+            if isinstance(node, ast.Import):
+                for alias in node.names:
+                    root_mod = alias.name.split(".")[0]
+                    self.assertNotIn(root_mod, forbidden_top_imports)
+            elif isinstance(node, ast.ImportFrom) and node.module:
+                root_mod = node.module.split(".")[0]
+                self.assertNotIn(root_mod, forbidden_top_imports)
+
+        for node in ast.walk(tree):
+            if isinstance(node, ast.Call):
+                if isinstance(node.func, ast.Attribute):
+                    self.assertNotEqual(node.func.attr, "file_digest")
+                    self.assertNotEqual(node.func.attr, "get_r_rfree_sigma")
+                    if (
+                        isinstance(node.func.value, ast.Name)
+                        and node.func.value.id == "reflection_file"
+                    ):
+                        self.assertNotEqual(node.func.attr, "crystal_symmetry")
+                    if (
+                        isinstance(node.func.value, ast.Name)
+                        and node.func.value.id == "extract_xtal_data"
+                        and node.func.attr == "run"
+                    ):
+                        kw_names = {kw.arg for kw in node.keywords}
+                        self.assertNotIn("log", kw_names)
+                    if (
+                        isinstance(node.func.value, ast.Attribute)
+                        and node.func.value.attr == "f_model"
+                        and node.func.attr == "manager"
+                    ):
+                        kw_names = {kw.arg for kw in node.keywords}
+                        self.assertNotIn("log", kw_names)
+                    if node.func.attr == "map_coefficients":
+                        kw_names = {kw.arg for kw in node.keywords}
+                        self.assertIn("sharp", kw_names)
+                        self.assertNotIn("resharp_after_isotropize", kw_names)
+                elif isinstance(node.func, ast.Name) and node.func.id == "zip":
+                    kw_names = {kw.arg for kw in node.keywords}
+                    self.assertNotIn("strict", kw_names)
+
+    def test_operation_rejects_protstruct_env_overrides(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            fake_repo = Path(tmp).resolve(strict=True)
+            with mock.patch.dict(
+                os.environ, {"PROTSTRUCT_PHENIX_BIN": "/custom/phenix_bin"}
+            ):
+                with self.assertRaisesRegex(
+                    ValueError, "no PROTSTRUCT_\\* environment overrides"
+                ):
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        fake_repo / "out", subject_id="R4", repo_root=fake_repo
+                    )
+
+    def test_stage_subject_inputs_and_unmeasured_build_rejection(self) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            fake_repo = Path(tmp).resolve(strict=True)
+            staged = onesar_rerun.stage_subject_inputs(
+                fake_repo / "runs" / "r4_stage",
+                subject_id="R4",
+                repo_root=fake_repo,
+                archive_path=COMMITTED_ARCHIVE,
+            )
+            self.assertTrue(staged["original_pdb_path"].is_file())
+            self.assertTrue(staged["original_mtz_path"].is_file())
+            self.assertTrue(staged["projected_mtz_path"].is_file())
+            self.assertTrue(staged["input_ledger_path"].is_file())
+            self.assertTrue(staged["admission_json_path"].is_file())
+            self.assertEqual(
+                staged["admission_record"]["subject"]["sha256"],
+                onesar_rerun.ALLOWED_SUBJECTS["R4"]["sha256"],
+            )
+            self.assertEqual(
+                staged["input_ledger"]["counts"]["eligible_total"], 7228
+            )
+            persisted_ledger = json.loads(
+                staged["input_ledger_path"].read_text(encoding="utf-8")
+            )
+            self.assertEqual(
+                persisted_ledger["counts"], staged["input_ledger"]["counts"]
+            )
+            self.assertEqual(
+                len(persisted_ledger["eligible_by_hkl"]), 7228
+            )
+            self.assertEqual(len(persisted_ledger["ineligible_rows"]), 20)
+            self.assertEqual(len(persisted_ledger["raw_rows"]), 7248)
+
+            # Rejects unmeasured PHENIX build dictionary before invoking subprocess (#1014).
+            unmeasured_build = {
+                "reported_version_source": "command_output",
+                "reported_version": "2.0-5936",
+                "parsed_build": "2.0-5936",
+                "version_probe": {"probe_mode": "command_output"},
+            }
+            unmeasured_runner_calls: list[list[str]] = []
+
+            def _unexpected_runner(
+                argv: list[str], log: Path, *, cwd: Path, timeout: float
+            ) -> subprocess.CompletedProcess[str]:
+                unmeasured_runner_calls.append(list(argv))
+                raise AssertionError("Scientific worker must not be launched")
+
+            unmeasured_run_dir = fake_repo / "runs" / "r4_unmeasured"
+            with mock.patch.object(
+                toolchain, "phenix_build_evidence", return_value=unmeasured_build
+            ):
+                with self.assertRaisesRegex(
+                    ValueError, "failed #957 admission gate"
+                ):
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        unmeasured_run_dir,
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=_unexpected_runner,
+                    )
+            self.assertEqual(unmeasured_runner_calls, [])
+            self.assertFalse((unmeasured_run_dir / "phenix" / "cache").exists())
+            preflight_unmeasured_path = (
+                unmeasured_run_dir
+                / "phenix"
+                / "phenix_preflight_build_evidence.json"
+            )
+            failure_unmeasured_path = (
+                unmeasured_run_dir
+                / "phenix"
+                / "phenix_build_admission_failure.json"
+            )
+            self.assertTrue(preflight_unmeasured_path.is_file())
+            self.assertTrue(failure_unmeasured_path.is_file())
+            self.assertEqual(
+                json.loads(preflight_unmeasured_path.read_text(encoding="utf-8")),
+                unmeasured_build,
+            )
+            failure_unmeasured_doc = json.loads(
+                failure_unmeasured_path.read_text(encoding="utf-8")
+            )
+            self.assertEqual(failure_unmeasured_doc["status"], "rejected")
+            self.assertFalse(failure_unmeasured_doc["worker_launched"])
+            self.assertEqual(
+                failure_unmeasured_doc["measured_build"], unmeasured_build
+            )
+            self.assertEqual(
+                failure_unmeasured_doc["failure"]["failed_stage"],
+                "validate_measured_phenix_build",
+            )
+            self.assertEqual(
+                failure_unmeasured_doc["preflight_build_evidence_sha256"],
+                hashlib.sha256(preflight_unmeasured_path.read_bytes()).hexdigest(),
+            )
+
+            # Rejects failed version_probe and retains full raw probe evidence on disk (#1014).
+            failed_probe_build = {
+                "reported_version_source": "unknown",
+                "reported_version": "unknown",
+                "parsed_build": None,
+                "version_probe": {
+                    "error": "phenix_env.sh exited with returncode 1",
+                    "probe_mode": "vendor_setup_environment",
+                    "setup_returncode": 1,
+                    "setup_stdout": "",
+                    "setup_stderr": "phenix_env.sh: missing dispatcher",
+                },
+            }
+            failed_probe_run_dir = fake_repo / "runs" / "r4_failed_probe"
+            with mock.patch.object(
+                toolchain,
+                "phenix_build_evidence",
+                return_value=failed_probe_build,
+            ):
+                with self.assertRaisesRegex(
+                    ValueError, "failed #957 admission gate"
+                ):
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        failed_probe_run_dir,
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=_unexpected_runner,
+                    )
+            self.assertEqual(unmeasured_runner_calls, [])
+            self.assertFalse(
+                (failed_probe_run_dir / "phenix" / "cache").exists()
+            )
+            preflight_failed_path = (
+                failed_probe_run_dir
+                / "phenix"
+                / "phenix_preflight_build_evidence.json"
+            )
+            admission_failed_path = (
+                failed_probe_run_dir
+                / "phenix"
+                / "phenix_build_admission_failure.json"
+            )
+            self.assertTrue(preflight_failed_path.is_file())
+            self.assertTrue(admission_failed_path.is_file())
+            self.assertEqual(
+                json.loads(preflight_failed_path.read_text(encoding="utf-8")),
+                failed_probe_build,
+            )
+            admission_failed_doc = json.loads(
+                admission_failed_path.read_text(encoding="utf-8")
+            )
+            self.assertEqual(admission_failed_doc["status"], "rejected")
+            self.assertFalse(admission_failed_doc["worker_launched"])
+            self.assertEqual(
+                admission_failed_doc["measured_build"], failed_probe_build
+            )
+            self.assertEqual(
+                admission_failed_doc["failure"]["failed_stage"],
+                "validate_measured_phenix_build",
+            )
+
+    def test_orchestration_with_real_cache_success_and_failure_retention(
+        self,
+    ) -> None:
+        with tempfile.TemporaryDirectory() as tmp:
+            tmp_root = Path(tmp).resolve(strict=True)
+            fake_repo = tmp_root / "repo"
+            fake_repo.mkdir()
+
+            # Build a synthetic PHENIX installation tree for file_identity and vendor source hashing.
+            fake_prefix = tmp_root / "phenix-2.0-5936"
+            fake_bin = fake_prefix / "phenix_bin"
+            fake_site = fake_prefix / "lib" / "python3.9" / "site-packages"
+            fake_bin.mkdir(parents=True)
+            (fake_prefix / "phenix_env.sh").write_text(
+                "#!/bin/sh\nexport PHENIX_VERSION=2.0\nexport PHENIX_RELEASE_TAG=5936\n",
+                encoding="utf-8",
+            )
+            (fake_bin / "phenix.version").write_text(
+                "#!/bin/sh\n", encoding="utf-8"
+            )
+            (fake_bin / "phenix.python").write_text(
+                "#!/bin/sh\n", encoding="utf-8"
+            )
+
+            vendor_modules_payload = {}
+            for key, rel in onesar_rerun.PHENIX_VENDOR_SOURCE_RELPATHS.items():
+                mod_file = fake_site / rel
+                mod_file.parent.mkdir(parents=True, exist_ok=True)
+                content = f"# synthetic {key}\n".encode("utf-8")
+                mod_file.write_bytes(content)
+                resolved_mod = mod_file.resolve(strict=True)
+                vendor_modules_payload[key] = {
+                    "module_name": key,
+                    "configured_path": str(resolved_mod),
+                    "resolved_path": str(resolved_mod),
+                    "sha256": hashlib.sha256(content).hexdigest(),
+                }
+
+            banner = "PHENIX: 2.0-5936\n  Version: 2.0\n  Release tag: 5936"
+            setup_resolved = (fake_prefix / "phenix_env.sh").resolve(strict=True)
+            disp_resolved = (fake_bin / "phenix.version").resolve(strict=True)
+            resolved_prefix_str = str(fake_prefix.resolve(strict=True))
+            fake_measured_build = {
+                "reported_version_source": "vendor_setup_environment",
+                "reported_version": banner,
+                "parsed_build": "2.0-5936",
+                "version_probe": {
+                    "error": None,
+                    "probe_mode": "vendor_setup_environment",
+                    "configured_prefix": str(fake_prefix),
+                    "resolved_prefix": resolved_prefix_str,
+                    "setup_configured_path": str(fake_prefix / "phenix_env.sh"),
+                    "setup_resolved_path": str(setup_resolved),
+                    "setup_sha256": hashlib.sha256(
+                        setup_resolved.read_bytes()
+                    ).hexdigest(),
+                    "setup_argv": toolchain.phenix_setup_argv(setup_resolved),
+                    "setup_returncode": 0,
+                    "setup_stdout": "",
+                    "setup_stderr": "",
+                    "setup_version_fields": {
+                        "PHENIX_VERSION": "2.0",
+                        "PHENIX_RELEASE_TAG": "5936",
+                    },
+                    "setup_parsed_build": "2.0-5936",
+                    "configured_path": str(fake_bin / "phenix.version"),
+                    "resolved_path": str(disp_resolved),
+                    "sha256": hashlib.sha256(
+                        disp_resolved.read_bytes()
+                    ).hexdigest(),
+                    "argv": [str(disp_resolved)],
+                    "returncode": 0,
+                    "stdout": banner + "\n",
+                    "stderr": "",
+                    "parsed_build": "2.0-5936",
+                    "ignored_inherited_version_variables": [],
+                    "ignored_inherited_shell_hooks": [],
+                },
+            }
+            self.assertTrue(
+                refinement_cache._build_is_measured(fake_measured_build)
+            )
+
+            # Internally consistent wrong version (2.0-5935) must be rejected (#1013, #1014)
+            # and retain preflight build evidence + admission failure without launching worker.
+            wrong_ver_banner = "PHENIX: 2.0-5935\n  Version: 2.0\n  Release tag: 5935"
+            wrong_ver_build = copy.deepcopy(fake_measured_build)
+            wrong_ver_build["reported_version"] = wrong_ver_banner
+            wrong_ver_build["parsed_build"] = "2.0-5935"
+            wrong_ver_build["version_probe"]["setup_version_fields"] = {
+                "PHENIX_VERSION": "2.0",
+                "PHENIX_RELEASE_TAG": "5935",
+            }
+            wrong_ver_build["version_probe"]["setup_parsed_build"] = "2.0-5935"
+            wrong_ver_build["version_probe"]["stdout"] = wrong_ver_banner + "\n"
+            wrong_ver_build["version_probe"]["parsed_build"] = "2.0-5935"
+            self.assertTrue(refinement_cache._build_is_measured(wrong_ver_build))
+            wrong_ver_runner_calls: list[list[str]] = []
+
+            def _wrong_ver_spy_runner(
+                argv: list[str], log: Path, *, cwd: Path, timeout: float
+            ) -> subprocess.CompletedProcess[str]:
+                wrong_ver_runner_calls.append(list(argv))
+                raise AssertionError("Scientific worker must not be launched")
+
+            wrong_ver_run_dir = fake_repo / "runs" / "r4_wrong_ver"
+            with mock.patch.object(
+                toolchain, "phenix_build_evidence", return_value=wrong_ver_build
+            ):
+                with self.assertRaisesRegex(
+                    ValueError, "PHENIX build version mismatch: expected '2.0-5936'"
+                ):
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        wrong_ver_run_dir,
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=_wrong_ver_spy_runner,
+                    )
+            self.assertEqual(wrong_ver_runner_calls, [])
+            self.assertFalse((wrong_ver_run_dir / "phenix" / "cache").exists())
+            wrong_ver_preflight_path = (
+                wrong_ver_run_dir
+                / "phenix"
+                / "phenix_preflight_build_evidence.json"
+            )
+            wrong_ver_failure_path = (
+                wrong_ver_run_dir
+                / "phenix"
+                / "phenix_build_admission_failure.json"
+            )
+            self.assertTrue(wrong_ver_preflight_path.is_file())
+            self.assertTrue(wrong_ver_failure_path.is_file())
+            self.assertEqual(
+                json.loads(wrong_ver_preflight_path.read_text(encoding="utf-8")),
+                wrong_ver_build,
+            )
+            wrong_ver_failure_doc = json.loads(
+                wrong_ver_failure_path.read_text(encoding="utf-8")
+            )
+            self.assertEqual(wrong_ver_failure_doc["status"], "rejected")
+            self.assertFalse(wrong_ver_failure_doc["worker_launched"])
+            self.assertEqual(
+                wrong_ver_failure_doc["measured_build"], wrong_ver_build
+            )
+            self.assertEqual(
+                wrong_ver_failure_doc["failure"]["failed_stage"],
+                "validate_measured_phenix_build",
+            )
+
+            def fake_success_runner(
+                argv: list[str], log: Path, *, cwd: Path, timeout: float
+            ) -> subprocess.CompletedProcess[str]:
+                self.assertEqual(
+                    Path(argv[0]),
+                    (fake_bin / "phenix.python").resolve(strict=True),
+                )
+                mtz_arg = Path(argv[argv.index("--mtz") + 1])
+                out_json = Path(argv[argv.index("--out-json") + 1])
+                out_maps = Path(argv[argv.index("--out-maps-mtz") + 1])
+                out_obs_pair = Path(argv[argv.index("--out-obs-pair-mtz") + 1])
+                out_diag = Path(
+                    argv[argv.index("--out-diagnostics-json") + 1]
+                )
+                # Outputs must be placed inside the cache attempt directory (cwd).
+                self.assertEqual(out_json.parent, cwd)
+                self.assertEqual(out_maps.parent, cwd)
+                self.assertEqual(out_obs_pair.parent, cwd)
+                self.assertEqual(out_diag.parent, cwd)
+
+                ledger = onesar_rerun.build_input_hkl_ledger(
+                    mtz_arg.read_bytes()
+                )
+                work_candidates = [
+                    v
+                    for v in ledger["eligible_by_hkl"].values()
+                    if not v["is_free"] and v["f_obs"] > 0.0
+                ][:2]
+                free_candidates = [
+                    v
+                    for v in ledger["eligible_by_hkl"].values()
+                    if v["is_free"] and v["f_obs"] > 0.0
+                ][:2]
+                w0, w1 = work_candidates
+                f0, f1 = free_candidates
+
+                scale_k1_w = 0.5
+                scale_k1_t = 0.5
+                scale_k1 = 0.5
+                _, base_payload, _, _, _ = _make_reconciliation_fixture()
+                synth = copy.deepcopy(base_payload)
+                synth["expected_resolved_prefix"] = resolved_prefix_str
+                synth["expected_phenix_version"] = toolchain.PHENIX_VERSION
+                synth["runtime_environment"]["sys_prefix"] = str(fake_prefix)
+                synth["runtime_environment"]["resolved_sys_prefix"] = (
+                    resolved_prefix_str
+                )
+                synth["runtime_environment"]["sys_executable"] = str(
+                    (fake_bin / "phenix.python").resolve(strict=True)
+                )
+                synth["runtime_environment"]["resolved_sys_executable"] = str(
+                    (fake_bin / "phenix.python").resolve(strict=True)
+                )
+                synth["runtime_environment"]["vendor_modules"] = (
+                    vendor_modules_payload
+                )
+                selected = [w0, w1, f0, f1]
+                synth["extracted_pre_fmodel"] = [
+                    {
+                        "hkl": list(item["hkl"]),
+                        "f_obs": float(item["f_obs"]),
+                        "sigf_obs": float(item["sigf_obs"]),
+                        "is_free": bool(item["is_free"]),
+                    }
+                    for item in selected
+                ]
+                synth["final_effective"] = copy.deepcopy(
+                    synth["extracted_pre_fmodel"]
+                )
+                synth["actual_work"] = [
+                    {
+                        "hkl": list(item["hkl"]),
+                        "f_obs": float(item["f_obs"]),
+                        "sigf_obs": float(item["sigf_obs"]),
+                        "f_model_real": float(item["f_obs"]) / scale_k1_w,
+                        "f_model_imag": 0.0,
+                        "f_model_amplitude": float(item["f_obs"]) / scale_k1_w,
+                        "f_model_scaled_with_k1_w_real": float(item["f_obs"]),
+                        "f_model_scaled_with_k1_w_imag": 0.0,
+                        "f_model_scaled_with_k1_w_amplitude": float(
+                            item["f_obs"]
+                        ),
+                    }
+                    for item in (w0, w1)
+                ]
+                synth["actual_free"] = [
+                    {
+                        "hkl": list(item["hkl"]),
+                        "f_obs": float(item["f_obs"]),
+                        "sigf_obs": float(item["sigf_obs"]),
+                        "f_model_real": float(item["f_obs"]) / scale_k1_t,
+                        "f_model_imag": 0.0,
+                        "f_model_amplitude": float(item["f_obs"]) / scale_k1_t,
+                        "f_model_scaled_with_k1_t_real": float(item["f_obs"]),
+                        "f_model_scaled_with_k1_t_imag": 0.0,
+                        "f_model_scaled_with_k1_t_amplitude": float(
+                            item["f_obs"]
+                        ),
+                    }
+                    for item in (f0, f1)
+                ]
+                w_den = float(w0["f_obs"] + w1["f_obs"])
+                f_den = float(f0["f_obs"] + f1["f_obs"])
+                synth["scales_and_rfactors"].update(
+                    {
+                        "r_work": 0.0,
+                        "r_free": 0.0,
+                        "r_all": 0.0,
+                        "r_free_r_work_gap": 0.0,
+                        "scale_k1_w": scale_k1_w,
+                        "scale_k1_t": scale_k1_t,
+                        "scale_k1": scale_k1,
+                        "work_numerator": 0.0,
+                        "work_denominator": w_den,
+                        "free_numerator": 0.0,
+                        "free_denominator": f_den,
+                        "all_numerator": 0.0,
+                        "all_denominator": w_den + f_den,
+                        "work_count": 2,
+                        "free_count": 2,
+                        "final_effective_count": 4,
+                        "extracted_pre_fmodel_count": 4,
+                    }
+                )
+                map_recs = [
+                    {
+                        "hkl": list(item["hkl"]),
+                        "real": 1.0,
+                        "imag": 0.0,
+                        "amplitude": 1.0,
+                        "phase_deg": 0.0,
+                        "is_centric": True,
+                    }
+                    for item in selected
+                ]
+                synth["map_coefficients"]["2mFo-DFc"]["records"] = copy.deepcopy(
+                    map_recs
+                )
+                synth["map_coefficients"]["2mFo-DFc"]["centric_count"] = 4
+                synth["map_coefficients"]["2mFo-DFc"]["acentric_count"] = 0
+                synth["map_coefficients"]["mFo-DFc"]["records"] = copy.deepcopy(
+                    map_recs
+                )
+                synth["map_coefficients"]["mFo-DFc"]["centric_count"] = 4
+                synth["map_coefficients"]["mFo-DFc"]["acentric_count"] = 0
+
+                cell_floats = tuple(
+                    float(x)
+                    for x in ledger["crystallographic_context"][
+                        "cell_a_b_c_alpha_beta_gamma"
+                    ]
+                )
+                runtime_cell_floats = onesar_rerun.ccp4_mtz_float32_unit_cell(
+                    ledger["crystallographic_context"][
+                        "cell_a_b_c_alpha_beta_gamma"
+                    ]
+                )
+                synth["crystal_symmetry"] = {
+                    "unit_cell": list(runtime_cell_floats),
+                    "space_group_symbol": "P 21 21 21 (No. 19)",
+                    "space_group_number": 19,
+                    "space_group_operators_xyz": [
+                        "x,y,z",
+                        "-x+1/2,-y,z+1/2",
+                        "-x,y+1/2,-z+1/2",
+                        "x+1/2,-y+1/2,-z",
+                    ],
+                }
+                ca, cb, cc = (
+                    runtime_cell_floats[0],
+                    runtime_cell_floats[1],
+                    runtime_cell_floats[2],
+                )
+
+                def _orth_range(
+                    items: list[dict[str, Any]],
+                ) -> dict[str, float]:
+                    ds = [
+                        1.0
+                        / math.sqrt(
+                            (it["hkl"][0] / ca) ** 2
+                            + (it["hkl"][1] / cb) ** 2
+                            + (it["hkl"][2] / cc) ** 2
+                        )
+                        for it in items
+                    ]
+                    return {"d_max": max(ds), "d_min": min(ds)}
+
+                synth["resolution_ranges"] = {
+                    "extracted_pre_fmodel": _orth_range(selected),
+                    "final_effective": _orth_range(selected),
+                    "actual_work": _orth_range([w0, w1]),
+                    "actual_free": _orth_range([f0, f1]),
+                }
+                m_bytes, p_bytes = _build_synthetic_map_mtz_pair(
+                    map_recs, map_recs, cell=cell_floats  # type: ignore[arg-type]
+                )
+                synth["map_coefficients"]["mtz_supplements"][
+                    "phx_maps_sha256"
+                ] = hashlib.sha256(m_bytes).hexdigest()
+                synth["map_coefficients"]["mtz_supplements"][
+                    "obs_pair_only_sha256"
+                ] = hashlib.sha256(p_bytes).hexdigest()
+
+                diag_doc = {
+                    "schema_version": 1,
+                    "status": "succeeded",
+                    "extract_xtal_data_log": synth["diagnostics"][
+                        "extract_xtal_data_log"
+                    ],
+                    "model_manager_log": synth["diagnostics"][
+                        "model_manager_log"
+                    ],
+                    "fmodel_update_all_scales_log": synth["diagnostics"][
+                        "fmodel_update_all_scales_log"
+                    ],
+                    "failure": None,
+                }
+                log.write_text("synthetic phenix.python stdout\n", encoding="utf-8")
+                out_maps.write_bytes(m_bytes)
+                out_obs_pair.write_bytes(p_bytes)
+                out_diag.write_text(
+                    json.dumps(diag_doc, indent=2, sort_keys=True) + "\n",
+                    encoding="utf-8",
+                )
+                out_json.write_text(
+                    json.dumps(synth, indent=2, sort_keys=True) + "\n",
+                    encoding="utf-8",
+                )
+                return subprocess.CompletedProcess(argv, 0)
+
+            with (
+                mock.patch.object(
+                    toolchain,
+                    "phenix",
+                    side_effect=lambda exe: fake_bin / exe,
+                ),
+                mock.patch.object(
+                    toolchain,
+                    "phenix_build_evidence",
+                    return_value=fake_measured_build,
+                ),
+            ):
+                reconciled = onesar_rerun.run_phenix_rfactor_operation(
+                    fake_repo / "runs" / "r4_success",
+                    subject_id="R4",
+                    repo_root=fake_repo,
+                    archive_path=COMMITTED_ARCHIVE,
+                    require_no_protstruct_overrides=False,
+                    runner=fake_success_runner,
+                )
+
+                self.assertEqual(
+                    reconciled["operation_manifest"]["returncode"], 0
+                )
+                self.assertTrue(reconciled["operation_manifest"]["reusable"])
+                manifest_path = Path(reconciled["manifest_path"])
+                self.assertTrue(manifest_path.is_file())
+                self.assertEqual(manifest_path.name, "success.json")
+                self.assertEqual(manifest_path.parent.name, "bundle")
+                self.assertTrue(
+                    Path(reconciled["phenix_rfactors_json_path"]).is_file()
+                )
+                self.assertTrue(
+                    Path(reconciled["phenix_diagnostics_json_path"]).is_file()
+                )
+                self.assertTrue(
+                    Path(reconciled["input_hkl_ledger_json_path"]).is_file()
+                )
+                self.assertTrue(Path(reconciled["phx_maps_mtz_path"]).is_file())
+                self.assertTrue(
+                    Path(reconciled["phx_obs_pair_only_mtz_path"]).is_file()
+                )
+                self.assertTrue(
+                    Path(
+                        reconciled["preflight_build_evidence_json_path"]
+                    ).is_file()
+                )
+                self.assertEqual(
+                    json.loads(
+                        Path(
+                            reconciled["preflight_build_evidence_json_path"]
+                        ).read_text(encoding="utf-8")
+                    ),
+                    fake_measured_build,
+                )
+                self.assertFalse(
+                    (
+                        fake_repo
+                        / "runs"
+                        / "r4_success"
+                        / "phenix"
+                        / "phenix_build_admission_failure.json"
+                    ).exists()
+                )
+                self.assertEqual(
+                    reconciled["reconciliation"]["reconciliation_status"],
+                    "verified",
+                )
+
+                # Failure retention test: non-zero exit raises OperationFailed and retains log
+                # as well as parent failure manifest with paths/hashes of partial/diagnostic artifacts (#1014).
+                def fake_failing_runner(
+                    argv: list[str], log: Path, *, cwd: Path, timeout: float
+                ) -> subprocess.CompletedProcess[str]:
+                    out_diag = Path(
+                        argv[argv.index("--out-diagnostics-json") + 1]
+                    )
+                    out_partial = Path(
+                        argv[argv.index("--out-partial-json") + 1]
+                    )
+                    self.assertEqual(out_diag.parent, cwd)
+                    self.assertEqual(out_partial.parent, cwd)
+                    log.write_text(
+                        "simulated extraction failure\n", encoding="utf-8"
+                    )
+                    onesar_phenix_measure._atomic_write_json_checkpoint(
+                        {
+                            "schema_version": 1,
+                            "status": "failed",
+                            "current_phase": "rfactor_replay",
+                            "completed_raw_stages": [
+                                "extract_xtal_data",
+                                "xray_structure_invariance",
+                                "rfactor_raw_arrays_and_scalars",
+                            ],
+                            "partial_evidence_status": "failed_partial_unverified",
+                            "failure": {
+                                "failed_phase": "rfactor_replay",
+                                "exception_type": "RuntimeError",
+                                "exception_message": "simulated rfactor mismatch",
+                            },
+                        },
+                        out_diag,
+                    )
+                    onesar_phenix_measure._atomic_write_json_checkpoint(
+                        {
+                            "schema_version": 1,
+                            "artifact_role": "partial_unverified_phenix_evidence",
+                            "reusable_cache_output": False,
+                            "canonical_eval_qds_eligible": False,
+                            "status": "failed_partial_unverified",
+                            "current_phase": "rfactor_replay",
+                            "completed_stages": [
+                                "extract_xtal_data",
+                                "xray_structure_invariance",
+                                "rfactor_raw_arrays_and_scalars",
+                            ],
+                            "rfactor_replay_verified": False,
+                            "map_export_verified": False,
+                            "failure": {
+                                "failed_phase": "rfactor_replay",
+                                "exception_type": "RuntimeError",
+                                "exception_message": "simulated rfactor mismatch",
+                            },
+                        },
+                        out_partial,
+                    )
+                    return subprocess.CompletedProcess(argv, 2)
+
+                r4_failure_dir = fake_repo / "runs" / "r4_failure"
+                with self.assertRaises(refinement_cache.OperationFailed) as ctx:
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        r4_failure_dir,
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=fake_failing_runner,
+                    )
+                self.assertTrue(ctx.exception.log_path.is_file())
+                self.assertIn(
+                    "simulated extraction failure",
+                    ctx.exception.log_path.read_text(encoding="utf-8"),
+                )
+                parent_failure_path = (
+                    r4_failure_dir / "phenix" / "phenix_operation_failure.json"
+                )
+                self.assertTrue(parent_failure_path.is_file())
+                parent_failure_doc = json.loads(
+                    parent_failure_path.read_text(encoding="utf-8")
+                )
+                self.assertEqual(parent_failure_doc["status"], "failed")
+                self.assertFalse(parent_failure_doc["reusable_cache_output"])
+                self.assertFalse(
+                    parent_failure_doc["canonical_eval_qds_eligible"]
+                )
+                self.assertIn(
+                    "phenix_partial_evidence_json",
+                    parent_failure_doc["retained_attempt_artifacts"],
+                )
+                self.assertIn(
+                    "phenix_diagnostics_json",
+                    parent_failure_doc["retained_attempt_artifacts"],
+                )
+                retained_partial_entry = parent_failure_doc[
+                    "retained_attempt_artifacts"
+                ]["phenix_partial_evidence_json"]
+                self.assertEqual(
+                    retained_partial_entry["sha256"],
+                    hashlib.sha256(
+                        Path(retained_partial_entry["path"]).read_bytes()
+                    ).hexdigest(),
+                )
+                self.assertEqual(
+                    parent_failure_doc["partial_evidence_summary"][
+                        "completed_stages"
+                    ],
+                    [
+                        "extract_xtal_data",
+                        "xray_structure_invariance",
+                        "rfactor_raw_arrays_and_scalars",
+                    ],
+                )
+                self.assertFalse(
+                    (
+                        r4_failure_dir
+                        / "phenix"
+                        / "phenix_reconciled_manifest.json"
+                    ).exists()
+                )
+
+            # Drift between preflight phenix_build_evidence() and cached_phenix_operation's
+            # internal phenix_build_evidence() call is rejected (#1013).
+            drifted_build = copy.deepcopy(fake_measured_build)
+            drifted_build["version_probe"]["setup_sha256"] = "b" * 64
+            with (
+                mock.patch.object(
+                    toolchain,
+                    "phenix",
+                    side_effect=lambda exe: fake_bin / exe,
+                ),
+                mock.patch.object(
+                    toolchain,
+                    "phenix_build_evidence",
+                    side_effect=[fake_measured_build, drifted_build],
+                ),
+            ):
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Cache manifest measured_build drifted from preflight",
+                ):
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        fake_repo / "runs" / "r4_drift",
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=fake_success_runner,
+                    )
+
+            # Hermetic local-Python subprocess timeout test (#1014):
+            # Invoke the actual onesar_phenix_measure.py worker in a child subprocess via
+            # cached_phenix_operation + toolchain.run_logged (runner=None) with a bounded
+            # timeout while fmodel.update_all_scales writes through _PersistentTeeLogStream
+            # and then blocks past the timeout (so finally: and except: never run).
+            timeout_prefix = tmp_root / "phenix-timeout-2.0-5936"
+            timeout_bin = timeout_prefix / "phenix_bin"
+            timeout_site = (
+                timeout_prefix / "lib" / "python3.9" / "site-packages"
+            )
+            timeout_bin.mkdir(parents=True)
+            (timeout_prefix / "phenix_env.sh").write_text(
+                "#!/bin/sh\nexport PHENIX_VERSION=2.0\nexport PHENIX_RELEASE_TAG=5936\n",
+                encoding="utf-8",
+            )
+            (timeout_bin / "phenix.version").write_text(
+                "#!/bin/sh\n", encoding="utf-8"
+            )
+            for pkg_rel in (
+                "cctbx/__init__.py",
+                "iotbx/__init__.py",
+                "iotbx/mtz/__init__.py",
+                "mmtbx/__init__.py",
+                "mmtbx/f_model/__init__.py",
+            ):
+                pkg_init = timeout_site / pkg_rel
+                pkg_init.parent.mkdir(parents=True, exist_ok=True)
+                pkg_init.write_text(
+                    "from .f_model import manager\n"
+                    if pkg_rel == "mmtbx/f_model/__init__.py"
+                    else "# synthetic package\n",
+                    encoding="utf-8",
+                )
+
+            (timeout_site / "cctbx" / "crystal.py").write_text(
+                "import struct, types\n"
+                "_F32_CELL = struct.unpack('<6f', struct.pack('<6f', 64.897, 78.323, 38.792, 90.0, 90.0, 90.0))\n"
+                "_OPS = [types.SimpleNamespace(as_xyz=lambda s=op: s) for op in ('x,y,z', '-x+1/2,-y,z+1/2', '-x,y+1/2,-z+1/2', 'x+1/2,-y+1/2,-z')]\n"
+                "_SG = types.SimpleNamespace(type=lambda: types.SimpleNamespace(number=lambda: 19), all_ops=lambda: list(_OPS))\n"
+                "_SGI = types.SimpleNamespace(symbol_and_number=lambda: 'P 21 21 21 (No. 19)', type=lambda: types.SimpleNamespace(number=lambda: 19), group=lambda: _SG)\n"
+                "_SYM = types.SimpleNamespace(\n"
+                "    unit_cell=lambda: types.SimpleNamespace(parameters=lambda: _F32_CELL),\n"
+                "    space_group=lambda: _SG,\n"
+                "    space_group_info=lambda: _SGI,\n"
+                ")\n"
+                "def select_crystal_symmetry(**kwargs):\n"
+                "    return _SYM\n",
+                encoding="utf-8",
+            )
+            (timeout_site / "iotbx" / "pdb.py").write_text(
+                "from cctbx.crystal import _SYM\n"
+                "class _Inp:\n"
+                "    def crystal_symmetry(self):\n"
+                "        return _SYM\n"
+                "def input(file_name):\n"
+                "    return _Inp()\n",
+                encoding="utf-8",
+            )
+            (timeout_site / "iotbx" / "mtz" / "__init__.py").write_text(
+                "import types\n"
+                "def label_decorator(phases_prefix='PH'):\n"
+                "    return types.SimpleNamespace(phases_prefix=phases_prefix)\n",
+                encoding="utf-8",
+            )
+            (timeout_site / "iotbx" / "reflection_file_reader.py").write_text(
+                "import types\n"
+                "from cctbx.crystal import _SYM\n"
+                "def any_reflection_file(file_name):\n"
+                "    return types.SimpleNamespace(\n"
+                "        file_type=lambda: 'ccp4_mtz',\n"
+                "        as_miller_arrays=lambda: [types.SimpleNamespace(crystal_symmetry=lambda: _SYM)],\n"
+                "    )\n",
+                encoding="utf-8",
+            )
+            (timeout_site / "iotbx" / "reflection_file_utils.py").write_text(
+                "def reflection_file_server(**kwargs):\n    return 'RFS'\n",
+                encoding="utf-8",
+            )
+            (timeout_site / "iotbx" / "extract_xtal_data.py").write_text(
+                "import io, struct, types\n"
+                "_CA_F32 = struct.unpack(' subprocess.CompletedProcess[str]:
+                cwd_path = Path(kwargs["cwd"])
+                diag_file = cwd_path / "phenix_diagnostics.json"
+                proc = subprocess.Popen(
+                    cmd,
+                    cwd=kwargs.get("cwd"),
+                    stdout=kwargs.get("stdout"),
+                    stderr=kwargs.get("stderr"),
+                    text=kwargs.get("text", True),
+                    env=kwargs.get("env"),
+                )
+                try:
+                    deadline = time.monotonic() + 15.0
+                    while time.monotonic() < deadline:
+                        if diag_file.is_file():
+                            try:
+                                doc = json.loads(
+                                    diag_file.read_text(encoding="utf-8")
+                                )
+                                if (
+                                    doc.get("current_phase")
+                                    == "update_all_scales"
+                                    and "LIVE_SCALING_MARKER_BEFORE_TIMEOUT"
+                                    in (
+                                        doc.get("fmodel_update_all_scales_log")
+                                        or ""
+                                    )
+                                ):
+                                    break
+                            except (OSError, ValueError):
+                                pass
+                        if proc.poll() is not None:
+                            break
+                        time.sleep(0.01)
+                    proc.communicate(timeout=0.05)
+                except subprocess.TimeoutExpired:
+                    proc.kill()
+                    proc.wait()
+                    raise
+                return subprocess.CompletedProcess(cmd, proc.returncode or 0)
+
+            timeout_run_dir = fake_repo / "runs" / "r4_live_timeout"
+            with (
+                mock.patch.object(
+                    toolchain,
+                    "phenix",
+                    side_effect=lambda exe: timeout_bin / exe,
+                ),
+                mock.patch.object(
+                    toolchain,
+                    "phenix_build_evidence",
+                    return_value=timeout_measured_build,
+                ),
+                mock.patch.object(
+                    toolchain.subprocess,
+                    "run",
+                    side_effect=_readiness_aware_timeout_subprocess_run,
+                ),
+            ):
+                with self.assertRaises(refinement_cache.OperationFailed) as t_ctx:
+                    onesar_rerun.run_phenix_rfactor_operation(
+                        timeout_run_dir,
+                        subject_id="R4",
+                        repo_root=fake_repo,
+                        archive_path=COMMITTED_ARCHIVE,
+                        require_no_protstruct_overrides=False,
+                        runner=None,
+                        timeout=2.5,
+                    )
+
+            self.assertTrue(t_ctx.exception.log_path.is_file())
+            self.assertIn(
+                "PHENIX operation timed out; retained log:",
+                str(t_ctx.exception),
+            )
+            self.assertIsInstance(
+                t_ctx.exception.__cause__, subprocess.TimeoutExpired
+            )
+            stdout_log_text = t_ctx.exception.log_path.read_text(
+                encoding="utf-8"
+            )
+            self.assertIn(
+                "=== [onesar_phenix_measure] mmtbx.model.manager ===",
+                stdout_log_text,
+            )
+            self.assertIn(
+                "LIVE_MODEL_MANAGER_MARKER_BEFORE_SCALES",
+                stdout_log_text,
+            )
+            self.assertIn(
+                "=== [onesar_phenix_measure] fmodel.update_all_scales ===",
+                stdout_log_text,
+            )
+            self.assertIn(
+                "LIVE_SCALING_MARKER_BEFORE_TIMEOUT",
+                stdout_log_text,
+            )
+
+            timeout_diag_path = (
+                t_ctx.exception.log_path.parent / "phenix_diagnostics.json"
+            )
+            self.assertTrue(timeout_diag_path.is_file())
+            timeout_diag_doc = json.loads(
+                timeout_diag_path.read_text(encoding="utf-8")
+            )
+            self.assertEqual(timeout_diag_doc["status"], "running")
+            self.assertEqual(
+                timeout_diag_doc["current_phase"], "update_all_scales"
+            )
+            self.assertEqual(
+                timeout_diag_doc["phase_history"],
+                [
+                    "parameter_validation",
+                    "vendor_imports",
+                    "runtime_environment_inspection",
+                    "pdb_input",
+                    "reflection_file_input",
+                    "crystal_symmetry_consensus",
+                    "extract_xtal_data",
+                    "model_manager",
+                    "fmodel_manager_init",
+                    "update_all_scales",
+                ],
+            )
+            self.assertEqual(
+                timeout_diag_doc["extract_xtal_data_log"],
+                "LIVE_EXTRACT_XTAL_DATA_MARKER\n",
+            )
+            self.assertEqual(
+                timeout_diag_doc["model_manager_log"],
+                "LIVE_MODEL_MANAGER_MARKER_BEFORE_SCALES\n",
+            )
+            self.assertEqual(
+                timeout_diag_doc["fmodel_update_all_scales_log"],
+                "LIVE_SCALING_MARKER_BEFORE_TIMEOUT\n",
+            )
+            self.assertIsNone(timeout_diag_doc["failure"])
+
+            timeout_partial_path = (
+                t_ctx.exception.log_path.parent
+                / "phenix_rfactors_partial_evidence.json"
+            )
+            self.assertTrue(timeout_partial_path.is_file())
+            timeout_partial_doc = json.loads(
+                timeout_partial_path.read_text(encoding="utf-8")
+            )
+            self.assertEqual(
+                timeout_partial_doc["status"], "running_partial_unverified"
+            )
+            self.assertEqual(
+                timeout_partial_doc["completed_stages"],
+                ["extract_xtal_data", "xray_structure_snapshot_before"],
+            )
+            self.assertEqual(
+                len(timeout_partial_doc["extracted_pre_fmodel"]), 2
+            )
+            self.assertEqual(
+                timeout_partial_doc["xray_structure_snapshots_raw"]["before"][
+                    "n_scatterers"
+                ],
+                1,
+            )
+            self.assertIsNone(
+                timeout_partial_doc["xray_structure_snapshots_raw"][
+                    "after_model"
+                ]
+            )
+            self.assertIsNone(
+                timeout_partial_doc["xray_structure_snapshots_raw"][
+                    "after_fmodel"
+                ]
+            )
+            self.assertFalse(
+                timeout_partial_doc["xray_structure_snapshots_raw"][
+                    "invariance_verified"
+                ]
+            )
+            self.assertIsNone(timeout_partial_doc["xray_structure_invariance"])
+            timeout_parent_fail = (
+                timeout_run_dir / "phenix" / "phenix_operation_failure.json"
+            )
+            self.assertTrue(timeout_parent_fail.is_file())
+            timeout_parent_doc = json.loads(
+                timeout_parent_fail.read_text(encoding="utf-8")
+            )
+            self.assertIn(
+                "phenix_partial_evidence_json",
+                timeout_parent_doc["retained_attempt_artifacts"],
+            )
+            self.assertIn(
+                "phenix_diagnostics_json",
+                timeout_parent_doc["retained_attempt_artifacts"],
+            )
+
+    def test_in_process_measure_first_block_rfactors_with_source_backed_doubles_1017(
+        self,
+    ) -> None:
+        """Directly execute measure_first_block_rfactors in-process with narrow source-backed doubles."""
+
+        class _FakeArray(list):
+            def size(self) -> int:
+                return len(self)
+
+            def all_eq(self, other: Any) -> bool:
+                return list(self) == list(other)
+
+        class _FakeMillerArray:
+            def __init__(
+                self,
+                indices: list[tuple[int, int, int]],
+                data: list[Any],
+                sigmas: list[float] | None = None,
+                centric_flags: list[bool] | None = None,
+            ) -> None:
+                self._indices = _FakeArray(indices)
+                self._data = _FakeArray(data)
+                self._sigmas = (
+                    _FakeArray(sigmas) if sigmas is not None else None
+                )
+                self._centric = (
+                    _FakeArray(centric_flags)
+                    if centric_flags is not None
+                    else _FakeArray([idx[1] == 0 for idx in indices])
+                )
+
+            def indices(self) -> _FakeArray:
+                return self._indices
+
+            def data(self) -> _FakeArray:
+                return self._data
+
+            def sigmas(self) -> _FakeArray | None:
+                return self._sigmas
+
+            def size(self) -> int:
+                return len(self._indices)
+
+            def _d_values(self) -> list[float]:
+                f32_cell = onesar_rerun.ccp4_mtz_float32_unit_cell(
+                    (64.897, 78.323, 38.792, 90.0, 90.0, 90.0)
+                )
+                return [
+                    onesar_rerun.compute_hkl_d_spacing((h, k, l), f32_cell)
+                    for h, k, l in self._indices
+                ]
+
+            def d_min(self) -> float:
+                return min(self._d_values())
+
+            def d_max_min(self) -> tuple[float, float]:
+                d_vals = self._d_values()
+                return (max(d_vals), min(d_vals))
+
+            def info(self) -> str:
+                return "synthetic_miller_array"
+
+            def centric_flags(self) -> Any:
+                parent = self
+
+                class _Flags:
+                    def data(self_inner) -> _FakeArray:
+                        return parent._centric
+
+                return _Flags()
+
+            def as_mtz_dataset(
+                self, column_root_label: str, label_decorator: Any = None
+            ) -> Any:
+                return _FakeMtzDataset(self, column_root_label)
+
+        class _FakeMtzDataset:
+            fail_in_map_write: bool = False
+
+            def __init__(self, first_array: _FakeMillerArray, root_label: str) -> None:
+                self.arrays: list[tuple[_FakeMillerArray, str]] = [
+                    (first_array, root_label)
+                ]
+
+            def add_miller_array(
+                self,
+                miller_array: _FakeMillerArray,
+                column_root_label: str,
+                label_decorator: Any = None,
+            ) -> None:
+                self.arrays.append((miller_array, column_root_label))
+
+            def mtz_object(self) -> Any:
+                parent = self
+
+                class _Writer:
+                    def column_labels(self_inner) -> list[str]:
+                        cols = ["H", "K", "L"]
+                        for _, lbl in parent.arrays:
+                            cols.extend([lbl, f"PH{lbl}"])
+                        return cols
+
+                    def write(self_inner, file_name: str) -> None:
+                        if _FakeMtzDataset.fail_in_map_write:
+                            raise RuntimeError(
+                                "Injected synthetic map write failure"
+                            )
+                        recs_by_label: dict[str, list[dict[str, Any]]] = {}
+                        for arr, lbl in parent.arrays:
+                            recs: list[dict[str, Any]] = []
+                            for idx, cval, is_c in zip(
+                                arr.indices(),
+                                arr.data(),
+                                arr.centric_flags().data(),
+                            ):
+                                re_v = float(cval.real)
+                                im_v = float(cval.imag)
+                                amp_v = math.hypot(re_v, im_v)
+                                ph_v = (
+                                    0.0
+                                    if amp_v == 0.0
+                                    else math.degrees(math.atan2(im_v, re_v))
+                                )
+                                recs.append(
+                                    {
+                                        "hkl": list(idx),
+                                        "real": re_v,
+                                        "imag": im_v,
+                                        "amplitude": amp_v,
+                                        "phase_deg": ph_v,
+                                        "is_centric": bool(is_c),
+                                    }
+                                )
+                            recs_by_label[lbl] = recs
+                        if "FOFCWT" in recs_by_label:
+                            m_bytes, _ = _build_synthetic_map_mtz_pair(
+                                recs_by_label["2FOFCWT"],
+                                recs_by_label["FOFCWT"],
+                            )
+                            Path(file_name).write_bytes(m_bytes)
+                        else:
+                            _, p_bytes = _build_synthetic_map_mtz_pair(
+                                recs_by_label["2FOFCWT"],
+                                recs_by_label["2FOFCWT"],
+                            )
+                            Path(file_name).write_bytes(p_bytes)
+
+                return _Writer()
+
+        class _FakeMtzObjectReadback:
+            def __init__(self, file_name: str) -> None:
+                mtz_bytes = Path(file_name).read_bytes()
+                _, headers = onesar_rerun._mtz_headers(mtz_bytes)
+                ncol_row = [
+                    line.split() for line in headers if line.startswith("NCOL ")
+                ][0]
+                self._nrows = int(ncol_row[2])
+                col_rows = [
+                    line.split()
+                    for line in headers
+                    if line.startswith("COLUMN ")
+                ]
+                self._labels = [parts[1] for parts in col_rows]
+                self._types = [parts[2] for parts in col_rows]
+
+            def columns(self) -> list[Any]:
+                return [
+                    types.SimpleNamespace(
+                        label=lambda l=lbl: l,
+                        type=lambda t=tp: t,
+                    )
+                    for lbl, tp in zip(self._labels, self._types)
+                ]
+
+            def column_labels(self) -> list[str]:
+                return list(self._labels)
+
+            def column_types(self) -> list[str]:
+                return list(self._types)
+
+            def n_reflections(self) -> int:
+                return self._nrows
+
+        class _FakeScatterer:
+            def __init__(self) -> None:
+                self.label = "PDB=\" N   VAL A   1 \""
+                self.scattering_type = "N"
+                self.site = (0.1, 0.2, 0.3)
+                self.occupancy = 1.0
+                self.u_iso = 0.25
+                self.u_star = (0.0, 0.0, 0.0, 0.0, 0.0, 0.0)
+                self.flags = types.SimpleNamespace(
+                    use_u_iso=lambda: True,
+                    use_u_aniso=lambda: False,
+                )
+
+        class _FakeXrayStructure:
+            def scatterers(self) -> list[_FakeScatterer]:
+                return [_FakeScatterer()]
+
+            def sites_cart(self) -> list[tuple[float, float, float]]:
+                return [(6.4897, 15.6646, 11.6376)]
+
+            def scattering_type_registry(self) -> Any:
+                return types.SimpleNamespace(
+                    Unassigned=False,
+                    has_unknown_type_symbols=lambda: False,
+                )
+
+        hkls_5 = [(1, 0, 0), (0, 1, 0), (1, 1, 1), (2, 0, 0), (0, 2, 0)]
+        f_obs_5 = [100.0, 200.0, 150.0, 80.0, 120.0]
+        sigf_5 = [2.0, 3.0, 2.5, 1.8, 2.2]
+        flags_5 = [False, False, False, True, True]
+
+        hkls_4 = [(1, 0, 0), (0, 1, 0), (2, 0, 0), (0, 2, 0)]
+        f_obs_4 = [100.0, 200.0, 80.0, 120.0]
+        sigf_4 = [2.0, 3.0, 1.8, 2.2]
+        flags_4 = [False, False, True, True]
+
+        hkls_w = [(1, 0, 0), (0, 1, 0)]
+        f_obs_w = [100.0, 200.0]
+        sigf_w = [2.0, 3.0]
+        fm_w = [complex(108.0, 144.0), complex(252.0, 336.0)]
+        fm_scaled_w = [complex(54.0, 72.0), complex(126.0, 168.0)]
+
+        hkls_t = [(2, 0, 0), (0, 2, 0)]
+        f_obs_t = [80.0, 120.0]
+        sigf_t = [1.8, 2.2]
+        fm_t = [complex(60.0, 80.0), complex(150.0, 200.0)]
+        fm_scaled_t = [complex(36.0, 48.0), complex(90.0, 120.0)]
+
+        _fake_f32_cell = onesar_rerun.ccp4_mtz_float32_unit_cell(
+            (64.897, 78.323, 38.792, 90.0, 90.0, 90.0)
+        )
+        _fake_rt_mx_ops = [
+            types.SimpleNamespace(as_xyz=lambda s=op: s)
+            for op in (
+                "x,y,z",
+                "-x+1/2,-y,z+1/2",
+                "-x,y+1/2,-z+1/2",
+                "x+1/2,-y+1/2,-z",
+            )
+        ]
+        _fake_space_group = types.SimpleNamespace(
+            type=lambda: types.SimpleNamespace(number=lambda: 19),
+            all_ops=lambda: list(_fake_rt_mx_ops),
+        )
+        _fake_space_group_info = types.SimpleNamespace(
+            symbol_and_number=lambda: "P 21 21 21 (No. 19)",
+            type=lambda: types.SimpleNamespace(number=lambda: 19),
+            group=lambda: _fake_space_group,
+        )
+        _fake_symmetry = types.SimpleNamespace(
+            unit_cell=lambda: types.SimpleNamespace(
+                parameters=lambda: _fake_f32_cell
+            ),
+            space_group=lambda: _fake_space_group,
+            space_group_info=lambda: _fake_space_group_info,
+        )
+
+        def _make_vendor_modules(
+            *,
+            df_err: list[str] | None = None,
+            raise_in_extract: bool = False,
+            fail_in_fmodel: bool = False,
+            fail_in_map_write: bool = False,
+            fail_in_map_coefficients: bool = False,
+            fail_in_second_map_coefficients: bool = False,
+            mutate_scatterer_count_in_scales: bool = False,
+            mutate_scatterer_occupancy_in_scales: bool = False,
+            nonfinite_scatterer_occupancy_in_scales: float | None = None,
+            nonfinite_scatterer_u_iso_in_scales: float | None = None,
+            empty_scatterers_in_scales: bool = False,
+            symmetry_override: Any = None,
+            nonfinite_work_model_coeff: bool = False,
+            nonpositive_final_obs: bool = False,
+            fail_in_free_array: bool = False,
+            native_r_work_override: float | None = None,
+        ) -> dict[str, Any]:
+            _FakeMtzDataset.fail_in_map_write = fail_in_map_write
+            scales_completed = [False]
+            active_symmetry = (
+                symmetry_override
+                if symmetry_override is not None
+                else _fake_symmetry
+            )
+
+            class _ScopedXrayStructure(_FakeXrayStructure):
+                def scatterers(self) -> list[_FakeScatterer]:
+                    if scales_completed[0] and empty_scatterers_in_scales:
+                        return []
+                    if (
+                        scales_completed[0]
+                        and mutate_scatterer_count_in_scales
+                    ):
+                        return [_FakeScatterer(), _FakeScatterer()]
+                    if (
+                        scales_completed[0]
+                        and mutate_scatterer_occupancy_in_scales
+                    ):
+                        sc = _FakeScatterer()
+                        sc.occupancy = 0.5
+                        return [sc]
+                    if (
+                        scales_completed[0]
+                        and nonfinite_scatterer_occupancy_in_scales is not None
+                    ):
+                        sc = _FakeScatterer()
+                        sc.occupancy = nonfinite_scatterer_occupancy_in_scales
+                        return [sc]
+                    if (
+                        scales_completed[0]
+                        and nonfinite_scatterer_u_iso_in_scales is not None
+                    ):
+                        sc = _FakeScatterer()
+                        sc.u_iso = nonfinite_scatterer_u_iso_in_scales
+                        return [sc]
+                    return super().scatterers()
+
+                def sites_cart(self) -> list[tuple[float, float, float]]:
+                    if scales_completed[0] and empty_scatterers_in_scales:
+                        return []
+                    if (
+                        scales_completed[0]
+                        and mutate_scatterer_count_in_scales
+                    ):
+                        return [
+                            (6.4897, 15.6646, 11.6376),
+                            (6.4897, 15.6646, 11.6376),
+                        ]
+                    return super().sites_cart()
+
+            iotbx_mod = types.ModuleType("iotbx")
+            iotbx_pdb = types.ModuleType("iotbx.pdb")
+            iotbx_mtz = types.ModuleType("iotbx.mtz")
+            iotbx_rfr = types.ModuleType("iotbx.reflection_file_reader")
+            iotbx_rfu = types.ModuleType("iotbx.reflection_file_utils")
+            iotbx_exd = types.ModuleType("iotbx.extract_xtal_data")
+            mmtbx_mod = types.ModuleType("mmtbx")
+            mmtbx_fmodel = types.ModuleType("mmtbx.f_model")
+            mmtbx_fmodel_mod = types.ModuleType("mmtbx.f_model.f_model")
+            mmtbx_map_tools = types.ModuleType("mmtbx.map_tools")
+            mmtbx_model = types.ModuleType("mmtbx.model")
+            mmtbx_mvd = types.ModuleType("mmtbx.model_vs_data")
+            cctbx_mod = types.ModuleType("cctbx")
+            cctbx_crystal = types.ModuleType("cctbx.crystal")
+
+            class _PdbInp:
+                def crystal_symmetry(self) -> Any:
+                    return active_symmetry
+
+                def get_r_rfree_sigma(self, *args: Any, **kwargs: Any) -> Any:
+                    raise AssertionError(
+                        "Second-block get_r_rfree_sigma must never be called"
+                    )
+
+            iotbx_pdb.input = lambda file_name: _PdbInp()
+            iotbx_mtz.object = _FakeMtzObjectReadback
+            iotbx_mtz.label_decorator = (
+                lambda phases_prefix="PH": types.SimpleNamespace(
+                    phases_prefix=phases_prefix
+                )
+            )
+            iotbx_rfr.any_reflection_file = lambda file_name: types.SimpleNamespace(
+                file_type=lambda: "ccp4_mtz",
+                as_miller_arrays=lambda: [
+                    types.SimpleNamespace(
+                        crystal_symmetry=lambda: active_symmetry
+                    )
+                ],
+            )
+            iotbx_rfu.reflection_file_server = lambda **kwargs: "RFS"
+            mmtbx_mvd.reflection_file_server = lambda **kwargs: "RFS"
+            cctbx_crystal.select_crystal_symmetry = (
+                lambda **kwargs: active_symmetry
+            )
+
+            def _data_and_flags_master_params() -> Any:
+                return types.SimpleNamespace(
+                    extract=lambda: types.SimpleNamespace(
+                        file_name=None,
+                        labels=None,
+                        r_free_flags=types.SimpleNamespace(
+                            file_name=None,
+                            label=None,
+                            test_flag_value=None,
+                            generate=True,
+                            ignore_r_free_flags=True,
+                        ),
+                    )
+                )
+
+            def _extract_run(
+                reflection_file_server: Any,
+                parameters: Any = None,
+                keep_going: bool = False,
+            ) -> Any:
+                if raise_in_extract:
+                    raise RuntimeError(
+                        "extract_xtal_data constructor raised before returning"
+                    )
+                log_buf = io.StringIO()
+                log_buf.write("Detailed extract_xtal_data log output\n")
+                return types.SimpleNamespace(
+                    log=log_buf,
+                    err=list(df_err) if df_err is not None else [],
+                    f_obs=_FakeMillerArray(hkls_5, f_obs_5, sigf_5),
+                    r_free_flags=_FakeMillerArray(hkls_5, flags_5),
+                    raw_data=_FakeMillerArray(hkls_5, f_obs_5, sigf_5),
+                    raw_flags=_FakeMillerArray(hkls_5, [0, 0, 0, 1, 1]),
+                    test_flag_value=1,
+                )
+
+            iotbx_exd.data_and_flags_master_params = (
+                _data_and_flags_master_params
+            )
+            iotbx_exd.run = _extract_run
+
+            def _model_manager(
+                model_input: Any,
+                crystal_symmetry: Any,
+                log: Any,
+            ) -> Any:
+                log.write("model.manager initialization log\n")
+                xrs = _ScopedXrayStructure()
+                return types.SimpleNamespace(
+                    get_xray_structure=lambda: xrs,
+                    setup_scattering_dictionaries=lambda **kw: None,
+                )
+
+            mmtbx_model.manager = _model_manager
+
+            def _map_coefficients(
+                map_type: str,
+                acentrics_scale: float = 2.0,
+                centrics_pre_scale: float = 1.0,
+                exclude_free_r_reflections: bool = False,
+                fill_missing: bool = False,
+                isotropize: bool = True,
+                sharp: bool = False,
+            ) -> _FakeMillerArray:
+                if fail_in_map_coefficients:
+                    raise RuntimeError(
+                        "Injected synthetic map_coefficients failure"
+                    )
+                if fail_in_second_map_coefficients and map_type == "mFo-DFc":
+                    raise RuntimeError(
+                        "Injected synthetic second-map coefficient failure"
+                    )
+                if map_type == "2mFo-DFc":
+                    return _FakeMillerArray(
+                        hkls_4,
+                        [
+                            complex(10.0, 0.0),
+                            complex(-12.0, 0.0),
+                            complex(0.0, 8.0),
+                            complex(0.0, -6.0),
+                        ],
+                        centric_flags=[True, True, False, False],
+                    )
+                return _FakeMillerArray(
+                    hkls_4,
+                    [
+                        complex(2.0, 0.0),
+                        complex(-1.5, 0.0),
+                        complex(0.0, 1.25),
+                        complex(0.0, -0.75),
+                    ],
+                    centric_flags=[True, True, False, False],
+                )
+
+            def _fmodel_manager(
+                xray_structure: Any,
+                r_free_flags: Any,
+                f_obs: Any,
+                twin_law: Any = None,
+            ) -> Any:
+                if fail_in_fmodel:
+                    raise RuntimeError("Simulated f_model outlier failure")
+
+                def _update_all_scales(
+                    update_f_part1: bool = True,
+                    log: Any = None,
+                ) -> None:
+                    if log is not None:
+                        log.write("fmodel.update_all_scales log\n")
+                    scales_completed[0] = True
+
+                def _f_obs_free() -> _FakeMillerArray:
+                    if fail_in_free_array:
+                        raise RuntimeError(
+                            "Injected synthetic f_obs_free failure"
+                        )
+                    return _FakeMillerArray(hkls_t, f_obs_t, sigf_t)
+
+                effective_r_work = (
+                    native_r_work_override
+                    if native_r_work_override is not None
+                    else (20.0 / 300.0)
+                )
+                return types.SimpleNamespace(
+                    xray_structure=_ScopedXrayStructure(),
+                    update_all_scales=_update_all_scales,
+                    map_coefficients=_map_coefficients,
+                    f_obs=lambda: _FakeMillerArray(
+                        hkls_4,
+                        [0.0, *f_obs_4[1:]]
+                        if nonpositive_final_obs
+                        else f_obs_4,
+                        sigf_4,
+                    ),
+                    r_free_flags=lambda: _FakeMillerArray(hkls_4, flags_4),
+                    f_obs_work=lambda: _FakeMillerArray(
+                        hkls_w,
+                        [0.0, *f_obs_w[1:]]
+                        if nonpositive_final_obs
+                        else f_obs_w,
+                        sigf_w,
+                    ),
+                    f_obs_free=_f_obs_free,
+                    f_model_work=lambda: _FakeMillerArray(
+                        hkls_w,
+                        [complex(float("nan"), 144.0), *fm_w[1:]]
+                        if nonfinite_work_model_coeff
+                        else fm_w,
+                    ),
+                    f_model_free=lambda: _FakeMillerArray(hkls_t, fm_t),
+                    f_model_scaled_with_k1_w=lambda: _FakeMillerArray(
+                        hkls_w,
+                        [complex(float("nan"), 72.0), *fm_scaled_w[1:]]
+                        if nonfinite_work_model_coeff
+                        else fm_scaled_w,
+                    ),
+                    f_model_scaled_with_k1_t=lambda: _FakeMillerArray(
+                        hkls_t, fm_scaled_t
+                    ),
+                    r_work=lambda: effective_r_work,
+                    r_free=lambda: 50.0 / 200.0,
+                    r_all=lambda: 70.6 / 500.0,
+                    scale_k1_w=lambda: 0.5,
+                    scale_k1_t=lambda: 0.6,
+                    scale_k1=lambda: 0.54,
+                )
+
+            mmtbx_fmodel.manager = _fmodel_manager
+            mmtbx_fmodel.f_model = mmtbx_fmodel_mod
+            mmtbx_fmodel_mod.manager = _fmodel_manager
+
+            iotbx_mod.pdb = iotbx_pdb
+            iotbx_mod.mtz = iotbx_mtz
+            iotbx_mod.reflection_file_reader = iotbx_rfr
+            iotbx_mod.reflection_file_utils = iotbx_rfu
+            iotbx_mod.extract_xtal_data = iotbx_exd
+            mmtbx_mod.f_model = mmtbx_fmodel
+            mmtbx_mod.map_tools = mmtbx_map_tools
+            mmtbx_mod.model = mmtbx_model
+            mmtbx_mod.model_vs_data = mmtbx_mvd
+            cctbx_mod.crystal = cctbx_crystal
+
+            return {
+                "iotbx": iotbx_mod,
+                "iotbx.pdb": iotbx_pdb,
+                "iotbx.mtz": iotbx_mtz,
+                "iotbx.reflection_file_reader": iotbx_rfr,
+                "iotbx.reflection_file_utils": iotbx_rfu,
+                "iotbx.extract_xtal_data": iotbx_exd,
+                "mmtbx": mmtbx_mod,
+                "mmtbx.f_model": mmtbx_fmodel,
+                "mmtbx.f_model.f_model": mmtbx_fmodel_mod,
+                "mmtbx.map_tools": mmtbx_map_tools,
+                "mmtbx.model": mmtbx_model,
+                "mmtbx.model_vs_data": mmtbx_mvd,
+                "cctbx": cctbx_mod,
+                "cctbx.crystal": cctbx_crystal,
+            }
+
+        with tempfile.TemporaryDirectory() as tmp:
+            tmp_dir = Path(tmp).resolve(strict=True)
+            fake_prefix = tmp_dir / "opt_phenix"
+            fake_prefix.mkdir(parents=True, exist_ok=True)
+            resolved_fake_prefix = str(fake_prefix.resolve(strict=True))
+            pdb_path = tmp_dir / "model.pdb"
+            mtz_path = tmp_dir / "obs.mtz"
+            params_path = tmp_dir / "params.json"
+            out_json = tmp_dir / "rfactors.json"
+            out_maps = tmp_dir / "phx_maps.mtz"
+            out_pair = tmp_dir / "PHX_OBS_PAIR_ONLY.mtz"
+            out_diag = tmp_dir / "diagnostics.json"
+
+            pdb_path.write_text("HEADER TEST\nEND\n", encoding="utf-8")
+            ledger, fixture_payload, vendor_shas, _, _ = (
+                _make_reconciliation_fixture()
+            )
+            mtz_path.write_bytes(b"MTZ_DUMMY")
+            params = dict(onesar_phenix_measure.EXPECTED_PARAMETERS)
+            params["subject_id"] = "R4"
+            params["expected_resolved_prefix"] = resolved_fake_prefix
+            params["expected_phenix_version"] = toolchain.PHENIX_VERSION
+            params["map_coefficients"] = dict(
+                onesar_phenix_measure.EXPECTED_MAP_PARAMETERS
+            )
+            params_path.write_text(
+                json.dumps(params, indent=2) + "\n", encoding="utf-8"
+            )
+
+            fake_runtime_env = copy.deepcopy(
+                fixture_payload["runtime_environment"]
+            )
+            fake_runtime_env["sys_prefix"] = resolved_fake_prefix
+            fake_runtime_env["resolved_sys_prefix"] = resolved_fake_prefix
+            fake_runtime_env["sys_executable"] = (
+                f"{resolved_fake_prefix}/bin/python3.9"
+            )
+            fake_runtime_env["resolved_sys_executable"] = (
+                f"{resolved_fake_prefix}/bin/python3.9"
+            )
+            for mod_key, rel in onesar_rerun.PHENIX_VENDOR_SOURCE_RELPATHS.items():
+                mod_p = f"{resolved_fake_prefix}/lib/python3.9/site-packages/{rel}"
+                fake_runtime_env["vendor_modules"][mod_key]["configured_path"] = (
+                    mod_p
+                )
+                fake_runtime_env["vendor_modules"][mod_key]["resolved_path"] = (
+                    mod_p
+                )
+
+            # 1. Success path in-process:
+            with (
+                mock.patch.dict(sys.modules, _make_vendor_modules()),
+                mock.patch.object(
+                    onesar_phenix_measure,
+                    "_inspect_vendor_modules",
+                    return_value=fake_runtime_env,
+                ),
+            ):
+                result_payload = (
+                    onesar_phenix_measure.measure_first_block_rfactors(
+                        pdb_path=pdb_path,
+                        mtz_path=mtz_path,
+                        params_path=params_path,
+                        out_json_path=out_json,
+                        out_maps_mtz_path=out_maps,
+                        out_obs_pair_mtz_path=out_pair,
+                        out_diagnostics_json_path=out_diag,
+                    )
+                )
+
+            self.assertTrue(out_json.is_file())
+            self.assertTrue(out_maps.is_file())
+            self.assertTrue(out_pair.is_file())
+            self.assertTrue(out_diag.is_file())
+            self.assertEqual(
+                result_payload["diagnostics"]["current_phase"], "completed"
+            )
+            self.assertIn(
+                "update_all_scales",
+                result_payload["diagnostics"]["phase_history"],
+            )
+            reconciled = onesar_rerun.reconcile_phenix_rfactor_output(
+                result_payload,
+                ledger,
+                expected_vendor_source_sha256=vendor_shas,
+                expected_resolved_prefix=resolved_fake_prefix,
+                phx_maps_mtz_path=out_maps,
+                phx_obs_pair_only_mtz_path=out_pair,
+                phenix_diagnostics_path=out_diag,
+            )
+            self.assertEqual(reconciled["reconciliation_status"], "verified")
+
+            # 1b. Direct _PersistentTeeLogStream live-flush verification before finalize():
+            live_diag_path = tmp_dir / "live_tee_diag.json"
+            live_log_path = tmp_dir / "live_tee.log"
+            live_diag: dict[str, Any] = {
+                "status": "running",
+                "current_phase": "update_all_scales",
+                "fmodel_update_all_scales_log": "",
+            }
+            tee_stream = onesar_phenix_measure._PersistentTeeLogStream(
+                "fmodel.update_all_scales",
+                live_diag,
+                "fmodel_update_all_scales_log",
+                diagnostics_json_path=live_diag_path,
+                log_file_path=live_log_path,
+            )
+            tee_stream.write("chunk_one\n")
+            # Persisted on disk immediately on write() without calling finalize():
+            self.assertEqual(
+                live_log_path.read_text(encoding="utf-8"), "chunk_one\n"
+            )
+            self.assertEqual(
+                json.loads(live_diag_path.read_text(encoding="utf-8"))[
+                    "fmodel_update_all_scales_log"
+                ],
+                "chunk_one\n",
+            )
+            tee_stream.write("chunk_two")
+            self.assertEqual(
+                live_log_path.read_text(encoding="utf-8"),
+                "chunk_one\nchunk_two",
+            )
+            self.assertEqual(
+                json.loads(live_diag_path.read_text(encoding="utf-8"))[
+                    "fmodel_update_all_scales_log"
+                ],
+                "chunk_one\nchunk_two",
+            )
+
+            # 1c. Deterministic interrupted-checkpoint test (#1014 Follow-up 2, #1017):
+            # A failed/interrupted checkpoint write or promotion must preserve the prior
+            # complete JSON checkpoint on disk and unlink the temporary sibling.
+            with self.subTest(case="1c_interrupted_checkpoint"):
+                self._verify_atomic_checkpoint_interrupted_write_and_promotion(
+                    tmp_dir
+                )
+
+            # 2. Failure in extract_xtal_data retains df.log in diagnostics file (#1014, #1017):
+            with self.subTest(case="2_extract_xtal_data_err"):
+                out_diag_err = tmp_dir / "diagnostics_exd_err.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            df_err=["Bad R-free flags column"]
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError, "extract_xtal_data reported errors"
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=tmp_dir / "unused1.json",
+                            out_maps_mtz_path=tmp_dir / "unused1.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "unused1_pair.mtz",
+                            out_diagnostics_json_path=out_diag_err,
+                        )
+                self.assertTrue(out_diag_err.is_file())
+                diag_err_doc = json.loads(
+                    out_diag_err.read_text(encoding="utf-8")
+                )
+                self.assertEqual(diag_err_doc["status"], "failed")
+                self.assertEqual(
+                    diag_err_doc["current_phase"], "extract_xtal_data"
+                )
+                self.assertEqual(
+                    diag_err_doc["failure"]["failed_phase"],
+                    "extract_xtal_data",
+                )
+                self.assertIn(
+                    "Detailed extract_xtal_data log output",
+                    diag_err_doc["extract_xtal_data_log"],
+                )
+
+            # 2b. Exception inside extract_xtal_data.run before returning records
+            # constructor_log_limitation for the unreturned internal StringIO (#1014):
+            with self.subTest(case="2b_extract_xtal_data_raise"):
+                out_diag_exd_raise = tmp_dir / "diagnostics_exd_raise.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(raise_in_extract=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError,
+                        "extract_xtal_data constructor raised before returning",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=tmp_dir / "unused1b.json",
+                            out_maps_mtz_path=tmp_dir / "unused1b.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "unused1b_pair.mtz",
+                            out_diagnostics_json_path=out_diag_exd_raise,
+                        )
+                self.assertTrue(out_diag_exd_raise.is_file())
+                diag_exd_raise_doc = json.loads(
+                    out_diag_exd_raise.read_text(encoding="utf-8")
+                )
+                self.assertEqual(diag_exd_raise_doc["status"], "failed")
+                self.assertEqual(
+                    diag_exd_raise_doc["current_phase"], "extract_xtal_data"
+                )
+                self.assertIsNone(diag_exd_raise_doc["extract_xtal_data_log"])
+                self.assertIn(
+                    "internal self.log StringIO on the unreturned instance is not accessible",
+                    diag_exd_raise_doc["failure"]["constructor_log_limitation"],
+                )
+
+            # 3. Failure in mmtbx.f_model.manager records constructor_log_limitation (#1014):
+            with self.subTest(case="3_fmodel_manager_init_fail"):
+                out_diag_fm = tmp_dir / "diagnostics_fmodel_err.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(fail_in_fmodel=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError, "Simulated f_model outlier failure"
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=tmp_dir / "unused2.json",
+                            out_maps_mtz_path=tmp_dir / "unused2.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "unused2_pair.mtz",
+                            out_diagnostics_json_path=out_diag_fm,
+                        )
+                self.assertTrue(out_diag_fm.is_file())
+                diag_fm_doc = json.loads(
+                    out_diag_fm.read_text(encoding="utf-8")
+                )
+                self.assertEqual(diag_fm_doc["status"], "failed")
+                self.assertEqual(
+                    diag_fm_doc["failure"]["failed_phase"],
+                    "fmodel_manager_init",
+                )
+                self.assertIn(
+                    "accepts no log parameter",
+                    diag_fm_doc["failure"]["constructor_log_limitation"],
+                )
+                self.assertIn(
+                    "model.manager initialization log",
+                    diag_fm_doc["model_manager_log"],
+                )
+                partial_fm_doc = json.loads(
+                    (tmp_dir / "unused2_partial_evidence.json").read_text(
+                        encoding="utf-8"
+                    )
+                )
+                self.assertEqual(
+                    partial_fm_doc["xray_structure_snapshots_raw"]["before"],
+                    result_payload["xray_structure_invariance"]["snapshots"][
+                        "before"
+                    ],
+                )
+                self.assertIsNone(
+                    partial_fm_doc["xray_structure_snapshots_raw"][
+                        "after_model"
+                    ]
+                )
+                self.assertIsNone(
+                    partial_fm_doc["xray_structure_snapshots_raw"][
+                        "after_fmodel"
+                    ]
+                )
+                self.assertFalse(
+                    partial_fm_doc["xray_structure_snapshots_raw"][
+                        "invariance_verified"
+                    ]
+                )
+                self.assertIsNone(partial_fm_doc["xray_structure_invariance"])
+
+            # 3a1. Scatterer count change during scaling retains all three raw scatterer
+            # snapshots (counts 1, 2, 2) while leaving verified invariance None (#1014, Task 120):
+            with self.subTest(case="3a1_scatterer_count_change"):
+                out_json_scc = tmp_dir / "rfactors_scc.json"
+                out_partial_scc = (
+                    tmp_dir / "rfactors_scc_partial_evidence.json"
+                )
+                out_diag_scc = tmp_dir / "diagnostics_scc.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            mutate_scatterer_count_in_scales=True
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Scatterer count changed across f_model scaling",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_scc,
+                            out_maps_mtz_path=tmp_dir / "scc_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "scc_pair.mtz",
+                            out_diagnostics_json_path=out_diag_scc,
+                        )
+                self.assertFalse(out_json_scc.is_file())
+                self.assertTrue(out_partial_scc.is_file())
+                partial_scc_doc = json.loads(
+                    out_partial_scc.read_text(encoding="utf-8")
+                )
+                self.assertEqual(
+                    partial_scc_doc["status"], "failed_partial_unverified"
+                )
+                self.assertFalse(partial_scc_doc["reusable_cache_output"])
+                self.assertFalse(partial_scc_doc["canonical_eval_qds_eligible"])
+                self.assertFalse(partial_scc_doc["rfactor_replay_verified"])
+                self.assertFalse(partial_scc_doc["map_export_verified"])
+                self.assertIsNone(partial_scc_doc["xray_structure_invariance"])
+                raw_snaps_scc = partial_scc_doc["xray_structure_snapshots_raw"]
+                self.assertFalse(raw_snaps_scc["invariance_verified"])
+                self.assertEqual(
+                    raw_snaps_scc["before"],
+                    result_payload["xray_structure_invariance"]["snapshots"][
+                        "before"
+                    ],
+                )
+                self.assertEqual(raw_snaps_scc["before"]["n_scatterers"], 1)
+                self.assertEqual(
+                    raw_snaps_scc["after_model"]["n_scatterers"], 2
+                )
+                self.assertEqual(
+                    len(raw_snaps_scc["after_model"]["records"]), 2
+                )
+                self.assertEqual(
+                    raw_snaps_scc["after_fmodel"]["n_scatterers"], 2
+                )
+                self.assertEqual(
+                    len(raw_snaps_scc["after_fmodel"]["records"]), 2
+                )
+                self.assertEqual(
+                    partial_scc_doc["completed_stages"],
+                    [
+                        "extract_xtal_data",
+                        "xray_structure_snapshot_before",
+                        "xray_structure_snapshot_after_model",
+                        "xray_structure_snapshot_after_fmodel",
+                    ],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_scc_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a2. Scatterer occupancy change during scaling retains structured raw snapshots
+            # in xray_structure_snapshots_raw while leaving verified invariance None (#1014, Task 120):
+            with self.subTest(case="3a2_scatterer_occupancy_change"):
+                out_json_soc = tmp_dir / "rfactors_soc.json"
+                out_partial_soc = (
+                    tmp_dir / "rfactors_soc_partial_evidence.json"
+                )
+                out_diag_soc = tmp_dir / "diagnostics_soc.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            mutate_scatterer_occupancy_in_scales=True
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Scatterer snapshot mutated during scaling",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_soc,
+                            out_maps_mtz_path=tmp_dir / "soc_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "soc_pair.mtz",
+                            out_diagnostics_json_path=out_diag_soc,
+                        )
+                self.assertFalse(out_json_soc.is_file())
+                self.assertTrue(out_partial_soc.is_file())
+                partial_soc_doc = json.loads(
+                    out_partial_soc.read_text(encoding="utf-8")
+                )
+                self.assertIsNone(partial_soc_doc["xray_structure_invariance"])
+                raw_snaps_soc = partial_soc_doc["xray_structure_snapshots_raw"]
+                self.assertFalse(raw_snaps_soc["invariance_verified"])
+                self.assertEqual(
+                    raw_snaps_soc["before"]["records"][0]["occupancy"], 1.0
+                )
+                self.assertEqual(
+                    raw_snaps_soc["after_model"]["records"][0]["occupancy"],
+                    0.5,
+                )
+                self.assertEqual(
+                    raw_snaps_soc["after_fmodel"]["records"][0]["occupancy"],
+                    0.5,
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_soc_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a2b. Non-finite post-scaling occupancy retains all captured scatterer records
+            # and exact IEEE-754 bits in xray_structure_snapshots_raw["after_model"] before
+            # snapshot_scatterer_records raises ValueError (#1014, Task 128):
+            with self.subTest(
+                case="3a2b_scatterer_nonfinite_occupancy_after_scales"
+            ):
+                out_json_snfo = tmp_dir / "rfactors_snfo.json"
+                out_partial_snfo = (
+                    tmp_dir / "rfactors_snfo_partial_evidence.json"
+                )
+                out_diag_snfo = tmp_dir / "diagnostics_snfo.json"
+                snfo_nan_val = struct.unpack(
+                    ">d", struct.pack(">Q", 0x7FF8000000000123)
+                )[0]
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            nonfinite_scatterer_occupancy_in_scales=snfo_nan_val
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Invalid scatterer record at index 0",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_snfo,
+                            out_maps_mtz_path=tmp_dir / "snfo_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "snfo_pair.mtz",
+                            out_diagnostics_json_path=out_diag_snfo,
+                        )
+                self.assertFalse(out_json_snfo.is_file())
+                self.assertTrue(out_partial_snfo.is_file())
+                partial_snfo_doc = json.loads(
+                    out_partial_snfo.read_text(encoding="utf-8"),
+                    parse_constant=lambda tok: (_ for _ in ()).throw(
+                        ValueError(f"Forbidden non-strict JSON constant: {tok}")
+                    ),
+                )
+                self.assertEqual(
+                    partial_snfo_doc["status"], "failed_partial_unverified"
+                )
+                self.assertIsNone(partial_snfo_doc["xray_structure_invariance"])
+                raw_snaps_snfo = partial_snfo_doc[
+                    "xray_structure_snapshots_raw"
+                ]
+                self.assertFalse(raw_snaps_snfo["invariance_verified"])
+                self.assertEqual(
+                    raw_snaps_snfo["before"]["n_scatterers"], 1
+                )
+                self.assertIsNotNone(raw_snaps_snfo["before"]["sha256"])
+                after_model_snfo = raw_snaps_snfo["after_model"]
+                self.assertIsNotNone(after_model_snfo)
+                self.assertEqual(after_model_snfo["n_scatterers"], 1)
+                self.assertIsNone(after_model_snfo["sha256"])
+                self.assertEqual(len(after_model_snfo["records"]), 1)
+                snfo_rec0 = after_model_snfo["records"][0]
+                self.assertEqual(snfo_rec0["label"], 'PDB=" N   VAL A   1 "')
+                self.assertEqual(snfo_rec0["scattering_type"], "N")
+                self.assertEqual(snfo_rec0["site_frac"], [0.1, 0.2, 0.3])
+                self.assertEqual(
+                    snfo_rec0["site_cart"], [6.4897, 15.6646, 11.6376]
+                )
+                self.assertEqual(snfo_rec0["u_iso"], 0.25)
+                occ_tag = snfo_rec0["occupancy"]
+                self.assertIsInstance(occ_tag, dict)
+                self.assertTrue(occ_tag["__nonfinite_ieee754_float64__"])
+                self.assertEqual(occ_tag["kind"], "nan")
+                self.assertEqual(
+                    occ_tag["ieee754_binary64_hex"], "0x7ff8000000000123"
+                )
+                self.assertEqual(occ_tag["sign_bit"], 0)
+                self.assertEqual(
+                    occ_tag["field_path"],
+                    "xray_structure_snapshots_raw.after_model.records[0].occupancy",
+                )
+                self.assertIsNone(raw_snaps_snfo["after_fmodel"])
+                self.assertEqual(
+                    partial_snfo_doc["completed_stages"],
+                    ["extract_xtal_data", "xray_structure_snapshot_before"],
+                )
+                self.assertEqual(
+                    [
+                        item["field_path"]
+                        for item in partial_snfo_doc["nonfinite_raw_values"]
+                    ],
+                    [
+                        "xray_structure_snapshots_raw.after_model.records[0].occupancy"
+                    ],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_snfo_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a2c. Non-finite post-scaling isotropic ADP (u_iso) retains all captured scatterer
+            # records and exact IEEE-754 bits in xray_structure_snapshots_raw["after_model"] (#1014, Task 128):
+            with self.subTest(case="3a2c_scatterer_nonfinite_adp_after_scales"):
+                out_json_snfu = tmp_dir / "rfactors_snfu.json"
+                out_partial_snfu = (
+                    tmp_dir / "rfactors_snfu_partial_evidence.json"
+                )
+                out_diag_snfu = tmp_dir / "diagnostics_snfu.json"
+                snfu_nan_val = struct.unpack(
+                    ">d", struct.pack(">Q", 0xFFF8000000000456)
+                )[0]
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            nonfinite_scatterer_u_iso_in_scales=snfu_nan_val
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Invalid scatterer record at index 0",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_snfu,
+                            out_maps_mtz_path=tmp_dir / "snfu_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "snfu_pair.mtz",
+                            out_diagnostics_json_path=out_diag_snfu,
+                        )
+                self.assertFalse(out_json_snfu.is_file())
+                self.assertTrue(out_partial_snfu.is_file())
+                partial_snfu_doc = json.loads(
+                    out_partial_snfu.read_text(encoding="utf-8"),
+                    parse_constant=lambda tok: (_ for _ in ()).throw(
+                        ValueError(f"Forbidden non-strict JSON constant: {tok}")
+                    ),
+                )
+                self.assertIsNone(partial_snfu_doc["xray_structure_invariance"])
+                raw_snaps_snfu = partial_snfu_doc[
+                    "xray_structure_snapshots_raw"
+                ]
+                self.assertFalse(raw_snaps_snfu["invariance_verified"])
+                after_model_snfu = raw_snaps_snfu["after_model"]
+                self.assertIsNotNone(after_model_snfu)
+                self.assertEqual(after_model_snfu["n_scatterers"], 1)
+                self.assertIsNone(after_model_snfu["sha256"])
+                self.assertEqual(
+                    after_model_snfu["records"][0]["occupancy"], 1.0
+                )
+                uiso_tag = after_model_snfu["records"][0]["u_iso"]
+                self.assertIsInstance(uiso_tag, dict)
+                self.assertTrue(uiso_tag["__nonfinite_ieee754_float64__"])
+                self.assertEqual(uiso_tag["kind"], "nan")
+                self.assertEqual(
+                    uiso_tag["ieee754_binary64_hex"], "0xfff8000000000456"
+                )
+                self.assertEqual(uiso_tag["sign_bit"], 1)
+                self.assertEqual(
+                    uiso_tag["field_path"],
+                    "xray_structure_snapshots_raw.after_model.records[0].u_iso",
+                )
+                self.assertIsNone(raw_snaps_snfu["after_fmodel"])
+                self.assertEqual(
+                    partial_snfu_doc["completed_stages"],
+                    ["extract_xtal_data", "xray_structure_snapshot_before"],
+                )
+                self.assertEqual(
+                    [
+                        item["field_path"]
+                        for item in partial_snfu_doc["nonfinite_raw_values"]
+                    ],
+                    [
+                        "xray_structure_snapshots_raw.after_model.records[0].u_iso"
+                    ],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_snfu_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a2d. Post-scaling zero scatterer population retains n_scatterers=0 and records=[]
+            # in xray_structure_snapshots_raw["after_model"] before failing closed (#1014, Task 128):
+            with self.subTest(
+                case="3a2d_scatterer_empty_population_after_scales"
+            ):
+                out_json_semp = tmp_dir / "rfactors_semp.json"
+                out_partial_semp = (
+                    tmp_dir / "rfactors_semp_partial_evidence.json"
+                )
+                out_diag_semp = tmp_dir / "diagnostics_semp.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(empty_scatterers_in_scales=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Scatterer snapshot cannot be empty",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_semp,
+                            out_maps_mtz_path=tmp_dir / "semp_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "semp_pair.mtz",
+                            out_diagnostics_json_path=out_diag_semp,
+                        )
+                self.assertFalse(out_json_semp.is_file())
+                self.assertTrue(out_partial_semp.is_file())
+                partial_semp_doc = json.loads(
+                    out_partial_semp.read_text(encoding="utf-8")
+                )
+                self.assertIsNone(partial_semp_doc["xray_structure_invariance"])
+                raw_snaps_semp = partial_semp_doc[
+                    "xray_structure_snapshots_raw"
+                ]
+                self.assertFalse(raw_snaps_semp["invariance_verified"])
+                self.assertEqual(
+                    raw_snaps_semp["before"]["n_scatterers"], 1
+                )
+                self.assertEqual(
+                    raw_snaps_semp["after_model"],
+                    {"n_scatterers": 0, "sha256": None, "records": []},
+                )
+                self.assertIsNone(raw_snaps_semp["after_fmodel"])
+                self.assertEqual(
+                    partial_semp_doc["completed_stages"],
+                    ["extract_xtal_data", "xray_structure_snapshot_before"],
+                )
+                self.assertEqual(partial_semp_doc["nonfinite_raw_values"], [])
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_semp_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a2e. Worker fails closed when runtime crystal_symmetry is missing or has
+            # inconsistent space_group_number / symmetry operators (#1025, Task 128):
+            missing_sg_symm = types.SimpleNamespace(
+                unit_cell=lambda: types.SimpleNamespace(
+                    parameters=lambda: _fake_f32_cell
+                ),
+                space_group_info=lambda: types.SimpleNamespace(
+                    symbol_and_number=lambda: "P 21 21 21 (No. 19)",
+                ),
+                is_similar_symmetry=lambda other: True,
+            )
+            mismatch_num_symm = types.SimpleNamespace(
+                unit_cell=lambda: types.SimpleNamespace(
+                    parameters=lambda: _fake_f32_cell
+                ),
+                space_group=lambda: types.SimpleNamespace(
+                    type=lambda: types.SimpleNamespace(number=lambda: 19),
+                    all_ops=lambda: list(_fake_rt_mx_ops),
+                ),
+                space_group_info=lambda: types.SimpleNamespace(
+                    symbol_and_number=lambda: "P 21 21 21 (No. 19)",
+                    type=lambda: types.SimpleNamespace(number=lambda: 18),
+                ),
+                is_similar_symmetry=lambda other: True,
+            )
+            empty_ops_symm = types.SimpleNamespace(
+                unit_cell=lambda: types.SimpleNamespace(
+                    parameters=lambda: _fake_f32_cell
+                ),
+                space_group=lambda: types.SimpleNamespace(
+                    type=lambda: types.SimpleNamespace(number=lambda: 19),
+                    all_ops=lambda: [],
+                ),
+                space_group_info=lambda: types.SimpleNamespace(
+                    symbol_and_number=lambda: "P 21 21 21 (No. 19)",
+                    type=lambda: types.SimpleNamespace(number=lambda: 19),
+                ),
+                is_similar_symmetry=lambda other: True,
+            )
+            for idx_sym, (sym_case_name, bad_sym_obj, bad_sym_pat) in enumerate(
+                (
+                    (
+                        "missing_space_group",
+                        missing_sg_symm,
+                        r"is missing space_group\(\)",
+                    ),
+                    (
+                        "mismatched_group_and_info_number",
+                        mismatch_num_symm,
+                        r"space_group_info\(\)\.type\(\)\.number\(\) 18 disagrees with space_group\(\)\.type\(\)\.number\(\) 19",
+                    ),
+                    (
+                        "empty_all_ops",
+                        empty_ops_symm,
+                        r"space_group\(\)\.all_ops\(\) returned no symmetry operations",
+                    ),
+                )
+            ):
+                with self.subTest(
+                    case=f"3a2e_worker_symmetry_{sym_case_name}"
+                ):
+                    out_json_sym = (
+                        tmp_dir / f"rfactors_symm_bad_{idx_sym}.json"
+                    )
+                    out_partial_sym = (
+                        tmp_dir
+                        / f"rfactors_symm_bad_{idx_sym}_partial_evidence.json"
+                    )
+                    out_maps_sym = (
+                        tmp_dir / f"symm_bad_{idx_sym}_maps.mtz"
+                    )
+                    out_pair_sym = (
+                        tmp_dir / f"symm_bad_{idx_sym}_pair.mtz"
+                    )
+                    out_diag_sym = (
+                        tmp_dir / f"diag_symm_bad_{idx_sym}.json"
+                    )
+                    with (
+                        mock.patch.dict(
+                            sys.modules,
+                            _make_vendor_modules(symmetry_override=bad_sym_obj),
+                        ),
+                        mock.patch.object(
+                            onesar_phenix_measure,
+                            "_inspect_vendor_modules",
+                            return_value=fake_runtime_env,
+                        ),
+                    ):
+                        with self.assertRaisesRegex(RuntimeError, bad_sym_pat):
+                            onesar_phenix_measure.measure_first_block_rfactors(
+                                pdb_path=pdb_path,
+                                mtz_path=mtz_path,
+                                params_path=params_path,
+                                out_json_path=out_json_sym,
+                                out_maps_mtz_path=out_maps_sym,
+                                out_obs_pair_mtz_path=out_pair_sym,
+                                out_diagnostics_json_path=out_diag_sym,
+                            )
+                    self.assertFalse(out_json_sym.is_file())
+                    self.assertFalse(out_maps_sym.is_file())
+                    self.assertFalse(out_pair_sym.is_file())
+                    self.assertTrue(out_partial_sym.is_file())
+                    self.assertTrue(out_diag_sym.is_file())
+                    partial_sym_doc = json.loads(
+                        out_partial_sym.read_text(encoding="utf-8")
+                    )
+                    diag_sym_doc = json.loads(
+                        out_diag_sym.read_text(encoding="utf-8")
+                    )
+                    self.assertEqual(
+                        partial_sym_doc["status"], "failed_partial_unverified"
+                    )
+                    self.assertEqual(
+                        partial_sym_doc["failure"]["failed_phase"], "pdb_input"
+                    )
+                    self.assertEqual(
+                        partial_sym_doc["failure"]["exception_type"],
+                        "RuntimeError",
+                    )
+                    self.assertRegex(
+                        partial_sym_doc["failure"]["exception_message"],
+                        bad_sym_pat,
+                    )
+                    self.assertEqual(diag_sym_doc["status"], "failed")
+                    self.assertEqual(
+                        diag_sym_doc["current_phase"], "pdb_input"
+                    )
+                    self.assertEqual(
+                        diag_sym_doc["partial_evidence_status"],
+                        "failed_partial_unverified",
+                    )
+                    self.assertEqual(
+                        diag_sym_doc["failure"]["exception_type"],
+                        "RuntimeError",
+                    )
+                    self.assertRegex(
+                        diag_sym_doc["failure"]["exception_message"],
+                        bad_sym_pat,
+                    )
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                    ):
+                        onesar_rerun.reconcile_phenix_rfactor_output(
+                            partial_sym_doc,
+                            ledger,
+                            expected_vendor_source_sha256=vendor_shas,
+                            expected_resolved_prefix=resolved_fake_prefix,
+                        )
+
+            # 3a3. Non-finite model coefficient fails in compute_rfactor_from_arrays after
+            # checkpointing final_effective, actual_work, and actual_free with strict-JSON
+            # IEEE-754 nonfinite tags (#1014, Task 120):
+            with self.subTest(case="3a3_nonfinite_work_model_coefficient"):
+                out_json_nf = tmp_dir / "rfactors_nonfinite.json"
+                out_partial_nf = (
+                    tmp_dir / "rfactors_nonfinite_partial_evidence.json"
+                )
+                out_diag_nf = tmp_dir / "diagnostics_nonfinite.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(nonfinite_work_model_coeff=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        r"Non-finite amplitude at index 0: fo=100\.0, fm=nan",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_nf,
+                            out_maps_mtz_path=tmp_dir / "nf_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "nf_pair.mtz",
+                            out_diagnostics_json_path=out_diag_nf,
+                        )
+                self.assertFalse(out_json_nf.is_file())
+                self.assertTrue(out_partial_nf.is_file())
+                # Verify strict RFC 8259 JSON compliance (no bare NaN/Infinity tokens):
+                partial_nf_doc = json.loads(
+                    out_partial_nf.read_text(encoding="utf-8"),
+                    parse_constant=lambda tok: (_ for _ in ()).throw(
+                        ValueError(f"Forbidden non-strict JSON constant: {tok}")
+                    ),
+                )
+                self.assertEqual(
+                    partial_nf_doc["status"], "failed_partial_unverified"
+                )
+                self.assertFalse(partial_nf_doc["rfactor_replay_verified"])
+                self.assertIsNone(partial_nf_doc["scales_and_rfactors"])
+                self.assertIsNone(partial_nf_doc["rfactor_replay_comparison"])
+                self.assertEqual(
+                    partial_nf_doc["extracted_pre_fmodel"],
+                    result_payload["extracted_pre_fmodel"],
+                )
+                self.assertEqual(
+                    partial_nf_doc["final_effective"],
+                    result_payload["final_effective"],
+                )
+                self.assertEqual(
+                    partial_nf_doc["actual_free"],
+                    result_payload["actual_free"],
+                )
+                self.assertEqual(len(partial_nf_doc["actual_work"]), 2)
+                self.assertEqual(
+                    partial_nf_doc["actual_work"][1],
+                    result_payload["actual_work"][1],
+                )
+                nf_row0 = partial_nf_doc["actual_work"][0]
+                self.assertEqual(nf_row0["hkl"], [1, 0, 0])
+                self.assertEqual(nf_row0["f_obs"], 100.0)
+                self.assertEqual(nf_row0["sigf_obs"], 2.0)
+                self.assertEqual(nf_row0["f_model_imag"], 144.0)
+                self.assertEqual(
+                    nf_row0["f_model_scaled_with_k1_w_imag"], 72.0
+                )
+                expected_nf_fields = [
+                    "f_model_real",
+                    "f_model_amplitude",
+                    "f_model_scaled_with_k1_w_real",
+                    "f_model_scaled_with_k1_w_amplitude",
+                ]
+                for nf_field in expected_nf_fields:
+                    tag_obj = nf_row0[nf_field]
+                    self.assertIsInstance(tag_obj, dict)
+                    self.assertTrue(tag_obj["__nonfinite_ieee754_float64__"])
+                    self.assertEqual(tag_obj["kind"], "nan")
+                    self.assertEqual(tag_obj["repr"], "nan")
+                    self.assertEqual(
+                        tag_obj["field_path"], f"actual_work[0].{nf_field}"
+                    )
+                    self.assertTrue(
+                        tag_obj["ieee754_binary64_hex"].startswith("0x")
+                    )
+                self.assertEqual(
+                    [
+                        item["field_path"]
+                        for item in partial_nf_doc["nonfinite_raw_values"]
+                    ],
+                    [f"actual_work[0].{f}" for f in expected_nf_fields],
+                )
+                self.assertIn(
+                    "final_effective_raw", partial_nf_doc["completed_stages"]
+                )
+                self.assertIn(
+                    "actual_work_raw", partial_nf_doc["completed_stages"]
+                )
+                self.assertIn(
+                    "actual_free_raw", partial_nf_doc["completed_stages"]
+                )
+                self.assertNotIn(
+                    "rfactor_raw_arrays_and_scalars",
+                    partial_nf_doc["completed_stages"],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_nf_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a4. Non-positive observation (fo=0.0) fails in compute_rfactor_from_arrays
+            # while retaining completed final_effective, actual_work, and actual_free (#1014, Task 120):
+            with self.subTest(case="3a4_nonpositive_final_observation"):
+                out_json_np = tmp_dir / "rfactors_nonpos.json"
+                out_partial_np = (
+                    tmp_dir / "rfactors_nonpos_partial_evidence.json"
+                )
+                out_diag_np = tmp_dir / "diagnostics_nonpos.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(nonpositive_final_obs=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        ValueError,
+                        r"Invalid amplitude sign at index 0: fo=0\.0, fm=90\.0",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_np,
+                            out_maps_mtz_path=tmp_dir / "np_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "np_pair.mtz",
+                            out_diagnostics_json_path=out_diag_np,
+                        )
+                self.assertFalse(out_json_np.is_file())
+                self.assertTrue(out_partial_np.is_file())
+                partial_np_doc = json.loads(
+                    out_partial_np.read_text(encoding="utf-8")
+                )
+                self.assertFalse(partial_np_doc["rfactor_replay_verified"])
+                self.assertIsNone(partial_np_doc["scales_and_rfactors"])
+                self.assertIsNone(partial_np_doc["rfactor_replay_comparison"])
+                self.assertEqual(partial_np_doc["nonfinite_raw_values"], [])
+                self.assertEqual(len(partial_np_doc["final_effective"]), 4)
+                self.assertEqual(
+                    partial_np_doc["final_effective"][0]["f_obs"], 0.0
+                )
+                self.assertEqual(
+                    partial_np_doc["final_effective"][1:],
+                    result_payload["final_effective"][1:],
+                )
+                self.assertEqual(len(partial_np_doc["actual_work"]), 2)
+                self.assertEqual(
+                    partial_np_doc["actual_work"][0]["f_obs"], 0.0
+                )
+                self.assertEqual(
+                    partial_np_doc["actual_work"][1],
+                    result_payload["actual_work"][1],
+                )
+                self.assertEqual(
+                    partial_np_doc["actual_free"],
+                    result_payload["actual_free"],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_np_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3a5. Failure while constructing actual_free preserves already completed
+            # final_effective and actual_work arrays (#1014, Task 120):
+            with self.subTest(
+                case="3a5_later_free_array_failure_preserves_final_and_work"
+            ):
+                out_json_ffail = tmp_dir / "rfactors_freefail.json"
+                out_partial_ffail = (
+                    tmp_dir / "rfactors_freefail_partial_evidence.json"
+                )
+                out_diag_ffail = tmp_dir / "diagnostics_freefail.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(fail_in_free_array=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError, "Injected synthetic f_obs_free failure"
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_ffail,
+                            out_maps_mtz_path=tmp_dir / "ffail_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir / "ffail_pair.mtz",
+                            out_diagnostics_json_path=out_diag_ffail,
+                        )
+                self.assertFalse(out_json_ffail.is_file())
+                self.assertTrue(out_partial_ffail.is_file())
+                partial_ffail_doc = json.loads(
+                    out_partial_ffail.read_text(encoding="utf-8")
+                )
+                self.assertEqual(
+                    partial_ffail_doc["final_effective"],
+                    result_payload["final_effective"],
+                )
+                self.assertEqual(
+                    partial_ffail_doc["actual_work"],
+                    result_payload["actual_work"],
+                )
+                self.assertIsNone(partial_ffail_doc["actual_free"])
+
+            # 3b. Deliberate native r_work mismatch fails before success output is written,
+            # while partial_evidence JSON on disk retains extracted/final/work/free HKLs,
+            # observations, complex model coefficients, scatterers, and both native and
+            # replayed scalar R values (#1014 Finding 4, #1017):
+            with self.subTest(case="3b_native_r_work_mismatch"):
+                out_json_rmis = tmp_dir / "rfactors_rmismatch.json"
+                out_partial_rmis = (
+                    tmp_dir / "rfactors_rmismatch_partial_evidence.json"
+                )
+                out_diag_rmis = tmp_dir / "diagnostics_rmismatch.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            native_r_work_override=(20.0 / 300.0 + 0.01)
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError,
+                        r"Native fmodel\.r_work\(\) .* disagrees with array replay",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_rmis,
+                            out_maps_mtz_path=tmp_dir / "rmismatch_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir
+                            / "rmismatch_pair.mtz",
+                            out_diagnostics_json_path=out_diag_rmis,
+                        )
+                self.assertFalse(out_json_rmis.is_file())
+                self.assertTrue(out_partial_rmis.is_file())
+                partial_rmis_doc = json.loads(
+                    out_partial_rmis.read_text(encoding="utf-8")
+                )
+                self.assertEqual(
+                    partial_rmis_doc["artifact_role"],
+                    "partial_unverified_phenix_evidence",
+                )
+                self.assertFalse(partial_rmis_doc["reusable_cache_output"])
+                self.assertFalse(
+                    partial_rmis_doc["canonical_eval_qds_eligible"]
+                )
+                self.assertEqual(
+                    partial_rmis_doc["status"], "failed_partial_unverified"
+                )
+                self.assertFalse(partial_rmis_doc["rfactor_replay_verified"])
+                self.assertEqual(
+                    partial_rmis_doc["extracted_pre_fmodel"],
+                    result_payload["extracted_pre_fmodel"],
+                )
+                self.assertEqual(
+                    partial_rmis_doc["final_effective"],
+                    result_payload["final_effective"],
+                )
+                self.assertEqual(
+                    partial_rmis_doc["actual_work"],
+                    result_payload["actual_work"],
+                )
+                self.assertEqual(
+                    partial_rmis_doc["actual_free"],
+                    result_payload["actual_free"],
+                )
+                self.assertEqual(
+                    partial_rmis_doc["xray_structure_invariance"],
+                    result_payload["xray_structure_invariance"],
+                )
+                self.assertAlmostEqual(
+                    partial_rmis_doc["scales_and_rfactors"]["r_work"],
+                    20.0 / 300.0 + 0.01,
+                    places=12,
+                )
+                self.assertAlmostEqual(
+                    partial_rmis_doc["scales_and_rfactors"]["replayed_r_work"],
+                    20.0 / 300.0,
+                    places=12,
+                )
+                self.assertAlmostEqual(
+                    partial_rmis_doc["rfactor_replay_comparison"][
+                        "r_work_abs_diff"
+                    ],
+                    0.01,
+                    places=12,
+                )
+                self.assertIsNone(partial_rmis_doc["map_coefficients"])
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_rmis_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 3c. Failure during map MTZ serialization/writing retains completed R-factor
+            # arrays AND already-obtained complex map_coefficients without writing out_json (#1014 Finding 4, #1017):
+            with self.subTest(case="3c_map_mtz_write_failure"):
+                out_json_mfail = tmp_dir / "rfactors_mapwrite_fail.json"
+                out_partial_mfail = (
+                    tmp_dir / "rfactors_mapwrite_fail_partial_evidence.json"
+                )
+                out_diag_mfail = tmp_dir / "diagnostics_mapwrite_fail.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(fail_in_map_write=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError, "Injected synthetic map write failure"
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_mfail,
+                            out_maps_mtz_path=tmp_dir / "mapwrite_fail_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir
+                            / "mapwrite_fail_pair.mtz",
+                            out_diagnostics_json_path=out_diag_mfail,
+                        )
+                self.assertFalse(out_json_mfail.is_file())
+                self.assertTrue(out_partial_mfail.is_file())
+                partial_mfail_doc = json.loads(
+                    out_partial_mfail.read_text(encoding="utf-8")
+                )
+                self.assertEqual(
+                    partial_mfail_doc["status"], "failed_partial_unverified"
+                )
+                self.assertTrue(partial_mfail_doc["rfactor_replay_verified"])
+                self.assertFalse(partial_mfail_doc["map_export_verified"])
+                self.assertEqual(
+                    partial_mfail_doc["actual_work"],
+                    result_payload["actual_work"],
+                )
+                self.assertEqual(
+                    partial_mfail_doc["actual_free"],
+                    result_payload["actual_free"],
+                )
+                self.assertEqual(
+                    partial_mfail_doc["map_coefficients"]["2mFo-DFc"],
+                    result_payload["map_coefficients"]["2mFo-DFc"],
+                )
+                self.assertEqual(
+                    partial_mfail_doc["map_coefficients"]["mFo-DFc"],
+                    result_payload["map_coefficients"]["mFo-DFc"],
+                )
+                self.assertIsNone(
+                    partial_mfail_doc["map_coefficients"]["mtz_supplements"]
+                )
+                self.assertNotIn("mtz_supplements", partial_mfail_doc)
+
+            # 3d. Failure inside fmodel.map_coefficients retains completed R-factor arrays
+            # without fabricating map_coefficients (#1014 Finding 4, #1017):
+            with self.subTest(case="3d_map_coefficients_failure"):
+                out_json_mcfail = tmp_dir / "rfactors_mapcoeff_fail.json"
+                out_partial_mcfail = (
+                    tmp_dir / "rfactors_mapcoeff_fail_partial_evidence.json"
+                )
+                out_diag_mcfail = tmp_dir / "diagnostics_mapcoeff_fail.json"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(fail_in_map_coefficients=True),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError,
+                        "Injected synthetic map_coefficients failure",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_mcfail,
+                            out_maps_mtz_path=tmp_dir
+                            / "mapcoeff_fail_maps.mtz",
+                            out_obs_pair_mtz_path=tmp_dir
+                            / "mapcoeff_fail_pair.mtz",
+                            out_diagnostics_json_path=out_diag_mcfail,
+                        )
+                self.assertFalse(out_json_mcfail.is_file())
+                self.assertTrue(out_partial_mcfail.is_file())
+                partial_mcfail_doc = json.loads(
+                    out_partial_mcfail.read_text(encoding="utf-8")
+                )
+                self.assertTrue(partial_mcfail_doc["rfactor_replay_verified"])
+                self.assertIsNone(partial_mcfail_doc["map_coefficients"])
+                self.assertNotIn("mtz_supplements", partial_mcfail_doc)
+
+            # 3d2. Failure on the second fmodel.map_coefficients("mFo-DFc") call retains
+            # the already obtained/exported first map ("2mFo-DFc") and completed R-factor
+            # arrays while leaving "mFo-DFc" and "mtz_supplements" None (#1014, Task 120):
+            with self.subTest(case="3d2_second_map_coefficients_failure"):
+                out_json_mc2fail = tmp_dir / "rfactors_mapcoeff2_fail.json"
+                out_partial_mc2fail = (
+                    tmp_dir / "rfactors_mapcoeff2_fail_partial_evidence.json"
+                )
+                out_diag_mc2fail = tmp_dir / "diagnostics_mapcoeff2_fail.json"
+                out_maps_mc2fail = tmp_dir / "mapcoeff2_fail_maps.mtz"
+                out_pair_mc2fail = tmp_dir / "mapcoeff2_fail_pair.mtz"
+                with (
+                    mock.patch.dict(
+                        sys.modules,
+                        _make_vendor_modules(
+                            fail_in_second_map_coefficients=True
+                        ),
+                    ),
+                    mock.patch.object(
+                        onesar_phenix_measure,
+                        "_inspect_vendor_modules",
+                        return_value=fake_runtime_env,
+                    ),
+                ):
+                    with self.assertRaisesRegex(
+                        RuntimeError,
+                        "Injected synthetic second-map coefficient failure",
+                    ):
+                        onesar_phenix_measure.measure_first_block_rfactors(
+                            pdb_path=pdb_path,
+                            mtz_path=mtz_path,
+                            params_path=params_path,
+                            out_json_path=out_json_mc2fail,
+                            out_maps_mtz_path=out_maps_mc2fail,
+                            out_obs_pair_mtz_path=out_pair_mc2fail,
+                            out_diagnostics_json_path=out_diag_mc2fail,
+                        )
+                self.assertFalse(out_json_mc2fail.is_file())
+                self.assertFalse(out_maps_mc2fail.is_file())
+                self.assertFalse(out_pair_mc2fail.is_file())
+                self.assertTrue(out_partial_mc2fail.is_file())
+                partial_mc2fail_doc = json.loads(
+                    out_partial_mc2fail.read_text(encoding="utf-8")
+                )
+                self.assertEqual(
+                    partial_mc2fail_doc["status"], "failed_partial_unverified"
+                )
+                self.assertTrue(partial_mc2fail_doc["rfactor_replay_verified"])
+                self.assertFalse(partial_mc2fail_doc["map_export_verified"])
+                self.assertEqual(
+                    partial_mc2fail_doc["extracted_pre_fmodel"],
+                    result_payload["extracted_pre_fmodel"],
+                )
+                self.assertEqual(
+                    partial_mc2fail_doc["final_effective"],
+                    result_payload["final_effective"],
+                )
+                self.assertEqual(
+                    partial_mc2fail_doc["actual_work"],
+                    result_payload["actual_work"],
+                )
+                self.assertEqual(
+                    partial_mc2fail_doc["actual_free"],
+                    result_payload["actual_free"],
+                )
+                self.assertEqual(
+                    partial_mc2fail_doc["map_coefficients"]["2mFo-DFc"],
+                    result_payload["map_coefficients"]["2mFo-DFc"],
+                )
+                self.assertIsNone(
+                    partial_mc2fail_doc["map_coefficients"]["mFo-DFc"]
+                )
+                self.assertIsNone(
+                    partial_mc2fail_doc["map_coefficients"]["mtz_supplements"]
+                )
+                self.assertNotIn("mtz_supplements", partial_mc2fail_doc)
+                self.assertIn(
+                    "map_coefficients_2mfo_dfc_raw",
+                    partial_mc2fail_doc["completed_stages"],
+                )
+                self.assertNotIn(
+                    "map_coefficients_raw",
+                    partial_mc2fail_doc["completed_stages"],
+                )
+                with self.assertRaisesRegex(
+                    ValueError,
+                    "Partial/unverified PHENIX evidence artifact cannot be reconciled",
+                ):
+                    onesar_rerun.reconcile_phenix_rfactor_output(
+                        partial_mc2fail_doc,
+                        ledger,
+                        expected_vendor_source_sha256=vendor_shas,
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+
+            # 4. Direct _inspect_vendor_modules check for inside/outside prefix (#1013):
+            with self.subTest(case="4_inspect_vendor_modules_prefix"):
+                fake_bin = fake_prefix / "bin"
+                fake_bin.mkdir(parents=True, exist_ok=True)
+                fake_py = fake_bin / "python3.9"
+                fake_py.write_text("#!/bin/sh\n", encoding="utf-8")
+                inside_mod_file = fake_prefix / "lib" / "mod_inside.py"
+                inside_mod_file.parent.mkdir(parents=True, exist_ok=True)
+                inside_mod_file.write_text("# inside\n", encoding="utf-8")
+                outside_mod_file = tmp_dir / "mod_outside.py"
+                outside_mod_file.write_text("# outside\n", encoding="utf-8")
+                with (
+                    mock.patch.object(sys, "prefix", resolved_fake_prefix),
+                    mock.patch.object(sys, "executable", str(fake_py)),
+                ):
+                    inspected = onesar_phenix_measure._inspect_vendor_modules(
+                        {
+                            "vendor_test": types.SimpleNamespace(
+                                __name__="vendor_test",
+                                __file__=str(inside_mod_file),
+                            )
+                        },
+                        expected_resolved_prefix=resolved_fake_prefix,
+                    )
+                    self.assertEqual(
+                        inspected["resolved_sys_prefix"], resolved_fake_prefix
+                    )
+                    with self.assertRaisesRegex(
+                        ValueError, "escapes sys.prefix"
+                    ):
+                        onesar_phenix_measure._inspect_vendor_modules(
+                            {
+                                "vendor_test": types.SimpleNamespace(
+                                    __name__="vendor_test",
+                                    __file__=str(outside_mod_file),
+                                )
+                            },
+                            expected_resolved_prefix=resolved_fake_prefix,
+                        )
+
+    def _verify_atomic_checkpoint_interrupted_write_and_promotion(
+        self, tmp_dir: Path
+    ) -> None:
+        """Verify mid-write partial bytes and failed promotion preserve prior JSON and unlink temp siblings."""
+        atomic_target = tmp_dir / "atomic_checkpoint.json"
+        prior_payload = {
+            "status": "running",
+            "current_phase": "extract_xtal_data",
+            "phase_history": ["startup", "extract_xtal_data"],
+            "extract_xtal_data_log": "complete prior log\n",
+        }
+        onesar_phenix_measure._checkpoint_diagnostics(
+            prior_payload, atomic_target
+        )
+        prior_bytes = atomic_target.read_bytes()
+        self.assertEqual(
+            json.loads(atomic_target.read_text(encoding="utf-8")),
+            prior_payload,
+        )
+
+        real_open_temp = onesar_phenix_measure._open_checkpoint_temp_file
+        partial_temp_records: list[tuple[Path, bytes]] = []
+
+        class _InterruptedTempFileWrapper:
+            def __init__(self, tmp_path: Path) -> None:
+                self._tmp_path = tmp_path
+                self._fh = real_open_temp(tmp_path)
+
+            def __enter__(self) -> "_InterruptedTempFileWrapper":
+                self._fh.__enter__()
+                return self
+
+            def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> Any:
+                return self._fh.__exit__(exc_type, exc_val, exc_tb)
+
+            def write(self, data: str) -> int:
+                partial_chunk = data[:16]
+                self._fh.write(partial_chunk)
+                self._fh.flush()
+                os.fsync(self._fh.fileno())
+                partial_temp_records.append(
+                    (self._tmp_path, self._tmp_path.read_bytes())
+                )
+                raise OSError(
+                    "Simulated mid-write interruption after partial bytes"
+                )
+
+        with mock.patch.object(
+            onesar_phenix_measure,
+            "_open_checkpoint_temp_file",
+            side_effect=_InterruptedTempFileWrapper,
+        ):
+            with self.assertRaisesRegex(
+                OSError,
+                "Simulated mid-write interruption after partial bytes",
+            ):
+                onesar_phenix_measure._checkpoint_diagnostics(
+                    {
+                        "status": "running",
+                        "current_phase": "update_all_scales",
+                    },
+                    atomic_target,
+                )
+
+        self.assertEqual(len(partial_temp_records), 1)
+        recorded_tmp_path, recorded_partial_bytes = partial_temp_records[0]
+        self.assertEqual(len(recorded_partial_bytes), 16)
+        self.assertFalse(recorded_tmp_path.exists())
+        self.assertEqual(atomic_target.read_bytes(), prior_bytes)
+        self.assertEqual(
+            json.loads(atomic_target.read_text(encoding="utf-8")),
+            prior_payload,
+        )
+        self.assertEqual(
+            list(tmp_dir.glob(".atomic_checkpoint.json.tmp.*")), []
+        )
+
+        real_os_replace = onesar_phenix_measure.os.replace
+        replace_temp_records: list[tuple[Path, bytes]] = []
+
+        def _interrupted_os_replace(src: Any, dst: Any) -> None:
+            src_path = Path(src)
+            replace_temp_records.append((src_path, src_path.read_bytes()))
+            raise OSError("Simulated os.replace interruption")
+
+        with mock.patch.object(
+            onesar_phenix_measure.os,
+            "replace",
+            side_effect=_interrupted_os_replace,
+        ):
+            with self.assertRaisesRegex(
+                OSError, "Simulated os.replace interruption"
+            ):
+                onesar_phenix_measure._checkpoint_diagnostics(
+                    {
+                        "status": "running",
+                        "current_phase": "update_all_scales",
+                    },
+                    atomic_target,
+                )
+        self.assertEqual(len(replace_temp_records), 1)
+        replace_tmp_path, replace_tmp_bytes = replace_temp_records[0]
+        self.assertEqual(
+            json.loads(replace_tmp_bytes.decode("utf-8"))["current_phase"],
+            "update_all_scales",
+        )
+        self.assertFalse(replace_tmp_path.exists())
+        self.assertEqual(atomic_target.read_bytes(), prior_bytes)
+        self.assertEqual(
+            json.loads(atomic_target.read_text(encoding="utf-8")),
+            prior_payload,
+        )
+        self.assertEqual(
+            list(tmp_dir.glob(".atomic_checkpoint.json.tmp.*")), []
+        )
+        self.assertIs(
+            onesar_phenix_measure._open_checkpoint_temp_file, real_open_temp
+        )
+        self.assertIs(onesar_phenix_measure.os.replace, real_os_replace)
+
+    def test_atomic_checkpoint_interrupted_write_and_promotion_preserve_prior_1014(
+        self,
+    ) -> None:
+        """Standalone test for mid-write partial bytes and failed promotion on atomic checkpoints."""
+        with tempfile.TemporaryDirectory() as tmp:
+            tmp_dir = Path(tmp).resolve(strict=True)
+            self._verify_atomic_checkpoint_interrupted_write_and_promotion(
+                tmp_dir
+            )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/scripts/test_toolchain.py b/scripts/test_toolchain.py
index fc9d3ae..665cf67 100644
--- a/scripts/test_toolchain.py
+++ b/scripts/test_toolchain.py
@@ -269,4 +269,209 @@ def check(label: str, condition: bool) -> None:
       and toolchain.EXTERNAL_TOOL_SPECS["gemmi"]["expected_version"] == toolchain.GEMMI_VERSION)
 check("tool paths are defined only in toolchain.py", not duplicate_constants)
 
+with tempfile.TemporaryDirectory(prefix="refmac wrapper ; $() ") as tmp:
+    root = Path(tmp)
+    spaced_py_dir = root / "spaced python interpreter ; dir"
+    spaced_py_dir.mkdir()
+    spaced_python = spaced_py_dir / "python3"
+    spaced_python.symlink_to(Path(sys.executable))
+    fake_refmac = root / "refmac5 ; $(never-run)"
+    fake_refmac.write_text(
+        "#!/usr/bin/env python3\n"
+        "import json, os, sys\n"
+        "print(json.dumps({\n"
+        "    'argv': sys.argv,\n"
+        "    'stdin': sys.stdin.read(),\n"
+        "    'marker': os.environ.get('PROTSTRUCT_REFMAC_ENV_MARKER'),\n"
+        "    'executable': sys.executable,\n"
+        "    'path_head': os.environ.get('PATH', '').split(os.pathsep)[0],\n"
+        "}))\n"
+    )
+    fake_refmac.chmod(0o755)
+    xyzin = root / "in ; model.pdb"
+    hklin = root / "in $(obs).mtz"
+    xyzout = root / "out ; model.pdb"
+    hklout = root / "out $(obs).mtz"
+    xmlout = root / "out ; xml $(literal).xml"
+    log_path = root / "refmac ; run.log"
+    custom_env = dict(os.environ)
+    custom_env["PATH"] = f"{spaced_py_dir}{os.pathsep}{os.environ.get('PATH', '')}"
+    custom_env["PROTSTRUCT_REFMAC_ENV_MARKER"] = "explicit-ccp4-env ; $()"
+
+    ccp4_env_calls = 0
+    orig_ccp4_environment = toolchain.ccp4_environment
+    try:
+        def _stub_ccp4_environment(setup_path=toolchain.CCP4_SETUP):
+            global ccp4_env_calls
+            ccp4_env_calls += 1
+            env_copy = dict(os.environ)
+            env_copy["PATH"] = f"{spaced_py_dir}{os.pathsep}{os.environ.get('PATH', '')}"
+            env_copy["PROTSTRUCT_REFMAC_ENV_MARKER"] = "stubbed-ccp4-default-env"
+            return env_copy
+
+        toolchain.ccp4_environment = _stub_ccp4_environment
+
+        proc = toolchain.run_refmac(
+            xyzin,
+            hklin,
+            xyzout,
+            hklout,
+            log_path,
+            "NCYC 0\nEND\n",
+            cwd=root,
+            timeout=10,
+            xml_output=xmlout,
+            executable=fake_refmac,
+            env=custom_env,
+        )
+        payload = json.loads(log_path.read_text())
+        check("run_refmac with explicit executable and xml_output succeeds", proc.returncode == 0)
+        check(
+            "fake executable runs under whitespace-safe shebang via spaced interpreter PATH",
+             payload["path_head"] == str(spaced_py_dir)
+            and Path(payload["executable"]).resolve() == Path(sys.executable).resolve(),
+        )
+        check(
+            "run_refmac passes XMLOUT and metacharacter paths literally",
+            payload["argv"]
+            == [
+                str(fake_refmac),
+                "XYZIN",
+                str(xyzin),
+                "HKLIN",
+                str(hklin),
+                "XYZOUT",
+                str(xyzout),
+                "HKLOUT",
+                str(hklout),
+                "XMLOUT",
+                str(xmlout),
+            ],
+        )
+        check(
+            "run_refmac forwards stdin keywords and explicit env without calling ccp4_environment",
+            payload["stdin"] == "NCYC 0\nEND\n"
+            and payload["marker"] == "explicit-ccp4-env ; $()"
+            and ccp4_env_calls == 0,
+        )
+
+        # Explicit env with ccp4=False is also authoritative and never calls ccp4_environment
+        log_path_false = root / "refmac ; ccp4-false.log"
+        proc_false = toolchain.run_refmac(
+            xyzin,
+            hklin,
+            xyzout,
+            hklout,
+            log_path_false,
+            "NCYC 0\nEND\n",
+            cwd=root,
+            timeout=10,
+            executable=fake_refmac,
+            env=custom_env,
+            ccp4=False,
+        )
+        payload_false = json.loads(log_path_false.read_text())
+        check(
+            "run_refmac with explicit env and ccp4=False preserves supplied env",
+            proc_false.returncode == 0
+            and payload_false["marker"] == "explicit-ccp4-env ; $()"
+            and ccp4_env_calls == 0,
+        )
+
+        # Explicit env combined with ccp4=True must raise ValueError without calling ccp4_environment
+        rejected_combo = False
+        try:
+            toolchain.run_refmac(
+                xyzin,
+                hklin,
+                xyzout,
+                hklout,
+                root / "refmac ; should-not-run.log",
+                "NCYC 0\nEND\n",
+                cwd=root,
+                timeout=10,
+                executable=fake_refmac,
+                env=custom_env,
+                ccp4=True,
+            )
+        except ValueError as exc:
+            rejected_combo = "ccp4=True" in str(exc) and "env" in str(exc)
+        check(
+            "run_refmac rejects explicit env combined with ccp4=True",
+            rejected_combo
+            and ccp4_env_calls == 0
+            and not (root / "refmac ; should-not-run.log").exists(),
+        )
+
+        # Legacy neither option (env=None, ccp4=None) loads ccp4_environment()
+        log_path_legacy = root / "refmac ; legacy-ccp4.log"
+        proc_legacy = toolchain.run_refmac(
+            xyzin,
+            hklin,
+            xyzout,
+            hklout,
+            log_path_legacy,
+            "END\n",
+            cwd=root,
+            timeout=10,
+            executable=fake_refmac,
+        )
+        payload_legacy = json.loads(log_path_legacy.read_text())
+        check(
+            "legacy run_refmac without env loads ccp4_environment by default",
+            proc_legacy.returncode == 0
+            and ccp4_env_calls == 1
+            and payload_legacy["marker"] == "stubbed-ccp4-default-env",
+        )
+    finally:
+        toolchain.ccp4_environment = orig_ccp4_environment
+
+    captured_calls: list[dict[str, object]] = []
+    orig_run_logged = toolchain.run_logged
+    try:
+        def _fake_run_logged(arguments, log_p, *, cwd=None, timeout=None, env=None, ccp4=False, input_text=None):
+            captured_calls.append(
+                {
+                    "arguments": list(arguments),
+                    "log_path": log_p,
+                    "cwd": cwd,
+                    "timeout": timeout,
+                    "env": env,
+                    "ccp4": ccp4,
+                    "input_text": input_text,
+                }
+            )
+            return toolchain.subprocess.CompletedProcess(["refmac5"], 0, "", "")
+
+        toolchain.run_logged = _fake_run_logged
+        toolchain.run_refmac(
+            "in.pdb",
+            "in.mtz",
+            "out.pdb",
+            "out.mtz",
+            "refmac.log",
+            "END\n",
+            cwd=root,
+        )
+    finally:
+        toolchain.run_logged = orig_run_logged
+    check(
+        "default run_refmac call preserves legacy argv and ccp4=True",
+        len(captured_calls) == 1
+        and captured_calls[0]["arguments"]
+        == [
+            "refmac5",
+            "XYZIN",
+            "in.pdb",
+            "HKLIN",
+            "in.mtz",
+            "XYZOUT",
+            "out.pdb",
+            "HKLOUT",
+            "out.mtz",
+        ]
+        and captured_calls[0]["ccp4"] is True
+        and captured_calls[0]["env"] is None,
+    )
+
 print("\nall toolchain unit tests passed")
diff --git a/scripts/toolchain.py b/scripts/toolchain.py
index fabb6e1..1c7f2ab 100644
--- a/scripts/toolchain.py
+++ b/scripts/toolchain.py
@@ -1035,23 +1035,47 @@ def run_refmac(
     *,
     cwd: str | os.PathLike[str],
     timeout: float = 1800,
+    xml_output: str | os.PathLike[str] | None = None,
+    executable: str | os.PathLike[str] = "refmac5",
+    env: Mapping[str, str] | None = None,
+    ccp4: bool | None = None,
 ) -> subprocess.CompletedProcess[str]:
-    """Run REFMAC with its keyword block on stdin, without a heredoc shell."""
+    """Run REFMAC with its keyword block on stdin, without a heredoc shell.
+
+    Environment contract:
+    - When ``env`` is supplied (``env is not None``) and ``ccp4`` is omitted
+      (``None``) or ``False``, ``env`` is authoritative and ``ccp4_environment()``
+      is not called.
+    - Combining ``env is not None`` with ``ccp4=True`` raises ``ValueError`` so
+      a caller-supplied environment is never silently overwritten.
+    - Omitting both ``env`` and ``ccp4`` preserves the legacy default
+      ``ccp4=True`` (loading ``ccp4_environment()``).
+    """
+    if env is not None and ccp4:
+        raise ValueError(
+            "run_refmac cannot combine an explicit env with ccp4=True; "
+            "pass env with ccp4 omitted or False so the supplied environment is authoritative"
+        )
+    arguments: list[str | os.PathLike[str]] = [
+        executable,
+        "XYZIN",
+        model,
+        "HKLIN",
+        reflections,
+        "XYZOUT",
+        xyz_output,
+        "HKLOUT",
+        hkl_output,
+    ]
+    if xml_output is not None:
+        arguments.extend(["XMLOUT", xml_output])
+    use_ccp4 = (env is None) if ccp4 is None else bool(ccp4)
     return run_logged(
-        [
-            "refmac5",
-            "XYZIN",
-            model,
-            "HKLIN",
-            reflections,
-            "XYZOUT",
-            xyz_output,
-            "HKLOUT",
-            hkl_output,
-        ],
+        arguments,
         log_path,
         cwd=cwd,
         timeout=timeout,
-        ccp4=True,
+        env=env,
+        ccp4=use_ccp4,
         input_text=keywords,
     )
diff --git a/scripts/validate.sh b/scripts/validate.sh
index ab86176..df11097 100755
--- a/scripts/validate.sh
+++ b/scripts/validate.sh
@@ -216,7 +216,13 @@ if ! "${PYTHON}" "${REPO_ROOT}/scripts/test_record_tools.py"; then
   fail "record tool unit tests"
 fi
 
-# 4i. Every remaining scripts/test_*.py, discovered rather than enumerated (#228).
+# 4i. 1SAR rerun input-admission, MTZ projection, and PHENIX R-factor
+#     reconciliation pure-logic unit tests (no PHENIX/CCP4/Gemmi execution).
+if ! "${PYTHON}" "${REPO_ROOT}/scripts/test_onesar_rerun.py"; then
+  fail "onesar_rerun unit tests"
+fi
+
+# 4j. Every remaining scripts/test_*.py, discovered rather than enumerated (#228).
 #     test_round_figures.py shipped in #188 with 18 checks and was never run here,
 #     because this file names each suite by hand and one was forgotten -- silently,
 #     as it always would be. A suite nothing executes is the repo's #2 class: a
@@ -226,7 +232,7 @@ for _suite in "${REPO_ROOT}"/scripts/test_*.py; do
   case "$(basename "${_suite}")" in
     test_qds_emit.py|test_t15_ss_agreement.py|test_t16_interface_quality.py| \
     test_t17_nmr_ensemble.py|test_t17_restraint_summary.py|test_bench_tolerances.py| \
-    test_guards.py|test_record_tools.py|test_summary_coverage.py) continue ;;
+    test_guards.py|test_record_tools.py|test_onesar_rerun.py|test_summary_coverage.py) continue ;;
   esac
   if ! "${PYTHON}" "${_suite}"; then
     fail "$(basename "${_suite}" .py)"