[us-commits] [ehb54/ultrascan3] 8472b0: Perceive hybridization/vdW radius/electron count f...

emre brookes noreply at github.com
Sun Aug 9 06:04:58 MDT 2026


  Branch: refs/heads/ehb54-issue-980
  Home:   https://github.com/ehb54/ultrascan3
  Commit: 8472b052799e2cf0ddc0b16060ffd844184a937e
      https://github.com/ehb54/ultrascan3/commit/8472b052799e2cf0ddc0b16060ffd844184a937e
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    A us_somo/develop/include/us_hydrodyn_perceive.h
    A us_somo/develop/include/us_hydrodyn_perceive_elements.h
    A us_somo/develop/include/us_hydrodyn_perceive_hybrid.h
    A us_somo/develop/include/us_hydrodyn_perceive_saxs.h
    A us_somo/develop/include/us_hydrodyn_perceive_somo.h
    M us_somo/develop/libus_somo.pro
    A us_somo/develop/perceiver/.gitignore
    A us_somo/develop/perceiver/DECISIONS.md
    A us_somo/develop/perceiver/README.md
    A us_somo/develop/perceiver/data/ref/f0_WaasKirf.dat
    A us_somo/develop/perceiver/data/ref/it1992.cpp
    A us_somo/develop/perceiver/data/ref/somo.saxs_atoms.generated
    A us_somo/develop/perceiver/pdb_lite.h
    A us_somo/develop/perceiver/residue_oracle.h
    A us_somo/develop/perceiver/tests/coverage.cpp
    A us_somo/develop/perceiver/tests/emit_residue.cpp
    A us_somo/develop/perceiver/tests/regression.cpp
    A us_somo/develop/perceiver/tests/tests_unit.cpp
    A us_somo/develop/perceiver/tinytest.h
    A us_somo/develop/perceiver/tools/gen_saxs_entries.py
    A us_somo/develop/src/us_hydrodyn_perceive.cpp
    A us_somo/develop/src/us_hydrodyn_perceive_somo.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  Perceive hybridization/vdW radius/electron count for non-coded residues

Phases A and B of ehb54/ultrascan-tickets#978.

SOMO needs an atom's hybridization, vdW radius and electron count to build a
bead model, and today they come only from the hand-curated somo.residue. For a
residue that table does not code, SOMO synthesises a "<RESNAME>_NC<n>"
placeholder and models it as a generic ABB average bead. This adds a chemical
perceiver that derives the real per-atom chemistry from element + coordinates,
so such a residue can be given a proper entry instead of an averaged one.

somo.residue remains the master: perception runs only for residues SOMO itself
recorded in unknown_residues.

Phase A - core compiled into libus_somo:
  include/us_hydrodyn_perceive{,_elements,_hybrid,_saxs}.h
  src/us_hydrodyn_perceive.cpp
Pipeline: spatial-grid bond perception (honouring CONECT where present, and
H-/ion-agnostic so implicit-H inference is identical with or without explicit
hydrogens) -> ring detection + planarity aromaticity -> Kekule bond orders ->
per-element classification -> emit a somo.residue block plus any somo.hybrid
rows for novel types.

The core is deliberately Qt-free, so the standalone harness in
us_somo/develop/perceiver builds the very same sources without Qt: 23 unit
tests plus a regression that takes residues somo.residue DOES code, pretends
they are unknown, and checks they are reproduced - 41,219 atoms over 8 demo
structures, 99.83% geometric perception, 0.17% genuine error. The residual is
protonation/tautomer state, which heavy-atom geometry cannot resolve in
principle. The harness reads us_somo/etc directly so it cannot drift from the
real tables.

us_hydrodyn_perceive_somo.{h,cpp} is the only Qt-aware layer: it converts
PDB_model/PDB_chain/PDB_atom, reads CONECT and HETNAM from the source file,
and returns tentative entries. It converts the whole model rather than one
residue because bond perception needs surrounding context (peptide links,
disulfides, metal coordination) to get coordination numbers, and hence
implicit-H counts, right. Instances are de-duplicated back to the base residue
name so one confirmation covers all instances of the same chemistry.

Phase B - headless "perceive <pdbfile>" gui_script command, so the whole path
is exercisable against real structures with no GUI:
  us3_somo _pad -I -g <script>
Entries carry a REVIEW block naming every atom whose perception was uncertain
(ambiguous protonation, assumed oxidation state, estimated vbar), which is what
the confirmation UI in ehb54/ultrascan-tickets#979 will present to the user.

No existing behaviour changes: nothing calls the perceiver outside the new
command.

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 63531b51cd920026bcb1dbe496b845029a8a29b5
      https://github.com/ehb54/ultrascan3/commit/63531b51cd920026bcb1dbe496b845029a8a29b5
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive_somo.h
    M us_somo/develop/perceiver/README.md
    A us_somo/develop/perceiver/examples/perceive.somo
    M us_somo/develop/src/us_hydrodyn_perceive_somo.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  Add "perceive compare" hand-testing sub-command

Part of ehb54/ultrascan-tickets#978.

"perceive compare <pdb>" runs perception over the residues somo.residue DOES
code and diffs the result against the curated types, reporting exact-match and
physics-match rates, a curated->perceived difference histogram, and the first
differing atoms. It is the interactive equivalent of the standalone regression
harness, but run inside SOMO against the user's own installed ~/ultrascan/etc
tables - so it also reveals when an installed table is out of date.

Cross-validation on 1HEL: the in-SOMO path and the standalone harness agree
exactly (1000 atoms scored, 984 exact 98.40%, 995 physics 99.50%), despite
using different PDB parsers and different copies of the tables. The five
physics differences are all known, explainable categories: the chain
N-terminus (a real -NH3+, where the perceiver is chemically right and the
residue template cannot know), the His tautomer and its charge, and the
Asp carboxylate protonation convention.

Also adds examples/perceive.somo documenting the invocation, including the
gotchas that cost time otherwise: the throwaway "_pad" argument is required on
macOS and must be omitted on Linux, "-I" avoids blocking config dialogs, and
the first run after rebuilding libus_somo only re-installs configs and exits,
so it must be run twice.

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 793f8717051b42f4264d67c46984f6a49731e154
      https://github.com/ehb54/ultrascan3/commit/793f8717051b42f4264d67c46984f6a49731e154
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/README.md

  Log Message:
  -----------
  Correct perceiver README: tables are read from us_somo/etc, not copied

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: a8f1166ccd28a4368cc8ea4e0284d2febc137dcf
      https://github.com/ehb54/ultrascan3/commit/a8f1166ccd28a4368cc8ea4e0284d2febc137dcf
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    A us_somo/develop/perceiver/tools/psv_model.py

  Log Message:
  -----------
  Add psv group-contribution probe (analysis for ticket #980)

Tests Mattia's suggestion that a rough psv can be suggested for a non-coded
residue from similarity to the hybridizations already in somo.residue, by
fitting a Cohn-Edsall / Durchschlag-Zipper style group-contribution model over
the ~100 genuine polyatomic residues somo.residue codes and scoring it
leave-one-out.

Result (MAE in vbar, cm^3/g; typical vbar ~0.73):
  global mean                     0.110
  sum-of-vdW-spheres (emitted now) 0.193   <- worst option available
  residue-type group average      0.070
  hybrid group-contribution       0.054   (organic only: 0.036, metals: 0.222)

So the estimate the perceiver currently emits is worse than guessing the mean,
the group-contribution model is good for organic residues, and it should not
be trusted for metal-containing ones. Findings recorded on
ehb54/ultrascan-tickets#980.

NOT pushed: this belongs with the #980 work rather than the #978 PR.

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 8d029e57b0d9941b0c763fd1919423a4b08dd6ca
      https://github.com/ehb54/ultrascan3/commit/8d029e57b0d9941b0c763fd1919423a4b08dd6ca
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/src/us_hydrodyn_perceive.cpp

  Log Message:
  -----------
  Remove the psv/molvol estimate from generated entries

The tentative entry emitted a vbar (and molvol) from a sum of vdW spheres.
Fitting a group-contribution model over the ~100 polyatomic residues
somo.residue codes, scored leave-one-out (perceiver/tools/psv_model.py),
shows that estimate is the WORST option available: mean absolute error 0.193
in vbar, against 0.110 for simply assuming the global mean. Summing spheres
ignores bond overlap and runs systematically high.

vbar enters hydrodynamics through the buoyancy term (1 - vbar*rho), so
emitting a number worse than a guess is actively misleading. Both fields are
now left at 0 = unset, and the entry says why and points at
ehb54/ultrascan-tickets#980, where the tiered replacement is tracked
(group-contribution for organic residues, MAE 0.036; residue-type group
average for out-of-domain cases such as metal-containing residues, where the
fit degrades to 0.222 - worse than that average; always user-overridable).

0 rather than omission because the header must keep its 7 fields to parse,
0 reads unambiguously as unset (the convention somo.saxs_atoms already uses
for F and SE), and it fails loudly: a 0 vbar is obviously wrong if used,
whereas a plausible-looking 0.80 is not.

The entry now reports the residue mass instead, since what decides whether an
imprecise psv matters is its mass fraction of the model - molecular vbar is
mass-weighted, so a 616 Da heme in a 50 kDa protein shifts it ~0.5% even with
a 0.34 vbar error.

Also tightens the REVIEW block to genuinely uncertain atoms: a confident
classification carrying an informational note was being listed as uncertain,
disagreeing with the flagged count callers report.

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 841df114213d8d707ead1290e4a95e7ef6b6a9a6
      https://github.com/ehb54/ultrascan3/commit/841df114213d8d707ead1290e4a95e7ef6b6a9a6
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive.h
    M us_somo/develop/src/us_hydrodyn_perceive.cpp

  Log Message:
  -----------
  Fix bead colour field in generated residue entries

emit_residue() wrote the residue's atom count into the bead line's COLOUR
field. The bead line is hydration, colour, placing_method, chain, volume
(US_Hydrodyn::read_residue_file, us_hydrodyn_load.cpp:509), so a generated
entry's colour was whatever the atom count happened to be:

  - 6 atoms  -> colour 6, RESERVED brown, which marks a bead as buried and
                EXCLUDES it from the hydrodynamic computation
  - 7 atoms  -> colour 7, reserved for fused beads
  - 8 atoms  -> colour 8, reserved for beads found exposed on re-check
  - >15      -> out of range entirely

Six- to eight-atom ligands are common (glycerol has 6, phosphate 5), so this
silently dropped beads from models built with a generated entry.

Emit DEFAULT_BEAD_COLOR instead, defined once in us_hydrodyn_perceive.h with
the documented colour list from the SOMO manual (somo_residue.html, Panel 3)
and helpers bead_color_is_reserved() / bead_color_is_selectable() for the
residue-definition UI. Default is 10 (light green), which the manual records
as the Automatic Bead Builder's colour for non-coded residues; it is a single
named constant so the policy can be changed in one place.

Also spell out the remaining bead-line fields rather than emitting bare zeros.

Refs ehb54/ultrascan-tickets#978

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 2675244c6230281cd9f6fdef71550fa5a729b0fb
      https://github.com/ehb54/ultrascan3/commit/2675244c6230281cd9f6fdef71550fa5a729b0fb
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    A us_somo/develop/include/us_hydrodyn_grid_volume.h
    M us_somo/develop/perceiver/DECISIONS.md
    A us_somo/develop/perceiver/Makefile
    M us_somo/develop/perceiver/README.md
    A us_somo/develop/perceiver/tests/grid_volume.cpp
    A us_somo/develop/perceiver/tools/calibrate_3v_context.py
    A us_somo/develop/perceiver/tools/hydration_table.py
    A us_somo/develop/perceiver/tools/psv_durchschlag.py
    A us_somo/develop/perceiver/tools/recalc_ligand_volumes.py
    A us_somo/develop/src/us_hydrodyn_grid_volume.cpp

  Log Message:
  -----------
  Native grid volume + analysis tools for vbar/molvol/hydration

Groundwork for filling the three fields a generated residue entry currently
leaves at zero: psv (vbar), anhydrous molar volume, and hydration.

Native grid volume (us_hydrodyn_grid_volume.h/.cpp, Qt-free)

  accessible A = { v : |v - c_i| > r_i + probe for every atom i }
  excluded   E = { v : v not in A, and dist(v, A) > probe }
  volume       = |E| * grid^3

probe 0 gives the bare van der Waals union, probe 1.4 the solvent-excluded
volume. Interior cavities are deliberately not counted, which is what lets
V(complex) - V(complex minus ligand) measure a bound ligand in context. Only
the accessible/blocked boundary is dilated -- the nearest accessible voxel to
any blocked voxel is always a boundary one -- so the cost is a surface rather
than a volume, and a 0.25 A grid runs in about a second for a 1300-atom
protein.

This removes what would otherwise be a new external dependency on 3V
(vossvolvox). 3V is retained only as the validation oracle: tests/grid_volume.cpp
reproduces Volume.exe to within 0.8% on an isolated residue, a whole protein,
and a bound-ligand difference, and adds analytic checks (single sphere,
coincident spheres, and a sealed hollow shell whose cavity must not be counted).
10 checks, 0 failures.

Analysis tools (prototypes, not built into libus_somo)

  psv_durchschlag.py      Durchschlag & Zipper atomic volume increments, no
                          fitting. Self-tests against the papers' own worked
                          values before reporting anything.
  hydration_table.py      derives a hybrid-type -> waters lookup from
                          somo.residue; 36 of 48 types unanimous, 7 weak and
                          flagged for review rather than silently defaulted.
  calibrate_3v_context.py calibrates the in-context difference method against
                          residues whose volume we already know.
  recalc_ligand_volumes.py recomputes prosthetic-group volumes, guarded on
                          elemental formula and on reproducing the stored value.

DECISIONS.md records the findings behind these, including why the anhydrous
volume field wants a 1.4 A probe rather than 0, and the measured-versus-
calculated trap in the Durchschlag 1986 tables.

The perceiver's hand-written test Makefile is force-added: the repository root
ignores "Makefile" for the qmake-generated ones, which left the documented
"make unit" impossible from a fresh clone.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: ababc336799317c1a31d55a185070570613eb5d7
      https://github.com/ehb54/ultrascan3/commit/ababc336799317c1a31d55a185070570613eb5d7
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/.gitignore
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/tools/calibrate_3v_context.py
    M us_somo/develop/perceiver/tools/psv_durchschlag.py
    M us_somo/develop/perceiver/tools/recalc_ligand_volumes.py

  Log Message:
  -----------
  Remove local filesystem paths from perceiver files

The repository is public, so absolute paths from a personal workspace should
not appear in it. Five places carried them, all written by me:

  .gitignore                 a home-directory path naming where the demo PDBs
                             were copied from
  DECISIONS.md               the local clone location
  psv_durchschlag.py         where the source PDFs happened to sit
  calibrate_3v_context.py    a hardcoded default path to Volume.exe
  recalc_ligand_volumes.py   the same

The two tools now resolve Volume.exe from $VOLUME_EXE, else PATH, which is
also what makes them runnable by anyone rather than only where that binary
was built. Behaviour is otherwise unchanged; self-tests still pass.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 2250593d17e20353ad36ad5207f7ff2c3a3774f8
      https://github.com/ehb54/ultrascan3/commit/2250593d17e20353ad36ad5207f7ff2c3a3774f8
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/Makefile
    A us_somo/develop/perceiver/tests/sssr.cpp
    A us_somo/develop/perceiver/tests/sssr_real.cpp
    M us_somo/develop/src/us_hydrodyn_perceive.cpp

  Log Message:
  -----------
  Add SSSR ring perception and expose the perceived bond graph

Prerequisite for the Durchschlag & Zipper volume increments, which charge a
ring-formation decrement once per ring (3-ring 2.1 through >=9-ring 14.1).
The perceiver exposed only an "aromatic" flag and never returned Bonds at all,
so ring sizes and the topology needed to classify a nitrogen or oxygen
environment were unavailable to any caller.

Bonds gains "rings", the smallest set of smallest rings. find_sssr() is
declared in the header so it can be unit-tested on hand-built adjacency with
no geometry involved. A new perceive(atoms, bonds_out, explicit_bonds)
overload hands the graph back; the existing signature delegates to it, so
current callers are unaffected.

SSSR is kept separate from find_rings(), which enumerates every simple 5- or
6-cycle and feeds aromaticity. That is the right input for "is this atom in a
flat conjugated ring" but the wrong one for anything charged per ring: a fused
bicyclic has three simple cycles and circuit rank two. Keeping them apart also
leaves the validated perception untouched, confirmed by the regression figure
holding at 99.833%.

Method is the standard one for molecule-sized graphs: smallest cycle through
each bond, then greedy smallest-first acceptance while a candidate covers a
bond no accepted ring covers, until the circuit rank is reached. Rings beyond
max_ring (12) are not sought, since the consumer charges a single large-ring
term for everything from nine up.

Tests

  sssr.cpp      35 checks on hand-built graphs: acyclic shapes, every ring size
                3 to 9, ring with substituent, fused bicyclics (naphthalene,
                indole, purine) giving two rings rather than three cycles,
                spiro, bridged, three-fused, disconnected components,
                macrocycle beyond max_ring, idempotency, and that every
                reported ring is a genuine cycle. Also checks the arithmetic
                the counts drive: one 6-ring decrement reproduces the published
                phenylalanine volume, one 5-ring the proline one.

  sssr_real.cpp 18 checks on real coordinates through the whole perception
                path, over all eight demo structures. Phe {6}, Tyr {6},
                Trp {5,6}, His {5}, Pro {5}, DA/DG {5,5,6}, DC/DT {5,6}, and
                Ala/Gly/Leu/Ser/Arg ring-free, with every instance of a type
                giving an identical signature.

Some demo structures carry physically impossible geometry, which produces
spurious small rings: 1AO6 models LYS536/NZ 0.73 A from LEU583/CG, shorter
than any covalent bond, and 6LYZ has two oxygens 1.22 A apart. This yields 13
cross-residue and 12 three-membered rings across the demo set, none of them
inside a residue, so no residue's own ring count is affected. The tests assert
this as such: a cross-residue ring must be closed by an inter-residue bond that
is neither a backbone link nor a disulfide, and no three-membered ring may lie
within a single residue.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: d57209e20a1928264c0779a782496c0c1362174e
      https://github.com/ehb54/ultrascan3/commit/d57209e20a1928264c0779a782496c0c1362174e
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    A us_somo/develop/include/us_hydrodyn_psv.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/Makefile
    A us_somo/develop/perceiver/tests/psv.cpp
    A us_somo/develop/src/us_hydrodyn_psv.cpp

  Log Message:
  -----------
  Add the Durchschlag & Zipper psv engine

Computes partial specific volume from published atomic volume increments,
consuming the perceived atoms and the bond/ring graph added in the previous
commit. Returns molar volume, vbar and a labelled decomposition -- atomic sum,
covolume, ring decrement, electrostriction, ring and charge counts -- so a
value can be audited rather than taken on trust. Atoms whose environment
cannot be classified are reported for review rather than guessed.

The covolume defaults off, since a residue is a monomeric unit and SOMO
already adds one structure-level covolume in calc_vbar_updated; a flag turns
it on for a standalone molecule. Only rings lying entirely inside the residue
are charged, which is both correct chemistry and immunity to the spurious
cross-residue rings that clashing coordinates produce.

Two rules a naive implementation gets wrong, both verified against the
published tables:

  The hydroxyl increment is topological, not a running count. A second
  NEIGHBOURING hydroxyl takes 0.4 but an isolated one is a fresh 2.3, so
  hydroxyls are clustered by whether their carrier atoms are bonded.
  1,2-ethanediol comes out at 53.5 and 1,8-octanediol at 152.0, exactly the
  published calculated volumes; a running count reproduces neither.

  Guanidinium charges only the terminal nitrogens. Table 1's footnote limits
  the 8.0 increment "in Arg only to the two terminal N", so arginine's NE
  takes the amine value.

Tests reproduce urea 44.2, glycerol 70.0 and all seven published group
increments exactly, and give mean absolute error of 1.64% against the stored
somo.residue vbar over the fifteen charge-neutral amino acids, none worse
than 6%.

Arginine and histidine sit outside that: the perceiver assigns them no formal
charge while somo.residue stores them protonated, so they lose an
electrostriction term. Arginine is essentially always protonated at
physiological pH, so that is a perception gap for the deferred pH layer rather
than ambiguous chemistry.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: a7ebb69d84899b7aeee07fb19c44612137949af3
      https://github.com/ehb54/ultrascan3/commit/a7ebb69d84899b7aeee07fb19c44612137949af3
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    A us_somo/develop/include/us_hydrodyn_hydration.h
    M us_somo/develop/include/us_hydrodyn_perceive.h
    A us_somo/develop/include/us_hydrodyn_residue_builder.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/Makefile
    A us_somo/develop/perceiver/tests/builder.cpp
    M us_somo/develop/perceiver/tests/emit_residue.cpp
    A us_somo/develop/perceiver/tests/hydration.cpp
    A us_somo/develop/src/us_hydrodyn_hydration.cpp
    M us_somo/develop/src/us_hydrodyn_perceive.cpp
    A us_somo/develop/src/us_hydrodyn_residue_builder.cpp

  Log Message:
  -----------
  Fill in psv, anhydrous volume and hydration for generated entries

A generated somo.residue entry left vbar, molvol and hydration at zero. This
computes all three and emits a complete entry, with anything still unset
stated as "NOT SET" in the header so a partly filled entry always looks
partly filled.

Hydration lookup (us_hydrodyn_hydration.h/.cpp)

  Derived at run time from whatever somo.residue is loaded rather than
  embedded, so it cannot drift from the user's own tables. 1581 observations
  give 48 hybrid types, 41 of them confident and 7 flagged; flagged types are
  surfaced for review rather than silently defaulted, and ties break toward
  the lower water count, since under-hydrating a proposal the user will edit
  is safer than over-hydrating one they may accept unchanged.

  The per-atom values in somo.residue are a hand distribution of a
  per-residue Kuntz total rather than a per-atom rule, so this is a starting
  point for editing and says so. The residue total is the quantity with
  literature backing; tests confirm the totals reproduce Kuntz for 14 of 14
  applicable residues, with Asp and Glu deliberately low because SOMO stores
  them protonated.

  One test records a gap Mattia suspected: every nucleotide entry carries
  zero hydration. It is asserted as the current state, so it turns red the day
  someone fills those in, which is when models built on the old values need
  revisiting.

Orchestration (us_hydrodyn_residue_builder.h/.cpp)

  perceive, then psv, volume and hydration, then emit. emit_residue takes an
  optional Properties argument rather than including the new headers, which
  include it in turn.

  A generated tryptophan entry now gives vbar 0.753 against a stored 0.738,
  molvol 224.29 against 228.2, and hydration 2.0 against 2.0. Over twelve
  residues treated as unknown, computed molvol is within 2.32% of the stored
  values on average and never worse than 4.6%.

The volume convention factor is specific to the radius set. The 1.204/1.131
measured previously came from 3V's default radii; this pipeline uses SOMO's
own, which are smaller, so the right factors are 1.248 and 1.179. The factor
tracks polarity as much as size, hydrophobics sitting near 1.26 to 1.29 and
polar residues near 1.16 to 1.21, so the band split is placed below the
hydrophobic cluster rather than at the midpoint, where it separated leucine
from isoleucine and cost both 8%.

Also corrects a comment that described the residue header's ASA field as a
molecular weight column.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: ed8bd4ef922e8eaac8468328751d81193a49cc4e
      https://github.com/ehb54/ultrascan3/commit/ed8bd4ef922e8eaac8468328751d81193a49cc4e
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive_somo.h
    M us_somo/develop/libus_somo.pro
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/src/us_hydrodyn_perceive_somo.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  Wire the computed properties into SOMO and add a headless perceive

libus_somo.pro did not list the four sources added since the perceiver work
resumed, so the grid volume, psv engine, hydration lookup and residue builder
were never compiled into the library at all. Registered, and the symbols are
confirmed present in the built dylib.

The Qt adapter now takes the bond graph from the new perceive overload and
goes through somo_residue_builder, so a proposal made inside SOMO carries
vbar, molvol and hydration rather than zeros. Tentative gained those three
fields for callers that want the numbers instead of the text block. The
hydration lookup is built from SOMO's own loaded residue_list, so a proposal
is always consistent with the user's tables rather than a snapshot compiled
into the binary.

"perceive auto <pdb>" is the headless sub-command: every default accepted,
nothing prompted, nothing written back to somo.residue. It also prints how
many atoms were flagged, since in a pipeline nobody is watching the REVIEW
block go past.

Defaults are pinnable through gparams in the same way as covolume:
perceive_psv, perceive_volume, perceive_hydration, perceive_volume_probe,
perceive_volume_grid and perceive_bead_color. A reserved bead colour is a
hard error rather than a silent substitution, because two of the reserved
values exclude a bead from the hydrodynamic computation and accepting one
would quietly produce a model missing beads.

Verified end to end through us3_somo -g on 2CMD, whose citrate is genuinely
not coded: vbar 0.542, molvol 186.56, and seven review flags. On the residue
basis SOMO stores, that vbar corresponds to 0.607 for the free molecule
against a literature value near 0.59 to 0.62. The proposed hydration of zero
for a triacid is plainly too low, and all four hydroxyl oxygens come back
flagged at 49% agreement, which is the intended behaviour: propose, then say
what cannot be supported.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: be8b39e00969c8d0e63b51fed22cce94a8f5d101
      https://github.com/ehb54/ultrascan3/commit/be8b39e00969c8d0e63b51fed22cce94a8f5d101
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive_somo.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/src/us_hydrodyn_perceive_somo.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  Add perceive validate: predict the coded residues as if unknown

Rebuilds every residue somo.residue does code, pretending it is unknown, and
reports computed vbar, molvol and hydration against the stored values with a
summary. The same idea as perceive compare, which does this for atom typing,
but for the computed properties, and it exercises the whole pipeline
including perception. That makes it the only end-to-end measure of accuracy.
Chain termini and residues with unmodelled atoms are counted and skipped
rather than quietly averaged in.

Over seven structures and 1157 rebuilt residue instances, ordinary amino
acids give mean absolute errors of about 2.7% on vbar and 2.1% on molvol,
matching what the standalone harness claimed.

The ligand rows are the interesting ones. Heme's computed volume is 32.6%
above the stored value and NAD's is 28.1% above. Earlier work in this branch
reached the same conclusion by a completely different route, using 3V both on
the isolated molecule and by difference in context, and arrived at comparable
numbers. Two independent methods agree the stored ligand volumes are around
30% low. NAD's computed vbar of 0.619 is also closer to the measured 0.620
than the stored 0.599 is.

Heme's vbar comes out 24% low, which is the expected failure: it is a
metalloporphyrin, Durchschlag and Zipper publish no increment for iron, and
the engine reports the atom for review rather than inventing a value.

Hydration is much the weakest of the three, around half the residues within
half a water and systematically low, the misses being the polar and charged
side chains. A per-type majority cannot express residue-specific hydration.
This also confirms the pH coupling from the runtime side: residue_list
carries pH-adjusted hydration, so aspartate reads 6.0 and glutamate 7.0,
exactly the charged-carboxylate values, where the raw file fields give 1.0.

The lookup counts one vote per residue name, since residue_list holds several
entries for residues with ionization variants and counting each weights them
more heavily purely for being pH-aware, which was enough to flip a borderline
type and cost serine and threonine a water each.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: a03c3e94da895de757342a2f0b3b1cd8d6ff759a
      https://github.com/ehb54/ultrascan3/commit/a03c3e94da895de757342a2f0b3b1cd8d6ff759a
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn.h
    A us_somo/develop/include/us_hydrodyn_perceive_dialog.h
    M us_somo/develop/libus_somo.pro
    M us_somo/develop/perceiver/DECISIONS.md
    A us_somo/develop/perceiver/run_dialog_test.sh
    A us_somo/develop/perceiver/tests/dialog_qt.cpp
    M us_somo/develop/src/us_hydrodyn.cpp
    A us_somo/develop/src/us_hydrodyn_perceive_dialog.cpp

  Log Message:
  -----------
  Add a GUI review dialog for non-coded residues

Everything built so far was reachable only from gui_script. A user working in
SOMO saw none of it, and a non-coded residue still fell silently to the
Automatic Bead Builder's averaged generic bead. This makes it visible and
correctable.

US_Hydrodyn_Perceive_Dialog presents one modal review per non-coded residue,
in the order Mattia laid out: view the residue in RasMol, confirm the
perceived atom table, edit the proposed per-atom hydration, check the
anhydrous volume, see the bead count and position, choose a bead colour, and
check the psv, with the somo.residue entry rebuilt live below so what is on
screen is exactly what Accept returns.

Only hydration is editable in the atom table. Name, hybrid, mass and radius
are perceived, and editing them would desynchronise the entry from the
geometry it came from. Hydration is editable because it is the weakest of the
three computed numbers; the residue total is shown beneath it and updates
live, with a note that the total is the quantity with literature backing while
the per-atom split is convention.

The colour combo is built from the manual's table and shows index, name and
meaning. The four reserved colours are never offered, since two of them mark a
bead for exclusion from the hydrodynamic computation and the other two are
assigned automatically during model generation.

Appending to somo.residue is opt-in and off by default, so accepting an entry
is per-session unless the user asks otherwise. After a save the user is told
to reload the structure, since hot-reloading the residue table would
invalidate indices the loaded model already holds.

The trigger is a Lookup Tables menu item rather than a hook inside model
building: check_for_missing_atoms runs per model and inside batch paths, so
opening a modal dialog from there would be invasive and wrong for scripted
runs. Skip leaves the residue to the Automatic Bead Builder exactly as before.

Testing a widget needs a widget test: run_dialog_test.sh builds the dialog
against the real library and exercises it under an offscreen Qt platform, so
it needs no display. Eighteen checks cover the proposal round-tripping through
parse and rebuild, a hydration edit propagating to both the atom line and the
bead total, a nonsense edit being rejected and the previous value restored,
read-only columns actually being read-only, the colour combo offering twelve
of the sixteen colours and defaulting to the configured one, and nothing being
marked accepted or saved until the user acts.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 075e7bb57adec796a30a714b7b8e5f8594e39668
      https://github.com/ehb54/ultrascan3/commit/075e7bb57adec796a30a714b7b8e5f8594e39668
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_hydration.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/tests/builder.cpp
    M us_somo/develop/src/us_hydrodyn_hydration.cpp
    M us_somo/develop/src/us_hydrodyn_perceive_somo.cpp
    M us_somo/develop/src/us_hydrodyn_residue_builder.cpp

  Log Message:
  -----------
  Propose hydration from pH 7 chemistry rules rather than type averages

Mattia supplied the chemistry: at pH 7 an acidic side chain is a carboxylate,
with one oxygen double-bonded and neutral and the other deprotonated and
charged; an aliphatic amine is protonated; and the water a carboxylate carries
depends on the length of the aliphatic chain linking it to the backbone, which
is what separates aspartate from glutamate. No general pH machinery, just the
pH 7 assumption.

The residue table confirms this exactly. The ionised hydration is field 15 of
an atom line, not 13 as the layout first suggests, and reading it gives
aspartate 5 waters on its carboxylate against glutamate's 6, lysine 3 on its
ammonium, arginine 1 on each terminal guanidinium nitrogen. So aspartate's
runtime total of 6 and glutamate's 7 are carboxylate plus backbone amide, and
the difference between them really is the extra methylene.

The rules replace the previous per-hybrid-type majority vote, which could not
express residue-specific hydration and systematically under-predicted the
polar and charged side chains. Anything the rules do not recognise still falls
back to the observed averages and is flagged, so a novel group never receives
a confident-looking number by accident.

Hydration within half a water improves from roughly 69 of 145 residue types to
136 of 145 across seven structures, 48% to 94%, with lysozyme and both
ribonucleases now exact on every type. All fifteen applicable coded residues
reproduce exactly in the unit tests, including the aspartate/glutamate
methylene difference. Partial specific volume and anhydrous volume are
unchanged, and perception still scores 99.833%.

Validation also has to skip chain termini at both ends, not just the carboxy
end. An amino-terminal residue has a free protonated backbone nitrogen rather
than an amide, so an amino-terminal lysine genuinely carries two ammonium
groups and six waters where the tabulated internal residue has four. The rules
were right and the comparison was wrong.

Two rule edges the coded residues exposed: a disulfide sulfur has two heavy
neighbours just as a thioether does, so both partners must be carbon before it
counts as hydrated; and proline's tertiary ring amide nitrogen carries no
hydrogen, so amide nitrogens are identified by their carbonyl neighbour rather
than by hydrogen count.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 3e3ff3ef9dda71c5a58c647b7181844e383010cf
      https://github.com/ehb54/ultrascan3/commit/3e3ff3ef9dda71c5a58c647b7181844e383010cf
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_psv.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/tests/psv.cpp
    M us_somo/develop/src/us_hydrodyn_psv.cpp

  Log Message:
  -----------
  Measure pH 7 ionisation for psv, and leave it off

The pH 7 assumption transformed hydration, so the same reasoning was applied
to partial specific volume: charge the carboxyl as a carboxylate, protonate
the guanidinium, and take an electrostriction term for each. Implemented as an
option and measured against the stored values.

The measurement says not to use it. Aspartate moves from 1.3% low to 11.0%
low, glutamate from 1.2% to 9.0%, while arginine improves from 15.9% high to
9.7%. Mean absolute error goes from 6.23% to 9.05%: a clear net loss.

The reason the symmetry with hydration breaks is visible in the residue table.
It lists the same vbar for the protonated and deprotonated state of every
ionisable residue, so ionisation there changes mass, radius, protons and
hydration but never volume. Those values descend from densitometry on real
amino acids at neutral pH and already contain whatever ionisation was present,
whereas the Durchschlag and Zipper electrostriction is an explicit correction
to a neutral-basis increment sum. Applying both counts it twice.

Hydration was different precisely because the table does carry two hydration
numbers per ionisable atom, with the pH 7 value in its own field. There the
assumption had a ground truth to match, and matching it took hydration from
48% to 94% of residue types within half a water.

The option stays, off by default, with the measured numbers in the header
comment and a test that asserts the default is whichever setting actually
measures better. If the reference values are ever placed on a single stated
ionisation basis, flipping it will be a one-line change and the test will
report whether it helped.

This does not explain arginine, which is still 15.9% high untouched and 9.7%
high with electrostriction. It is the known outlier of the scheme: the six
published residue-volume sets in Perkins 1986 disagree with each other about
arginine by 17%.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: e58e91fb8be1ca948a1a36262b98f4874c3dc79d
      https://github.com/ehb54/ultrascan3/commit/e58e91fb8be1ca948a1a36262b98f4874c3dc79d
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/DECISIONS.md

  Log Message:
  -----------
  Correct the record on the vbar ionisation design

An earlier note inferred that the identical vbar for the protonated and
deprotonated states meant the stored values already absorbed whatever
ionisation the underlying measurements carried. Mattia has corrected this: the
amino acid vbar values were recalculated by him at pH 7, and setting the
ionised value equal to the base is a deliberate statement that at pH 7 the
volume does not change.

There is also no stored "pH 7 value" anywhere. SOMO derives the species
fractions at run time from the stored pKa by Henderson-Hasselbalch, in
basic_fractions(), and mixes the base and ionised values by those fractions.
For aspartate at pH 7 that is 99.95% deprotonated, which is why the runtime
hydration is effectively the fully ionised value. The field described earlier
as "the pH 7 one" is the fully ionised one; pH 7 merely sits almost entirely
at that end.

This also reframes arginine. Its 0.698 is Mattia's recalculated pH 7 value,
which he states is better than the published sets, so the calculation being
16% high means it is reproducing a published-style number while the reference
is deliberately better than those, rather than the reference being uncertain.

The decision to leave the electrostriction option off is unchanged, and so is
the measurement behind it. Only the explanation changes: the correction
overshoots relative to a recalculated pH 7 reference.

What this confirms is that the implemented design is the intended one. Work at
pH 7 for a non-coded residue, since its pKa cannot be known, and assume the
hydrations determined for the amino acids, every carboxyl deprotonated and
every amine protonated. That is what the hydration rules already do.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 8cf6eebf22254cd8f7415d4d86993acf5f74f863
      https://github.com/ehb54/ultrascan3/commit/8cf6eebf22254cd8f7415d4d86993acf5f74f863
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_psv.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/perceiver/tests/psv.cpp
    M us_somo/develop/src/us_hydrodyn_psv.cpp

  Log Message:
  -----------
  Implement the published metal and ion volumes

The engine reported "no volume increment for this element" for every metal.
That was wrong: Durchschlag and Zipper tabulate them, and the values had
already been extracted into the Python prototype and then never carried across
to the C++ implementation.

Table 2 supplies atomic volumes for a metal bonded into a complex, such as a
haem iron or B12's cobalt. Table 3 supplies aqueous volumes for free monatomic
ions, including iron at -32.3 and -55.1 for the divalent and trivalent forms.
Those ion values already contain the ionisation contribution, so
electrostriction is not charged a second time on top of them, and a test
asserts an isolated magnesium comes out at exactly its tabulated value with no
electrostriction term.

The trivalent iron value of -55.1 is -0.99 mL/g, the same sign and order as the
figure quoted from another compilation. Negative because electrostriction
draws water inward around the ion more than the bare ion displaces.

The effect on haem is real but small: its partial specific volume moves from
24.3% low to 23.0% low. Iron is about a percent of a 616 dalton molecule, so it
was never going to close that gap. The bulk of the error is most likely the
porphyrin itself, four fused pyrrole rings and a heavily conjugated macrocycle,
which is not what Traube-style additivity was built for.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 1f7098121c8a7fb7aa6d5006c21517740378de96
      https://github.com/ehb54/ultrascan3/commit/1f7098121c8a7fb7aa6d5006c21517740378de96
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive_dialog.h
    M us_somo/develop/perceiver/DECISIONS.md
    M us_somo/develop/src/us_hydrodyn.cpp
    M us_somo/develop/src/us_hydrodyn_perceive_dialog.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  Fix cross-structure state leak and add Skip all remaining

Two defects found while preparing structures for GUI testing, both visible
only when perceive is run over more than the demo set.

Running perceive twice in one session reported the previous structure's
non-coded residues against the new one. Ubiquitin alone has none, but after
malate dehydrogenase it reported two, which were citrate's. SOMO fills
unknown_residues during model building and never clears it, so the set
accumulates across loads. The emitted entries were still right, since a stale
name absent from the new model produces nothing, but the reported counts
described a structure that was no longer open. Both the script command and the
GUI slot now intersect that set with the residue names actually present in the
loaded model.

The second is a usability failure with real consequences. 5PTI is a neutron
structure carrying heavy water and explicit deuteriums, so its residues have
more atoms than the table's entries and SOMO matches none of them: fifty-five
non-coded instances across eighteen types, which meant eighteen modal dialogs
in succession with no way out but to dismiss each one. The dialog now offers
Skip all remaining, which abandons the review and leaves everything to the
Automatic Bead Builder, with a tooltip naming the likely cause so a user
seeing an implausible count suspects the file rather than the tool. The caller
honours it and reports how many residues went unreviewed.

Worth noting that the perceiver itself handles these structures correctly, as
it excludes hydrogens from bond perception by design. It is SOMO's residue
matching that fails on the atom count. Making that match hydrogen-agnostic
would be the real fix.

Refs ehb54/ultrascan-tickets#980

Co-Authored-By: Claude Opus 4.8 <noreply at anthropic.com>


  Commit: 07fd46358b9ae949905ae9841142d28827b848d2
      https://github.com/ehb54/ultrascan3/commit/07fd46358b9ae949905ae9841142d28827b848d2
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/.gitignore
    M us_somo/develop/perceiver/README.md
    A us_somo/develop/perceiver/data/non-coded/1MBO.pdb
    A us_somo/develop/perceiver/data/non-coded/2CMD.pdb
    A us_somo/develop/perceiver/data/non-coded/3PTB.pdb
    A us_somo/develop/perceiver/data/non-coded/5PTI.pdb
    A us_somo/develop/perceiver/data/non-coded/README.md
    M us_somo/develop/perceiver/examples/perceive.somo

  Log Message:
  -----------
  perceiver: add tracked test structures containing non-coded residues

The demo structures under perceiver/data/ are all fully coded, so nothing in
the tree exercised the path the perceiver exists for. Add four structures that
do, tracked in git since they are copies of nothing else here (the parent
data/*.pdb ignore rule is deliberately non-recursive).

  1MBO  SO4 only -- heme and O2 are coded, so it also shows selectivity
  2CMD  citrate, 13 atoms, 3 flagged for review -- the clean first test
  3PTB  benzamidine -- the only case reaching SSSR/ring-decrement and
        electrostriction; its Ca2+ is coded and correctly left alone
  5PTI  neutron/deuterated -- 50 instances across 18 types, the stress case
        for "Skip all remaining" (residue matching fails on atom count, not
        chemistry: the perceiver excludes hydrogens by design)

data/non-coded/README.md records the expected scan output for each so a
regression is visible without rerunning everything.

Also correct the example gui_script: paths must be absolute, since SOMO
changes directory during startup and a shell-relative path fails to resolve.


  Commit: 787f8d6e747b396b0d8b0c3e032c5e5f6a811899
      https://github.com/ehb54/ultrascan3/commit/787f8d6e747b396b0d8b0c3e032c5e5f6a811899
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/perceiver/tests/dialog_qt.cpp
    M us_somo/develop/src/us_hydrodyn.cpp
    M us_somo/develop/src/us_hydrodyn_perceive_dialog.cpp

  Log Message:
  -----------
  perceiver: fix the review dialog never appearing from the GUI menu

Lookup Tables -> Perceive Non-Coded Residues... appeared to do nothing:
the dialog was constructed as a CHILD WIDGET of the main window, not as a
window. A QFrame given a parent and no window flag is an ordinary child, so
setWindowTitle and setWindowModality were silently ignored and the
setGeometry( x, y, 0, 0 ) idiom the other 116 SOMO dialogs use -- which a
window manager expands to the layout minimum -- left it 0 x 0, positioned by
a screen-coordinate global inside (and clipped by) the parent. The caller
then waited on isVisible(), which for a child never goes false, so SOMO sat
in the wait loop with nothing on screen.

Measured, with the dialog built the way the slot builds it:
  before   isWindow 0,    0 x 0
  after    isWindow 1, 1154 x 718

Every other SOMO dialog avoids this by passing no parent at all. Keeping the
parent and adding Qt::Window is better on macOS: the dialog then also stays
stacked above its owner. Adds fixWinButtons/raise/activateWindow to match
what the other dialogs do on show.

Also make the slot incapable of failing silently again: every exit path now
writes to the text window, including both "nothing to do" cases, and the
message boxes get a parent so they cannot open behind the main window.

dialog_qt.cpp gained the check that catches this -- it constructed the dialog
with no parent, i.e. exactly the case that works. Building it with a parent
and asserting isWindow() plus a non-degenerate size after show() reproduces
the failure offscreen (3 failures on the old code, 21/21 on the new).


  Commit: 6b28f03e1a21f13b0ef87ed6c6fc08c7631569a8
      https://github.com/ehb54/ultrascan3/commit/6b28f03e1a21f13b0ef87ed6c6fc08c7631569a8
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn.h
    M us_somo/develop/include/us_hydrodyn_perceive.h
    M us_somo/develop/perceiver/README.md
    M us_somo/develop/perceiver/examples/perceive.somo
    M us_somo/develop/perceiver/tests/builder.cpp
    M us_somo/develop/src/us_hydrodyn.cpp
    M us_somo/develop/src/us_hydrodyn_perceive.cpp
    M us_somo/develop/src/us_hydrodyn_script.cpp

  Log Message:
  -----------
  perceiver: make an accepted entry actually take effect

Accepting an entry in the review dialog only printed it. The bead builder
still saw a non-coded residue and fell back to a generic averaged bead --
the outcome this feature exists to prevent. Three separate defects, each of
which alone was enough to break it:

1. ASA was emitted as 0. Every SOMO residue loader -- read_residue_file
   (us_hydrodyn_load.cpp:534), US_AddResidue (:1288), US_Saxs_Util (:693) --
   skips a record whose ASA is zero, and skips it SILENTLY. The entry parsed,
   never entered residue_list or multi_residue_map, and the residue went on
   being reported non-coded with nothing to indicate why. All 127 coded
   residues have ASA > 0. Emit 18 A^2/atom, the coded-residue mean, labelled
   a placeholder in the REVIEW block: nothing computes with the tabulated
   residue ASA (it appears only in info dumps), but it may not be zero.
   Properties::asa lets a caller supply a measured value later.

2. The block was not a valid record. The table is strictly positional -- one
   free-text comment line, then header, atoms, beads -- and has no comment
   syntax. The emitted block's ~20 '#' lines were consumed two at a time as
   comment/header pairs, silently creating zero-atom residues named "#".
   Ticking "add to somo.residue" would have written that into the user's own
   table. perceived_table_record() collapses the block to one comment line.

3. Nothing applied the entry. apply_perceived_entries() puts accepted entries
   into a session overlay (<table>.perceived), re-runs read_residue_file on
   it and re-reads the structure, so the atoms rebind. The user's table is
   written only when they tick the box. Novel hybrid rows, previously dropped
   on the floor, are injected into hybrid_to_protons/electrons first, since
   read_residue_file rejects atoms whose hybrid it cannot resolve.

Adds "perceive apply <pdb>" -- the headless equivalent of accepting every
entry -- reporting the non-coded count after the fact. That assertion is what
found (1) and (2); the GUI path had no way to tell "file written" from
"entry in effect". builder.cpp now checks the emitted ASA is positive.

Verified: 1MBO SO4 and 2CMD CIT both apply, 0 types still non-coded, and a
fresh perceive of 1MBO reports every residue already coded. Suites green
(54/35/18/60/5/37/55/21), regression 99.833%.


  Commit: 99f7c5872cf9ba1208afb9ebccad691c291ac1d7
      https://github.com/ehb54/ultrascan3/commit/99f7c5872cf9ba1208afb9ebccad691c291ac1d7
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn_perceive_dialog.h
    M us_somo/develop/perceiver/tests/dialog_qt.cpp
    M us_somo/develop/src/us_hydrodyn_perceive_dialog.cpp

  Log Message:
  -----------
  perceiver: stop the review dialog overriding the emitted ASA

The dialog does not write the perceiver's block -- refresh_entry() rebuilds
the whole entry from its widgets, and its header format string hardcoded the
ASA slot to 0:

    s += QString( "%1\t0\t%2\t0\t%3\t1\t%4\n" )

so fixing the emitter changed nothing for anyone using the GUI. A run shows
both the new REVIEW line about the ASA placeholder AND a header reading
"SO4 0 61.75 0 5 1 0.390" -- one binary, two emitters, and the loaders drop a
zero-ASA record silently, so the residue stayed non-coded after acceptance.

"perceive apply" could not catch this: it uses the Tentative block directly
and never constructs a dialog, so it exercised the half that was already
right. The dialog now parses type and ASA out of the proposed header and
writes them back untouched, editing only what it owns -- molvol, vbar,
hydration and colour. Anything it hardcodes silently overrides perception,
so it should hold nothing it does not edit.

The dialog test could not catch it either: its fixture block carried ASA 0,
so any assertion would have passed vacuously. The fixture now carries 54.00
and the test parses the rebuilt header and asserts the ASA field is positive
and still 54.0, alongside molvol -- the two fields the loaders gate on.

25 checks, 0 failures; Qt-free suites unchanged (54/35/18/60/37/55).


  Commit: 58f0a23a65d2e1347d6b08707bcb6feeba16da05
      https://github.com/ehb54/ultrascan3/commit/58f0a23a65d2e1347d6b08707bcb6feeba16da05
  Author: ehb54 <brookes at uthscsa.edu>
  Date:   2026-08-09 (Sun, 09 Aug 2026)

  Changed paths:
    M us_somo/develop/include/us_hydrodyn.h
    M us_somo/develop/src/us_hydrodyn_beads.cpp
    M us_somo/develop/src/us_hydrodyn_core.cpp
    M us_somo/develop/src/us_hydrodyn_fractal_dimension.cpp
    M us_somo/develop/src/us_hydrodyn_load.cpp
    M us_somo/develop/src/us_hydrodyn_saxs_1d.cpp
    M us_somo/develop/src/us_hydrodyn_saxs_2d.cpp
    M us_somo/develop/src/us_hydrodyn_saxs_iqq.cpp

  Log Message:
  -----------
  somo: derive an atom-table entry instead of dropping the atom

somo.atom is keyed by ATOM NAME, so an atom whose name that table has never
seen resolves to nothing and is dropped from both the ASA and the excluded
volume -- reported once, then ignored. That is routine for a non-coded
ligand: 1MBO's sulfate sulfur is named "S", and the table carries only the
Fe-S cluster names S1..S4B.

    Chain A Molecule 2 Atom S Residue SO4 154 Hybrid S name missing from
    Atom file. Atom skipped for SAS.

ensure_atom_entry() derives one instead. It prefers another atom with the
SAME HYBRID -- which fixes element, mass and radius exactly, leaving only the
excluded volume a convention -- and falls back to the same ELEMENT. The
derived entry goes into the map, so it is also the memo: the next lookup is
an ordinary hit.

Excluded volume takes the value that key uses MOST OFTEN in the table rather
than whichever row is found first, because it is not a function of the
hybrid: C4H3 appears with both 31.89 and 16.44 A^3 depending on atom name.
For sulfur the question does not arise -- every S row is 19.86.

It reports what it did, once per atom name. A silent substitution here would
be the same class of defect as the silent skip it replaces:

    Note: S (S) is not in the atom table; using the hybrid match:
    mw 32.065, radius 0.43, excluded volume 19.86 A^3

Applied at all six lookup sites, not only the two a bead build reaches: bead
building, SAS, fractal dimension, and the SAXS 1D/2D/I(q) paths.

Verified on 1MBO: the fallback fires once for S, nothing is skipped, and the
perceived SO4 entry still applies cleanly (0 types still non-coded).


Compare: https://github.com/ehb54/ultrascan3/compare/6fbfe7a46324...58f0a23a65d2

To unsubscribe from these emails, change your notification settings at https://github.com/ehb54/ultrascan3/settings/notifications


More information about the us-commits mailing list