[us-commits] [ehb54/ultrascan3] ecba18: Native grid volume + analysis tools for vbar/molvo...

emre brookes noreply at github.com
Sun Aug 9 03:17:35 MDT 2026


  Branch: refs/heads/ehb54-issue-980
  Home:   https://github.com/ehb54/ultrascan3
  Commit: ecba184b1b6189b864121920b4efbb4aeb9b3436
      https://github.com/ehb54/ultrascan3/commit/ecba184b1b6189b864121920b4efbb4aeb9b3436
  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: a35936116aee7060cb68b428298720dddec2f781
      https://github.com/ehb54/ultrascan3/commit/a35936116aee7060cb68b428298720dddec2f781
  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: 97dedff289e10e0775e71a2d4c3d52f7ae89191c
      https://github.com/ehb54/ultrascan3/commit/97dedff289e10e0775e71a2d4c3d52f7ae89191c
  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: 00eb18db526d3bbb860d815906a7173193a8e2d1
      https://github.com/ehb54/ultrascan3/commit/00eb18db526d3bbb860d815906a7173193a8e2d1
  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: 810224b2cfb9eb88ecb7a7693f054aaf65f1cd30
      https://github.com/ehb54/ultrascan3/commit/810224b2cfb9eb88ecb7a7693f054aaf65f1cd30
  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: 874511dd96e87ebdf47acc03ced1dca297ab8e05
      https://github.com/ehb54/ultrascan3/commit/874511dd96e87ebdf47acc03ced1dca297ab8e05
  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: b3fc547240bfad78f92c45ca2802d458da7c5bae
      https://github.com/ehb54/ultrascan3/commit/b3fc547240bfad78f92c45ca2802d458da7c5bae
  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: c658d5f044454a6c22701513f7d44442fab685ec
      https://github.com/ehb54/ultrascan3/commit/c658d5f044454a6c22701513f7d44442fab685ec
  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: 5dd36b48be217e9a426832c8d28abdc9ac461f13
      https://github.com/ehb54/ultrascan3/commit/5dd36b48be217e9a426832c8d28abdc9ac461f13
  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: 6ca18cd20485b5794a5aad2397dcaa6795c09922
      https://github.com/ehb54/ultrascan3/commit/6ca18cd20485b5794a5aad2397dcaa6795c09922
  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: e80635bca93e1014ead951591be952fe957bf436
      https://github.com/ehb54/ultrascan3/commit/e80635bca93e1014ead951591be952fe957bf436
  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: c3c04c9db7113201ba37ec65a5e0dca8a954182e
      https://github.com/ehb54/ultrascan3/commit/c3c04c9db7113201ba37ec65a5e0dca8a954182e
  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>


Compare: https://github.com/ehb54/ultrascan3/compare/3f27d1bad3fe...c3c04c9db711

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