Skip to content

feat: add ΔCharge (pH 7.4 → 6.0) metric + R9 instability-floor info banner - #5

Merged
PaulNewling merged 7 commits into
mainfrom
paulnewling/MILAB-6140_delta-charge
May 5, 2026
Merged

PaulNewling merged 7 commits into
mainfrom
paulnewling/MILAB-6140_delta-charge

Conversation

@PaulNewling

Copy link
Copy Markdown
Collaborator

ΔCharge metric (pH 7.4 → 6.0)

New pl7.app/chargeShift PColumn at three scopes — peptide, CDR3 (per chain), and Fv. Captures pH-switching capacity (FcRn recycling, endosomal release): negative values mean the molecule gains positive charge on acidification, the productive direction for histidine-driven switching. Histidine dominates the metric (~−0.46 per His; pKa ~6.0 sits in the window).

Domain carries the pH endpoints (pl7.app/pH/from: "7.4", pl7.app/pH/to: "6.0") so additional pH pairs (tumor microenvironment ~6.5, lysosomal ~5.0) can land later without breaking the v1 column identity. Default-visible alongside the static charge column at each scope; not marked isScore (interpretive metric, not a Lead Selection ranking criterion). VH/VL chain-level ΔCharge is out of scope per spec — framework histidines dilute the binding-relevant signal.

Test coverage

  • test_properties.py::TestChargeShift — 6 behavior-focused tests: identity vs two-point subtraction (5 sequences), no-titrator-in-window magnitude bound, His dose-response, NA propagation, anti-symmetry of pH endpoints, Cys-include vs exclude.
  • test_properties.py::TestFvChargeShift — additivity across paired chains, NA propagation.
  • test_m3_validation.py::TestChargeShiftManualHH — 7 CDR-H3 sequences vs an independent pure-Python HH reference (catches BioPython formula drift).
  • test_m3_validation.py::TestFvChargeShiftAdditiveIdentity — 2 paired chains, additivity + manual two-point cross-check.
  • software/tests/data/corpus/manifest.json — chargeShift expectations on every existing peptide / antibody / TCR fixture; TCR's expected_columns_absent extended with chargeShift_Fv.

200 python tests pass; 99% src/ coverage.

R9 instability-floor info banner

Peptide mode emits a block-level info message whenever any peptide falls below the Instability Index length floor. Mirrors the per-row gate inside instability_index so the banner gates on the same condition that produces NA cells. New messages.peptidesShortInstability() helper; new hasPeptideBelowInstabilityFloor flag in run_peptide stats; new banner branch in process.tpl.tengo.

Tooltip polish

ΔCharge description tightened from spec R6b verbatim text (~110 words → ~70). Lead-with-the-rule, parallel small/large structure, dropped filler. Substance unchanged.

Captures pH-switching capacity (FcRn recycling, endosomal release).
Negative values mean the molecule gains positive charge on acidification
— the productive direction for histidine-driven pH switching.

Software: charge_shift / fv_charge_shift in properties.py;
chargeShift_peptide / chargeShift_<chain>_CDR3 / chargeShift_Fv in
pipeline.py TSV emission. VH/VL chain-level ΔCharge omitted per spec —
framework histidines dilute the binding-relevant signal.

Workflow: pl7.app/chargeShift PColumn at each scope. Domain carries
pl7.app/pH/from and pl7.app/pH/to so additional pH pairs land later
without breaking v1 column identity. Default-visible alongside the
static charge column at each scope; isScore omitted (interpretive, not
a Lead Selection ranking criterion). Description per spec R6b lifted
to a CHARGE_SHIFT_DESC constant.

Tests: TestChargeShift / TestFvChargeShift behavioral tests in
test_properties.py (identity, dose response, NA, anti-symmetry, Cys
policy, additivity). M3 validation in test_m3_validation.py — 7 CDR-H3
sequences vs textbook HH; 2 paired Fv chains for the additive
identity. CID quantization prefix expanded to chargeShift_*. Corpus
manifest expectations extended for all peptide / antibody / TCR cases;
TCR's expected_columns_absent extended with chargeShift_Fv.
- R9 — peptide mode emits an info banner when any peptide falls below the
  Instability Index length floor (INSTABILITY_MIN_LENGTH). Added
  hasPeptideBelowInstabilityFloor to run_peptide stats; banner via
  messages.peptidesShortInstability(). Mirrors the per-row gate inside
  instability_index so the banner gates on the same condition that
  produces NA cells.

- ΔCharge tooltip — tightened from spec R6b verbatim text (~110 words →
  ~70). Substance preserved: pH endpoints, sign convention, His
  dominance, magnitude → switching-capacity rule, scope exclusion.
  Lead-with-the-rule, parallel small/large structure, dropped filler.

- Test polish — ids= on TestFvChargeShift parametrize so test output
  shows [natural]/[his_rich] instead of full VH-VL string concat.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the ΔCharge metric (pl7.app/chargeShift), which measures the net charge change between pH 7.4 and 6.0 across peptide, CDR3, and Fv scopes to capture pH-switching capacity. It also implements a mechanism to flag and notify users when peptides are too short for the Instability Index calculation. Feedback identifies a logic error where empty sequences incorrectly trigger the instability warning banner and suggests an optimization for the charge_shift function to avoid redundant sequence processing and object instantiation.

Comment thread software/src/pipeline.py Outdated
Comment thread software/src/properties.py Outdated
…e_shift shares IP instance

PR #5 review feedback.

pipeline.py — `has_below_floor` previously fired on empty cells and
crashed on Polars nulls (effective_length called .upper() on None).
Filter `if s` to skip falsy values and `0 < effective_length(s)` to
skip all-non-standard sequences that clean to empty. Banner now fires
only on real peptides shorter than INSTABILITY_MIN_LENGTH, matching
the banner text.

properties.py — charge_shift now prepares the sequence once and
shares a single IsoelectricPoint instance across the two pH points.
Removes redundant cleanup pass and BioPython instantiation. Same
pattern is already used in isoelectric_point's bisection loop.
TestChargeShift.test_matches_two_point_subtraction guarantees the
result still equals charge(ph_from) − charge(ph_to).

Tests — reworked test_instability_floor_flag_tracks_na_rows
parametrize cases. Added empty-and-null-skip and all-nonstandard-skip
to lock in regression coverage for both bug paths.
@PaulNewling
PaulNewling marked this pull request as ready for review May 5, 2026 17:23
Builds one ProteinAnalysis and one IsoelectricPoint per (sequence, pka_set,
include_cys) instead of one per property. Pipeline reuses the full-chain
context for the Fv pass so VH/VL IsoelectricPoint(IPC2_PROTEIN,
include_cys=False) is shared between charge_at_pH(7.0) and the pI bisection.
DataFrames build columnarly (dict-of-lists) rather than list-of-dicts.

Output is byte-identical to pre-refactor on the corpus tests (verified by
sha256 across peptide / antibody / tcr modes for properties.tsv,
aa_fraction.tsv, and stats.json). ~1.7x faster on per-property micro-bench;
end-to-end ~40k peptides/s and ~10k antibody-clones/s with full-chain + Fv.

Adds chain-mode byte-stability tests (two-run + row-permutation invariance)
for the antibody path that the existing peptide tests don't cover.
Both changes ship together in this branch — keep one changeset for the
combined release notes and to consolidate the version bump.
Signed-off-by: Paul Newling <paulnewling@gmail.com>
@PaulNewling
PaulNewling added this pull request to the merge queue May 5, 2026
Merged via the queue into main with commit 516c5bc May 5, 2026
9 checks passed
@PaulNewling
PaulNewling deleted the paulnewling/MILAB-6140_delta-charge branch May 5, 2026 17:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant