Skip to content

Commit 5602dff

Browse files
committed
cleanup: workflow + Python pass
Pure refactor; zero behavior change. 65 pytest cases still pass; full block build green (7/7). Workflow (main.tpl.tengo): - Extract `withSidecar(cmd, bundleKey, filename, cliFlag)` helper for the optional enrichment sidecars. Two near-identical 7-line blocks (cdrh3 lengths, per-residue confidence) collapse to two one-liner calls returning the extended `cmd`. Filter stays inline since it uses the single-column accessor. - Extract `withOptArg(cmd, flag, value)` for the three optional CLI string knobs (numbering scheme, chain-h, chain-l). Three 3-line guarded `cmd.arg(...)` chains collapse to three one-liners. - Replace the `pdbIndex := ""` accumulator loop with one-shot `text.join(indexLines, "\n") + "\n"`. - Drop dead `rowCount` increment (never read after the loop). - Hoist `ll` import to module level (was lazy-imported inside wf.body's panic branch only). Python (metrics.py): - Extract `_weight_fns(in_bridge)` factory. The same three weight closures (hydrophobicity / pos_charge_abs / neg_charge_abs) were defined inline in `_chain_metrics` (per-chain, VHH branch) AND again in the TAP branch over the unified H+L `in_bridge`. Both callsites now unpack a single helper call. - Drop the local `h_scale` / `h_glycine` aliases, the helper reads the module constants directly. Python (main.py): - Replace `isinstance(surface_metrics, dict)` with the simpler `sm = surface_metrics` (compute_metrics always returns a dict; the paranoia check is unwarranted). - `hallmark` can be None (per its signature), so the mismatch check is now `if hallmark and hallmark.get("mismatch")`. Em-dash sweep: - Drop em-dashes / en-dashes across all Python source (cysteines.py, motifs.py, scoring.py, structure.py, metrics.py) per the no-em-dashes feedback. ~30 occurrences total. - `check_hallmark_tetrad` warning message switched the em-dash sentinel `'—'` to `'?'` so missing residues show up readably.
1 parent f308cfd commit 5602dff

8 files changed

Lines changed: 86 additions & 104 deletions

File tree

software/liabilities-script/cysteines.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def _collect_cys_records(parsed):
7272

7373
def _scan_disulfides(cys_records) -> dict[int, int]:
7474
"""Pairwise scan over cys_records; returns idx → partner_idx for every
75-
Cys engaged in a disulfide. First-match wins on ambiguous geometry
75+
Cys engaged in a disulfide. First-match wins on ambiguous geometry ,
7676
if one Cys could bond to two partners, we lock the first one we hit
7777
and treat the other as `unbonded` (consistent with REMARK SSBOND-style
7878
1:1 bond accounting; ambiguous cases are rare and biologically odd)."""
@@ -152,7 +152,7 @@ def detect_cysteines(
152152
(role, pos) for role, positions in canonical_positions.items() for pos in positions
153153
}
154154
# Used below to skip phantom rows for canonical positions that ARE
155-
# filled keyed by (role, res_seq).
155+
# filled , keyed by (role, res_seq).
156156
cys_by_role_pos: dict[tuple[str, int], int] = {}
157157
for idx, (chain_id, r, _ca, _sg) in enumerate(cys_records):
158158
role = role_of_chain(chain_id, heavy_chain_id, light_chain_id)

software/liabilities-script/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,13 +260,15 @@ def analyze_pdb(
260260
parsed, numbering_scheme, heavy_chain_id, chain_count_mode=mode
261261
)
262262
hallmark_warning = ""
263-
if isinstance(hallmark, dict) and hallmark.get("mismatch"):
263+
if hallmark and hallmark.get("mismatch"):
264264
impl = hallmark.get("impliedMode", "?")
265265
hallmark_warning = (
266266
f"hallmark tetrad implies {impl} but chain count says {mode}"
267267
)
268268

269-
sm = surface_metrics if isinstance(surface_metrics, dict) and "mode" in surface_metrics else {}
269+
# compute_metrics returns {} when neither chain is mapped; .get(...) below
270+
# returns None for every metric in that case.
271+
sm = surface_metrics
270272
flags = developability["flags"]
271273
extra_cys = sum(1 for h in cys_hits if h.cysClass == "cys_extra")
272274
exposed_extra_cys = sum(

software/liabilities-script/metrics.py

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
charge assignment. Section 2 is the patch detector + per-mode metric
66
implementations.
77
8-
Fv (TAP) Raybould 2019:
8+
Fv (TAP) , Raybould 2019:
99
R24 totalCdrLength : sum of CDR loop lengths from numbering
1010
R25 PSH : Σ H(R₁)·H(R₂)/r² over surface CDR-vicinity pairs
1111
within 7.5 Å heavy-atom distance, no type restriction
@@ -14,7 +14,7 @@
1414
R28 SFvCSP : [Σ_RH Q(RH)] × [Σ_RL Q(RL)] over surface-exposed
1515
whole-V-domain residues (NOT CDR-restricted)
1616
17-
VHH (TNP) Gordon 2025:
17+
VHH (TNP) , Gordon 2025:
1818
R29 totalCdrLength : sum of three CDR lengths (single chain)
1919
R31 PSH/PPC/PNC : same algorithms applied to one chain, but with
2020
same-type-pair restriction (PSH: H-H pairs only;
@@ -25,7 +25,7 @@
2525
Per R34/R36 the residue's local positional uncertainty is the parser's
2626
mean heavy-atom B-factor, gated region-aware against `frConfidenceGatingThreshold`
2727
(framework) or `cdrConfidenceGatingThreshold` (CDR). For each metric we
28-
emit a parallel `<metric>LowConfidenceResidueFraction` Double fraction
28+
emit a parallel `<metric>LowConfidenceResidueFraction` Double , fraction
2929
of *contributing* residues that exceed their region threshold (R36).
3030
"""
3131

@@ -112,6 +112,27 @@ def _centroid(atoms) -> tuple[float, float, float]:
112112
)
113113

114114

115+
def _weight_fns(in_bridge: dict):
116+
"""Build the three PSH / PPC / PNC weight closures over a shared
117+
`in_bridge` map (R15a: salt-bridged residues take the glycine
118+
hydrophobicity and zero charge). The same three are needed by both
119+
`_chain_metrics` (per-chain, VHH mode) and the TAP-mode Fv pool
120+
built from H+L; centralising avoids triple-duplicating them."""
121+
def hydrophobicity(i, aa):
122+
v = hydrophobicity_of(aa, in_bridge.get(i, False), KD_HYDROPHOBICITY, GLYCINE_HYDROPHOBICITY)
123+
return v if v is not None else 0.0
124+
125+
def pos_charge_abs(i, aa):
126+
c = charge_of(aa, in_bridge.get(i, False))
127+
return c if c > 0 else 0.0
128+
129+
def neg_charge_abs(i, aa):
130+
c = charge_of(aa, in_bridge.get(i, False))
131+
return -c if c < 0 else 0.0
132+
133+
return hydrophobicity, pos_charge_abs, neg_charge_abs
134+
135+
115136
def detect_salt_bridges(parsed) -> set[tuple[str, int, str]]:
116137
"""Walk every K/R + D/E pair, return the set of residue keys that are in
117138
a salt bridge. R15a uses N+ ↔ O- atom-pair distance ≤ 3.2 Å. Returned
@@ -182,7 +203,7 @@ def charge_of(aa_letter: str, in_salt_bridge: bool) -> float:
182203

183204
@dataclass
184205
class FvMetrics:
185-
"""R24-R28 (Fv mode) emitted only when 2 mapped chains and Fv-shape input."""
206+
"""R24-R28 (Fv mode) , emitted only when 2 mapped chains and Fv-shape input."""
186207
totalCdrLength: int
187208
psh: float
188209
pshPatchCount: int
@@ -193,7 +214,7 @@ class FvMetrics:
193214

194215
@dataclass
195216
class VhhMetrics:
196-
"""R29-R33 (VHH mode) single chain, type-restricted patches, CDRH3 compactness."""
217+
"""R29-R33 (VHH mode) , single chain, type-restricted patches, CDRH3 compactness."""
197218
totalCdrLength: int
198219
psh: float
199220
pshPatchCount: int
@@ -452,8 +473,6 @@ def compute_metrics(
452473

453474
salt_bridges = detect_salt_bridges(parsed)
454475
chain_id_to_residues = dict(parsed.residues_by_chain)
455-
h_scale = KD_HYDROPHOBICITY
456-
h_glycine = GLYCINE_HYDROPHOBICITY
457476

458477
h_present = heavy_chain_id and heavy_chain_id in chain_id_to_residues
459478
l_present = light_chain_id and light_chain_id in chain_id_to_residues
@@ -474,17 +493,7 @@ def _chain_metrics(chain_id, role, type_restricted_psh):
474493
regions = [region_for(role, r.res_seq, numbering_scheme, parsed.platforma_cdrs) for r in residues]
475494
cdr_vic = _cdr_vicinity_residues(residues, regions, rsasa_lookup, rsasa_buried_cutoff)
476495

477-
def h_weight(i, aa):
478-
v = hydrophobicity_of(aa, in_bridge.get(i, False), h_scale, h_glycine)
479-
return v if v is not None else 0.0
480-
481-
def pos_charge_abs(i, aa):
482-
c = charge_of(aa, in_bridge.get(i, False))
483-
return c if c > 0 else 0.0
484-
485-
def neg_charge_abs(i, aa):
486-
c = charge_of(aa, in_bridge.get(i, False))
487-
return -c if c < 0 else 0.0
496+
h_weight, pos_charge_abs, neg_charge_abs = _weight_fns(in_bridge)
488497

489498
psh, psh_patches, psh_contrib = _residue_pair_sum(
490499
cdr_vic, residues, aa_letters, h_weight,
@@ -506,7 +515,7 @@ def neg_charge_abs(i, aa):
506515
out: dict = {"mode": mode}
507516

508517
if mode == "TAP":
509-
# Combine H and L into a single CDR-vicinity / pair pool but the
518+
# Combine H and L into a single CDR-vicinity / pair pool , but the
510519
# spec defines PSH/PPC/PNC over the *Fv* (both chains together).
511520
# We compute per-chain CDR vicinities, then merge for inter-chain
512521
# pair contributions.
@@ -545,17 +554,7 @@ def neg_charge_abs(i, aa):
545554

546555
cdr_vic = _cdr_vicinity_residues(residues, regions, rsasa_lookup, rsasa_buried_cutoff)
547556

548-
def h_weight(i, aa):
549-
v = hydrophobicity_of(aa, in_bridge.get(i, False), h_scale, h_glycine)
550-
return v if v is not None else 0.0
551-
552-
def pos_charge_abs(i, aa):
553-
c = charge_of(aa, in_bridge.get(i, False))
554-
return c if c > 0 else 0.0
555-
556-
def neg_charge_abs(i, aa):
557-
c = charge_of(aa, in_bridge.get(i, False))
558-
return -c if c < 0 else 0.0
557+
h_weight, pos_charge_abs, neg_charge_abs = _weight_fns(in_bridge)
559558

560559
psh, psh_patches, psh_contrib = _residue_pair_sum(cdr_vic, residues, aa_letters, h_weight)
561560
ppc, _, ppc_contrib = _residue_pair_sum(cdr_vic, residues, aa_letters, pos_charge_abs)

software/liabilities-script/motifs.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Regex set + weights + risk taxonomy copied VERBATIM from
22
# blocks/antibody-sequence-liabilities/liabilities-calc-script/src/definitions.py
3-
# (R16). Spec requires this block to be standalone no import to avoid
3+
# (R16). Spec requires this block to be standalone , no import , to avoid
44
# silent half-coupling. A parity check vs the source is owed at M1.
55

66
import math
@@ -84,7 +84,7 @@ class MotifHit:
8484
region: str | None
8585
# R18: absolute SASA (Ų) for the chemically-relevant residue, paired
8686
# with rSASA. Spec mandates both even though rSASA × Ala-X-Ala ref
87-
# recovers it keeping the raw value makes downstream analytics
87+
# recovers it , keeping the raw value makes downstream analytics
8888
# comparable to TAP-style reports without a back-conversion step.
8989
sasa: float | None
9090
rsasa: float
@@ -106,7 +106,7 @@ class MotifHit:
106106

107107

108108
def _exposure_factor(rsasa: float | None) -> float:
109-
# R20: logistic centered at 0.30 avoids a cliff at the buried/exposed
109+
# R20: logistic centered at 0.30 , avoids a cliff at the buried/exposed
110110
# cutoff so transitional residues taper smoothly into the score.
111111
if rsasa is None:
112112
return 0.0
@@ -173,13 +173,13 @@ def _score_motif_hit(
173173
arithmetic per hit.
174174
175175
Computes:
176-
• exposureFactor R20 logistic on rSASA (smooths the buried/exposed
176+
• exposureFactor , R20 logistic on rSASA (smooths the buried/exposed
177177
cliff at 0.30).
178-
• confidence residue's mean heavy-atom B-factor (R34).
179-
• confidenceGated true when B-factor exceeds the region-aware
178+
• confidence , residue's mean heavy-atom B-factor (R34).
179+
• confidenceGated , true when B-factor exceeds the region-aware
180180
threshold (R35); gated hits stay in the table
181181
for traceability but skip motifStructuralRiskScore.
182-
• weightedScore fixability_weight × region_weight × exposureFactor.
182+
• weightedScore , fixability_weight × region_weight × exposureFactor.
183183
The R19 region weight rewards CDR-localized hits
184184
(most therapeutically relevant) over framework.
185185
"""
@@ -220,7 +220,7 @@ def detect_motifs(
220220
):
221221
"""Walk each chain, apply the regex set, and emit hits whose
222222
chemically-relevant residue (R17) has rSASA >= cutoff. Buried matches
223-
are suppressed entirely per the spec, not just down-weighted a buried
223+
are suppressed entirely per the spec, not just down-weighted , a buried
224224
NG can't be deamidated, so flagging it would be a false positive.
225225
226226
When numbering_scheme + heavy/light chain mapping are supplied, hits get
@@ -252,7 +252,7 @@ def detect_motifs(
252252
continue
253253
residue = residues[pos_in_seq]
254254
if AA_THREE_TO_ONE.get(residue.res_name) is None:
255-
# Non-standard residue skip (we can't trust the
255+
# Non-standard residue , skip (we can't trust the
256256
# 1-letter translation that fed the regex match).
257257
continue
258258
key = (chain_id, f"{residue.res_seq}{residue.i_code}".strip())

software/liabilities-script/scoring.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def _coerce_band_tuples(thresholds: dict) -> dict:
5555
def _flag_one_sided(value: float, spec: dict) -> str:
5656
"""Three-band: amber within (amber_lo, amber_hi); red on the bad side
5757
of the band, green on the good side. `direction` picks which side is
58-
bad "high_bad" (most metrics) treats values above amber as red,
58+
bad , "high_bad" (most metrics) treats values above amber as red,
5959
"low_bad" (SFvCSP) treats values below amber as red."""
6060
amber_lo, amber_hi = spec["amber"]
6161
direction = spec.get("direction", "high_bad")
@@ -141,7 +141,7 @@ def _cys_class_bump(cys_class: str, sidechain_rsasa: Optional[float], buried_cut
141141
return 0.0
142142

143143

144-
# R41a engineering-grade fixability tiers + the four-step risk ladder.
144+
# R41a , engineering-grade fixability tiers + the four-step risk ladder.
145145
# `_ENGINEERING_FIXABILITIES` is the same set the sequence-liabilities
146146
# block uses for `classify_developability_risk` (motifs we can credibly
147147
# fix without a sequence redesign).
@@ -151,14 +151,14 @@ def _cys_class_bump(cys_class: str, sidechain_rsasa: Optional[float], buried_cut
151151

152152

153153
def _seq_risk_to_level(rc: str) -> str:
154-
"""R41a sequence-side risk class → R41a level. Identity-ish mapping
154+
"""R41a , sequence-side risk class → R41a level. Identity-ish mapping
155155
that defends against unexpected `sequenceRiskClass` values by falling
156156
through to "None"."""
157157
return {"High": "High", "Medium": "Medium", "Low": "Low"}.get(rc, "None")
158158

159159

160160
def _developability_risk(motif_hits, flags: dict[str, str]) -> str:
161-
"""R41a over engineering-fixable, non-gated motifs only:
161+
"""R41a , over engineering-fixable, non-gated motifs only:
162162
1. Take the highest sequenceRiskClass among them as the base level.
163163
2. Promote to Medium if ANY metric flag is amber.
164164
3. Promote to High if ANY metric flag is red.
@@ -184,11 +184,11 @@ def _developability_risk(motif_hits, flags: dict[str, str]) -> str:
184184

185185

186186
def _has_integrity_issue(motif_hits, cys_hits, rsasa_buried_cutoff: float) -> bool:
187-
"""R41a structuralIntegrityRisk Present iff at least one of:
187+
"""R41a structuralIntegrityRisk , Present iff at least one of:
188188
• a canonical disulfide is broken or missing entirely (cys side),
189189
• an extra Cys is surface-exposed (free thiol → covalent aggregation risk),
190190
• a non-gated motif lives in the {hard_to_fix, structural} tier.
191-
Any one is enough short-circuits as soon as it finds a trigger."""
191+
Any one is enough , short-circuits as soon as it finds a trigger."""
192192
for h in cys_hits:
193193
if h.cysClass in ("disulfide_broken", "disulfide_missing"):
194194
return True

software/liabilities-script/structure.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,12 @@ class Parsed:
6161
chain_order: List[str] = field(default_factory=list)
6262
residues_by_chain: Dict[str, List[Residue]] = field(default_factory=dict)
6363
ssbonds: List[Ssbond] = field(default_factory=list)
64-
# Spec R10 CDR ranges from `REMARK 99 PLATFORMA CDR*` records emitted
64+
# Spec R10 , CDR ranges from `REMARK 99 PLATFORMA CDR*` records emitted
6565
# by the Structure Prediction block. Shape: {"H": {"CDR1": (start, end),
6666
# "CDR2": (...), "CDR3": (...)}, "L": {...}}. Empty when not present;
6767
# downstream code falls back to scheme-aware fixed ranges.
6868
platforma_cdrs: Dict[str, Dict[str, Tuple[int, int]]] = field(default_factory=dict)
69-
# Spec R9 REMARK 99 chain identity is authoritative. Maps role ("H"/"L")
69+
# Spec R9 , REMARK 99 chain identity is authoritative. Maps role ("H"/"L")
7070
# to the physical PDB chain letter the records reference. e.g. given
7171
# `REMARK 99 PLATFORMA CDRH1 B27-B38`, this becomes {"H": "B"}. Caller
7272
# uses this to override the user's heavy/light chain dropdowns when
@@ -117,7 +117,7 @@ def parse_pdb(text: str) -> Parsed:
117117
if end < start:
118118
continue
119119
out.platforma_cdrs.setdefault(role, {})[f"CDR{cdr_idx}"] = (start, end)
120-
# Spec R9 record the physical PDB chain letter for this role.
120+
# Spec R9 , record the physical PDB chain letter for this role.
121121
# Later records for the same role must agree; conflicts are
122122
# silently dropped (downstream falls back to the user's mapping).
123123
existing = out.chain_role_to_pdb_chain.get(role)
@@ -158,7 +158,7 @@ def parse_pdb(text: str) -> Parsed:
158158
# 30-37 x 38-45 y 46-53 z (Å, free-form floats)
159159
# 60-65 B-factor (Ų for crystals, Šfor ImmuneBuilder-predicted)
160160
#
161-
# altLoc filter multi-conformer side chains list each alternate
161+
# altLoc filter , multi-conformer side chains list each alternate
162162
# location with a letter ('A', 'B', ...). Keeping only ' ' and 'A'
163163
# ensures geometry tests (distance pairs, salt bridges) don't
164164
# double-count atoms.
@@ -274,7 +274,7 @@ def region_for(
274274
"""Return "FR1" / "CDR1" / "FR2" / "CDR2" / "FR3" / "CDR3" / "FR4" /
275275
None. Returns None when chain_role is unknown (e.g. antigen chains in a
276276
complex), scheme is invalid, or residue falls outside the V-domain (e.g.
277-
constant region in a Fab we don't tag CH1/CL).
277+
constant region in a Fab , we don't tag CH1/CL).
278278
279279
chain_role: "H" or "L". Pass "H" for VHH (single-chain camelid) too;
280280
its numbering follows heavy-chain convention.
@@ -305,7 +305,7 @@ def region_for(
305305
v_end = SCHEME_VDOMAIN_END[s][chain_role]
306306

307307
if res_seq > v_end:
308-
return None # constant region not tagged
308+
return None # constant region , not tagged
309309
if res_seq < cdr1_start:
310310
return "FR1"
311311
if cdr1_start <= res_seq <= cdr1_end:
@@ -513,7 +513,7 @@ def check_hallmark_tetrad(
513513
)
514514
if mismatch:
515515
observed = ", ".join(
516-
f"{p}={o or ''}" for p, o in zip(positions, one_letters)
516+
f"{p}={o or '?'}" for p, o in zip(positions, one_letters)
517517
)
518518
print(
519519
f"WARN (spec R33): hallmark-tetrad residues at {observed} "

ui/src/composables/ptableCell.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,4 @@ export function readString(col: PTableColumn, i: number): string | null {
5656
* `outputWithStatus` result whose `value` carries `fullTableHandle` for the
5757
* pFrameDriver. Lives here (next to readCell) since both consumers
5858
* (useClusterAssignments, useRunSummaryAlerts) operate on the same handle. */
59-
export type ScoresTableOutput =
60-
| { ok?: boolean; value?: { fullTableHandle?: unknown } }
61-
| undefined;
59+
export type ScoresTableOutput = { ok?: boolean; value?: { fullTableHandle?: unknown } } | undefined;

0 commit comments

Comments
 (0)