From 1317dc845c5bf8a5f4c96bfe5582ba6b8c8a96c3 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 09:04:49 -0700 Subject: [PATCH 01/21] Extend output quantization boundary to all float columns --- software/src/pipeline.py | 66 ++++++++--- software/tests/unit/test_quantization.py | 136 +++++++++++++++++------ 2 files changed, 156 insertions(+), 46 deletions(-) diff --git a/software/src/pipeline.py b/software/src/pipeline.py index 22742da..3aede3f 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -38,29 +38,64 @@ # CID quantization # --------------------------------------------------------------------------- # -# Only `charge_*` and `pi_*` outputs depend on a transcendental — `10**x` via -# libm — and only those carry ULP-level variance when the underlying FP path -# changes (libm patch, numpy SIMD reduction strategy, Python → numpy code -# substitution). Every other property is closed-form integer / constant -# arithmetic and bit-exact under IEEE-754 on a single machine. +# Determinism contract: "quantized-equal". Every emitted float column is +# rounded at the output boundary to one digit below its display precision (and +# at least 4 dp). This serves two ends: users see no change (rounding stays +# below the `.Nf` display format), and the upcoming BioPython → numpy code +# substitution's floating-point drift is absorbed, so the workflow's content- +# addressable id stays byte-stable across a same-machine re-run. # -# Rounding to 3 decimals matches the isoelectric_point bisection tolerance of -# 1e-3 — the value's true precision is already 0.0005, so rounding to 0.001 -# discards only ULP noise without losing real information. Display format -# (.2f) is even coarser, so users see no change. +# `charge_*`, `chargeShift_*`, and `pi_*` are the only outputs that depend on a +# transcendental (`10**x` via libm), so they carry true ULP-level FP variance +# today. They round to 3 dp, matching the isoelectric_point bisection tolerance +# of 1e-3 (the value's real precision is ~0.0005). The other families are +# closed-form arithmetic — bit-exact under IEEE-754 on one machine now, but +# rounded anyway so the contract holds uniformly once the math is vectorized. +# +# `eox_*`/`ered_*` are integer-valued; rounding to 0 dp is exact and never +# perturbs them. # # The quantization is a *boundary* concern. Internal property functions # (`charge_at_ph`, `isoelectric_point`, etc.) keep full precision so golden- # value tests stay sharp. Only the pipeline's emitted DataFrame is rounded. +# +# `CID_QUANTIZE_PREFIXES` / `CID_QUANTIZE_DECIMALS` remain as documentation of +# the charge/chargeShift/pi family's 3-dp contract (other tests assert them). CID_QUANTIZE_PREFIXES = ("charge_", "chargeShift_", "pi_") CID_QUANTIZE_DECIMALS = 3 +# Per-column-family rounding at the output boundary. Keys match the TSV column +# *prefixes* the pipeline emits; the first matching prefix wins. Chosen below +# each property's display format (columns.lib.tengo) with headroom over the +# BioPython->numpy FP drift the upcoming vectorization introduces. +CID_QUANTIZE_DECIMALS_BY_PREFIX: tuple[tuple[str, int], ...] = ( + ("charge_", 3), + ("chargeShift_", 3), + ("pi_", 3), # .2f display, unchanged + ("instability_", 4), # .2f display + ("mw_", 4), + ("aliphatic_", 4), # .1f display + ("gravy_", 5), + ("aromaticity_", 5), # .3f display + ("eox_", 0), + ("ered_", 0), # .0f, integer-valued +) + + +def _decimals_for(col: str) -> int | None: + for prefix, dp in CID_QUANTIZE_DECIMALS_BY_PREFIX: + if col.startswith(prefix): + return dp + return None + def _quantize_for_cid(df: pl.DataFrame) -> pl.DataFrame: - cols = [c for c in df.columns if any(c.startswith(p) for p in CID_QUANTIZE_PREFIXES)] - if not cols: - return df - return df.with_columns([pl.col(c).round(CID_QUANTIZE_DECIMALS) for c in cols]) + exprs = [] + for c in df.columns: + dp = _decimals_for(c) + if dp is not None and df.schema[c] == pl.Float64: + exprs.append(pl.col(c).round(dp)) + return df.with_columns(exprs) if exprs else df # --------------------------------------------------------------------------- @@ -203,6 +238,11 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: {"entity_key": aa_entity, "aminoAcid": aa_amino, "value": aa_value}, schema={"entity_key": pl.Utf8, "aminoAcid": pl.Utf8, "value": pl.Float64}, ) + # Quantize at the output boundary like the property columns. aaFraction + # displays as .3f; round to 5 dp (one digit below display, headroom for the + # upcoming vectorization's FP drift). `value` is a closed-form ratio today, + # so rounding only trims repeating-decimal tails — no display change. + aa_fraction = aa_fraction.with_columns(pl.col("value").round(5)) # R9 — flag whether any *real* peptide falls below the Instability Index # floor. `if s` filters None / "" so the banner does not fire on empty diff --git a/software/tests/unit/test_quantization.py b/software/tests/unit/test_quantization.py index b5f1719..aded2fc 100644 --- a/software/tests/unit/test_quantization.py +++ b/software/tests/unit/test_quantization.py @@ -1,13 +1,16 @@ """CID quantization tests. -Charge and pI columns depend on `10**x` via libm — a real source of ULP -variance across library upgrades or Python→numpy code-path swaps. The -pipeline rounds these columns to bisection-tolerance precision (1e-3) at -the output boundary so a same-machine re-run produces byte-identical -output and the workflow's content-addressable id stays stable. - -Every other property is closed-form integer / constant arithmetic and -bit-exact under IEEE-754; quantization there would be churn. +Determinism contract: "quantized-equal". Every emitted float column is rounded +at the pipeline output boundary to one digit below its display precision (and +at least 4 dp). Rounding stays below the `.Nf` display format, so users see no +change, while the upcoming BioPython → numpy code-path swap's floating-point +drift is absorbed — a same-machine re-run produces byte-identical output and +the workflow's content-addressable id stays stable. + +Per-family decimals (see `CID_QUANTIZE_DECIMALS_BY_PREFIX`): charge / chargeShift +/ pi at 3 dp; instability / mw / aliphatic at 4 dp; gravy / aromaticity at 5 dp; +eox / ered at 0 dp (integer-valued, exact). The aa_fraction frame's `value` +column rounds to 5 dp. These tests guard the boundary behaviour. Internal property functions (tested elsewhere) keep full precision so golden-value tests stay sharp. @@ -29,9 +32,10 @@ class TestQuantizeHelper: """Direct tests of the boundary helper.""" - # Quantization rounds only charge_* and pi_* columns; everything else - # passes through bit-identical. - def test_only_charge_and_pi_are_rounded(self): + # Quantization rounds every float family to its per-column decimals. + # charge/chargeShift/pi at 3 dp; instability/mw/aliphatic at 4 dp; + # gravy/aromaticity at 5 dp; eox/ered at 0 dp (integer-valued, exact). + def test_all_float_families_rounded(self): df = pl.DataFrame( { "entity_key": ["x"], @@ -47,15 +51,17 @@ def test_only_charge_and_pi_are_rounded(self): } ) out = _quantize_for_cid(df) - # Rounded: - assert out["charge_peptide"][0] == pytest.approx(0.123, abs=0) - assert out["pi_peptide"][0] == pytest.approx(7.123, abs=0) - # Untouched (each value retains every digit it carried in): - assert out["gravy_peptide"][0] == 0.4142857 - assert out["mw_peptide"][0] == 1234.56789 - assert out["instability_peptide"][0] == 38.7531234 - assert out["aliphatic_peptide"][0] == 61.296296 - assert out["aromaticity_peptide"][0] == 0.185185 + # charge / pi — 3 dp: + assert out["charge_peptide"][0] == round(0.1234567, 3) + assert out["pi_peptide"][0] == round(7.123456, 3) + # instability / mw / aliphatic — 4 dp: + assert out["mw_peptide"][0] == round(1234.56789, 4) + assert out["instability_peptide"][0] == round(38.7531234, 4) + assert out["aliphatic_peptide"][0] == round(61.296296, 4) + # gravy / aromaticity — 5 dp: + assert out["gravy_peptide"][0] == round(0.4142857, 5) + assert out["aromaticity_peptide"][0] == round(0.185185, 5) + # eox / ered — 0 dp, integer-valued, exact: assert out["eox_peptide"][0] == 22460.0 assert out["ered_peptide"][0] == 22460.0 @@ -73,9 +79,9 @@ def test_antibody_charge_and_pi_columns_match_prefix(self): "pi_A_VDJRegion": [7.018372], "pi_B_VDJRegion": [9.798889], "pi_Fv": [9.330627], - # Non-quantized — must pass through: - "gravy_A_VDJRegion": [-0.111111], - "mw_A_VDJRegion": [6050.7302], + # Other float families now round too — gravy 5 dp, mw 4 dp: + "gravy_A_VDJRegion": [-0.11111156], + "mw_A_VDJRegion": [6050.730289], } ) out = _quantize_for_cid(df) @@ -91,15 +97,16 @@ def test_antibody_charge_and_pi_columns_match_prefix(self): ): v = out[col][0] assert v == pytest.approx(round(v, CID_QUANTIZE_DECIMALS), abs=0) - # Non-quantized columns retain full precision: - assert out["gravy_A_VDJRegion"][0] == -0.111111 - assert out["mw_A_VDJRegion"][0] == 6050.7302 + # gravy / mw round to their per-column decimals (5 dp / 4 dp): + assert out["gravy_A_VDJRegion"][0] == round(-0.11111156, 5) + assert out["mw_A_VDJRegion"][0] == round(6050.730289, 4) - # No-op when nothing matches the prefix list. + # No-op when no column matches any known property prefix. Uses a fabricated + # column name so the genuine no-match branch stays covered. def test_passthrough_when_no_matching_columns(self): - df = pl.DataFrame({"entity_key": ["x"], "gravy_peptide": [0.123456789]}) + df = pl.DataFrame({"entity_key": ["x"], "notaproperty_x": [0.123456789]}) out = _quantize_for_cid(df) - assert out["gravy_peptide"][0] == 0.123456789 + assert out["notaproperty_x"][0] == 0.123456789 # Sanity — module-level constants match what the docstring promises. def test_constants_track_documented_values(self): @@ -110,7 +117,7 @@ def test_constants_track_documented_values(self): class TestPipelineQuantizationApplied: """Quantization fires at the pipeline boundary, not just in the helper.""" - # Peptide pipeline output: charge / pi rounded, others not. + # Peptide pipeline output: charge / pi land at their 3-dp boundary. def test_peptide_run_rounds_charge_and_pi(self): reads = pl.DataFrame( { @@ -127,9 +134,9 @@ def test_peptide_run_rounds_charge_and_pi(self): assert v is not None assert v == pytest.approx(round(v, CID_QUANTIZE_DECIMALS), abs=0), f"{c}={v} not at 3-decimal precision" - # Antibody full-coverage output: charge_*, pi_*, including Fv, all rounded. - # Non-rounded columns (gravy / mw / instability / aliphatic / aromaticity / ε) - # must keep full precision. + # Antibody full-coverage output: charge_*, pi_*, including Fv, all land at + # their 3-dp boundary. (Other families round too — covered by the helper + # tests; here we assert the 3-dp family specifically.) def test_antibody_run_rounds_all_charge_and_pi_columns( self, antibody_full_one_clone: pl.DataFrame, antibody_full_plan: dict ): @@ -164,3 +171,66 @@ def test_isoelectric_point_returns_unrounded_value(self): pi = isoelectric_point(vh, IPC2_PROTEIN, include_cys=False) assert pi == pytest.approx(6.006653, abs=1e-6) assert pi != round(pi, CID_QUANTIZE_DECIMALS) + + +class TestExtendedQuantizationBoundary: + # Every float column rounds; integer-valued ε columns are unaffected by 0-dp rounding. + def test_all_float_columns_rounded(self): + from pipeline import _quantize_for_cid + + df = pl.DataFrame( + { + "entity_key": ["x"], + "charge_peptide": [0.12345678], + "gravy_peptide": [0.41428571], + "mw_peptide": [1234.5678912], + "instability_peptide": [38.75312345], + "aliphatic_peptide": [61.29629629], + "aromaticity_peptide": [0.18518518], + "eox_peptide": [22460.0], + } + ) + out = _quantize_for_cid(df) + assert out["charge_peptide"][0] == round(0.12345678, 3) + assert out["gravy_peptide"][0] == round(0.41428571, 5) + assert out["mw_peptide"][0] == round(1234.5678912, 4) + assert out["instability_peptide"][0] == round(38.75312345, 4) + assert out["aliphatic_peptide"][0] == round(61.29629629, 4) + assert out["aromaticity_peptide"][0] == round(0.18518518, 5) + assert out["eox_peptide"][0] == 22460.0 + + # Rounding stays below display precision: the .Nf-formatted value is unchanged. + @pytest.mark.parametrize( + "col, value, display_dp", + [ + ("gravy_peptide", 0.4142857, 3), + ("mw_peptide", 1234.56789, 1), + ("instability_peptide", 38.7531234, 2), + ], + ) + def test_display_precision_unchanged(self, col, value, display_dp): + from pipeline import _quantize_for_cid + + out = _quantize_for_cid(pl.DataFrame({"entity_key": ["x"], col: [value]})) + assert round(out[col][0], display_dp) == round(value, display_dp) + + +class TestAaFractionQuantization: + """The aa_fraction frame's `value` column is rounded at the boundary too.""" + + # Peptide run rounds aa_fraction value to 5 dp (one below the .3f display). + # A length-7 sequence yields repeating-decimal fractions (1/7, 3/7) that are + # NOT at 5 dp pre-quantization — so this exercises the rounding, not a no-op. + def test_aa_fraction_value_rounded_to_5dp(self): + reads = pl.DataFrame( + { + "entity_key": ["p1"], + "sequence": ["AAACDEF"], # 3/7, 1/7 — repeating decimals + } + ) + out = run(reads, {"mode": "peptide"}) + values = [v for v in out["aa_fraction"]["value"].to_list() if v is not None] + assert values # non-empty + # Pre-quantization at least one value carries > 5 dp; all must end at 5 dp. + for v in values: + assert v == round(v, 5), f"aa_fraction value {v} not at 5-decimal precision" From 34d5c0934653793515481a213abaacb7fca53fb1 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 09:15:31 -0700 Subject: [PATCH 02/21] Add characterization corpus + frozen golden snapshot --- software/tests/_corpus_gen.py | 338 +++++ .../antibody_bulk_full_input.tsv | 51 + .../antibody_bulk_full_plan.json | 13 + .../antibody_cdr3_only_input.tsv | 51 + .../antibody_cdr3_only_plan.json | 10 + .../antibody_partial_input.tsv | 51 + .../antibody_partial_plan.json | 13 + .../golden/antibody_bulk_full.properties.tsv | 51 + .../golden/antibody_bulk_full.stats.json | 1 + .../golden/antibody_cdr3_only.properties.tsv | 51 + .../golden/antibody_cdr3_only.stats.json | 1 + .../golden/antibody_partial.properties.tsv | 51 + .../golden/antibody_partial.stats.json | 1 + .../golden/peptide.aa_fraction.tsv | 1141 +++++++++++++++++ .../golden/peptide.properties.tsv | 58 + .../golden/peptide.stats.json | 1 + .../golden/sc_dropout.properties.tsv | 51 + .../golden/sc_dropout.stats.json | 1 + .../golden/tcr_ab.properties.tsv | 51 + .../characterization/golden/tcr_ab.stats.json | 1 + .../golden/tcr_gd.properties.tsv | 51 + .../characterization/golden/tcr_gd.stats.json | 1 + .../data/characterization/peptide_input.tsv | 58 + .../data/characterization/peptide_plan.json | 3 + .../characterization/sc_dropout_input.tsv | 51 + .../characterization/sc_dropout_plan.json | 13 + .../data/characterization/tcr_ab_input.tsv | 51 + .../data/characterization/tcr_ab_plan.json | 13 + .../data/characterization/tcr_gd_input.tsv | 51 + .../data/characterization/tcr_gd_plan.json | 13 + .../test_characterization_snapshot.py | 40 + 31 files changed, 2332 insertions(+) create mode 100644 software/tests/_corpus_gen.py create mode 100644 software/tests/data/characterization/antibody_bulk_full_input.tsv create mode 100644 software/tests/data/characterization/antibody_bulk_full_plan.json create mode 100644 software/tests/data/characterization/antibody_cdr3_only_input.tsv create mode 100644 software/tests/data/characterization/antibody_cdr3_only_plan.json create mode 100644 software/tests/data/characterization/antibody_partial_input.tsv create mode 100644 software/tests/data/characterization/antibody_partial_plan.json create mode 100644 software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv create mode 100644 software/tests/data/characterization/golden/antibody_bulk_full.stats.json create mode 100644 software/tests/data/characterization/golden/antibody_cdr3_only.properties.tsv create mode 100644 software/tests/data/characterization/golden/antibody_cdr3_only.stats.json create mode 100644 software/tests/data/characterization/golden/antibody_partial.properties.tsv create mode 100644 software/tests/data/characterization/golden/antibody_partial.stats.json create mode 100644 software/tests/data/characterization/golden/peptide.aa_fraction.tsv create mode 100644 software/tests/data/characterization/golden/peptide.properties.tsv create mode 100644 software/tests/data/characterization/golden/peptide.stats.json create mode 100644 software/tests/data/characterization/golden/sc_dropout.properties.tsv create mode 100644 software/tests/data/characterization/golden/sc_dropout.stats.json create mode 100644 software/tests/data/characterization/golden/tcr_ab.properties.tsv create mode 100644 software/tests/data/characterization/golden/tcr_ab.stats.json create mode 100644 software/tests/data/characterization/golden/tcr_gd.properties.tsv create mode 100644 software/tests/data/characterization/golden/tcr_gd.stats.json create mode 100644 software/tests/data/characterization/peptide_input.tsv create mode 100644 software/tests/data/characterization/peptide_plan.json create mode 100644 software/tests/data/characterization/sc_dropout_input.tsv create mode 100644 software/tests/data/characterization/sc_dropout_plan.json create mode 100644 software/tests/data/characterization/tcr_ab_input.tsv create mode 100644 software/tests/data/characterization/tcr_ab_plan.json create mode 100644 software/tests/data/characterization/tcr_gd_input.tsv create mode 100644 software/tests/data/characterization/tcr_gd_plan.json create mode 100644 software/tests/integration/test_characterization_snapshot.py diff --git a/software/tests/_corpus_gen.py b/software/tests/_corpus_gen.py new file mode 100644 index 0000000..d6871f0 --- /dev/null +++ b/software/tests/_corpus_gen.py @@ -0,0 +1,338 @@ +"""Deterministic generator for the characterization corpus + golden snapshot. + +This is the keystone of the vectorization safety net. It builds a broad, +fixed corpus across every pipeline mode and edge case, then snapshots the +CURRENT quantized pipeline output to committed golden artifacts. The golden is +"whatever the current code produces" — NOT an independent correctness oracle. +The eventual vectorized engine must reproduce these exact golden bytes. + +Re-runnable and idempotent: a fixed `random.Random(0)` seed means re-running +produces byte-identical inputs, plans, and golden. If a regen changes any +golden file, that is a real nondeterminism signal — investigate, do not commit. + +Regenerate intentionally (writes inputs + plans + golden) with: + + uv run python -m tests._corpus_gen --write-golden + +The snapshot test (`tests/integration/test_characterization_snapshot.py`) then +asserts `run()` reproduces the committed golden byte-for-byte. +""" + +from __future__ import annotations + +import json +import random +import sys +from pathlib import Path + +# `pythonpath = ["src"]` in pyproject only applies under pytest. When run as a +# module (`python -m tests._corpus_gen`), wire up the same import root so +# `from pipeline import run` resolves exactly as the tests see it. +_SRC = Path(__file__).resolve().parents[1] / "src" +if str(_SRC) not in sys.path: + sys.path.insert(0, str(_SRC)) + +import polars as pl # noqa: E402 + +from io_layer import read_input_tsv, read_plan, write_output_tsv # noqa: E402 +from pipeline import run # noqa: E402 + +DATA = Path(__file__).resolve().parent / "data" / "characterization" +GOLDEN = DATA / "golden" + +# Number of generated valid entities per case (edge-case rows are additional). +N_VALID = 50 + +# IMGT regions per chain, in reconstruction order. +REGIONS = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") + +# Realistic per-region length bands (short-ish, per corpus convention). The +# point is path coverage + a frozen snapshot, not biological realism. +_REGION_LEN = { + "FR1": (7, 10), + "CDR1": (6, 8), + "FR2": (7, 9), + "CDR2": (6, 8), + "FR3": (10, 14), + "CDR3": (5, 18), + "FR4": (7, 9), +} + +# Standard residues only — keep generated sequences valid so they exercise the +# compute path (edge cases below deliberately break this). +_STANDARD_AAS = "ACDEFGHIKLMNPQRSTVWY" +_NO_AROMATIC_AAS = "ACDEGHIKLMNPQRSTV" # F, W, Y removed. + +CASES: list[str] = [ + "peptide", + "antibody_bulk_full", + "antibody_cdr3_only", + "antibody_partial", + "sc_dropout", + "tcr_ab", + "tcr_gd", +] + + +def _rand_seq(rng: random.Random, lo: int, hi: int, alphabet: str = _STANDARD_AAS) -> str: + n = rng.randint(lo, hi) + return "".join(rng.choice(alphabet) for _ in range(n)) + + +# --------------------------------------------------------------------------- +# Peptide case +# --------------------------------------------------------------------------- + + +def _peptide_rows(rng: random.Random) -> list[dict[str, str]]: + """~50 generated valid peptides PLUS the required edge-case rows. + + Edge cases (per task): empty cell, stop-codon, non-standard-only, single + residue, homopolymer, below-instability-floor (<10 aa), no-aromatic. + """ + rows: list[dict[str, str]] = [] + for i in range(N_VALID): + rows.append({"entity_key": f"pep_{i:03d}", "sequence": _rand_seq(rng, 8, 30)}) + + edge = [ + ("pep_empty", ""), # invalid -> NA row + ("pep_stop_codon", "ACDE*FGHI"), # stop codon -> NA row + ("pep_nonstandard_only", "BZXJ"), # cleans to empty -> NA row + ("pep_single_residue", "A"), + ("pep_homopolymer", "AAAAAAAAAA"), + ("pep_below_floor", "ACDEFG"), # 6 aa < instability floor (10) + ("pep_no_aromatic", _NO_AROMATIC_AAS), # no F/W/Y + ] + for key, seq in edge: + rows.append({"entity_key": key, "sequence": seq}) + return rows + + +def _write_peptide_input(rng: random.Random) -> None: + rows = _peptide_rows(rng) + df = pl.DataFrame( + { + "entity_key": [r["entity_key"] for r in rows], + "sequence": [r["sequence"] for r in rows], + }, + schema={"entity_key": pl.Utf8, "sequence": pl.Utf8}, + ) + write_output_tsv(df, DATA / "peptide_input.tsv") + _write_plan("peptide", {"mode": "peptide"}) + + +# --------------------------------------------------------------------------- +# Antibody / TCR cases +# --------------------------------------------------------------------------- + + +def _full_clone(rng: random.Random, key: str, chains: tuple[str, ...]) -> dict[str, str]: + """A clone with every region present on the given chains.""" + rec: dict[str, str] = {"entity_key": key} + for ch in chains: + for feat in REGIONS: + lo, hi = _REGION_LEN[feat] + rec[f"{ch}_{feat}"] = _rand_seq(rng, lo, hi) + return rec + + +def _empty_regions(chains: tuple[str, ...]) -> dict[str, str]: + return {f"{ch}_{feat}": "" for ch in chains for feat in REGIONS} + + +def _antibody_columns(chains: tuple[str, ...]) -> list[str]: + return ["entity_key"] + [f"{ch}_{feat}" for ch in chains for feat in REGIONS] + + +def _rows_to_df(rows: list[dict[str, str]], chains: tuple[str, ...]) -> pl.DataFrame: + cols = _antibody_columns(chains) + data = {c: [r.get(c, "") for r in rows] for c in cols} + return pl.DataFrame(data, schema={c: pl.Utf8 for c in cols}) + + +def _write_plan(case: str, plan: dict) -> None: + (DATA / f"{case}_plan.json").write_text(json.dumps(plan, indent=2) + "\n") + + +def _write_antibody_bulk_full(rng: random.Random) -> None: + """All 7 regions on both chains; IG; full chains + Fv.""" + chains = ("A", "B") + rows = [_full_clone(rng, f"clone_{i:03d}", chains) for i in range(N_VALID)] + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "antibody_bulk_full_input.tsv") + _write_plan( + "antibody_bulk_full", + { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, + }, + ) + + +def _write_antibody_cdr3_only(rng: random.Random) -> None: + """Only CDR3 present per chain; other regions empty. No full chains, no Fv.""" + chains = ("A", "B") + rows: list[dict[str, str]] = [] + for i in range(N_VALID): + rec = {"entity_key": f"clone_{i:03d}", **_empty_regions(chains)} + for ch in chains: + lo, hi = _REGION_LEN["CDR3"] + rec[f"{ch}_CDR3"] = _rand_seq(rng, lo, hi) + rows.append(rec) + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "antibody_cdr3_only_input.tsv") + _write_plan( + "antibody_cdr3_only", + { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": [], + "hasFv": False, + }, + ) + + +def _write_antibody_partial(rng: random.Random) -> None: + """Full coverage on most clones, but a deterministic subset is missing one + region on a chain (empty cell) -> full-chain is NA for those clones, and Fv + is NA when the missing region is on A or B. fullChains + Fv still requested. + """ + chains = ("A", "B") + rows: list[dict[str, str]] = [] + for i in range(N_VALID): + rec = _full_clone(rng, f"clone_{i:03d}", chains) + # Every 3rd clone drops one region; rotate which chain/region is dropped + # so both A-full-NA and B-full-NA paths are exercised. + if i % 3 == 0: + ch = "A" if (i // 3) % 2 == 0 else "B" + feat = REGIONS[(i // 3) % len(REGIONS)] + rec[f"{ch}_{feat}"] = "" + rows.append(rec) + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "antibody_partial_input.tsv") + _write_plan( + "antibody_partial", + { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, + }, + ) + + +def _write_sc_dropout(rng: random.Random) -> None: + """Single-cell mode; a deterministic subset of clones is missing chain B + entirely (all B regions empty) -> chain-B CDR3, full-chain, and Fv all NA. + """ + chains = ("A", "B") + rows: list[dict[str, str]] = [] + for i in range(N_VALID): + rec = _full_clone(rng, f"clone_{i:03d}", chains) + if i % 4 == 0: # ~1/4 of clones drop chain B entirely. + for feat in REGIONS: + rec[f"B_{feat}"] = "" + rows.append(rec) + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "sc_dropout_input.tsv") + _write_plan( + "sc_dropout", + { + "mode": "antibody_tcr_legacy_sc", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, + }, + ) + + +def _write_tcr_ab(rng: random.Random) -> None: + """TCRAB receptor; no Fv (TCR has no Fv). Full chains on both chains.""" + chains = ("A", "B") + rows = [_full_clone(rng, f"clone_{i:03d}", chains) for i in range(N_VALID)] + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "tcr_ab_input.tsv") + _write_plan( + "tcr_ab", + { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "TCRAB", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": False, + }, + ) + + +def _write_tcr_gd(rng: random.Random) -> None: + """TCRGD receptor; no Fv. Full chains on both chains.""" + chains = ("A", "B") + rows = [_full_clone(rng, f"clone_{i:03d}", chains) for i in range(N_VALID)] + _rows_to_df(rows, chains).pipe(write_output_tsv, DATA / "tcr_gd_input.tsv") + _write_plan( + "tcr_gd", + { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "TCRGD", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": False, + }, + ) + + +# --------------------------------------------------------------------------- +# Corpus + golden orchestration +# --------------------------------------------------------------------------- + +# Each writer consumes the SAME rng in CASES order, so the byte output is fixed +# by the seed and the call order alone. +_WRITERS = { + "peptide": _write_peptide_input, + "antibody_bulk_full": _write_antibody_bulk_full, + "antibody_cdr3_only": _write_antibody_cdr3_only, + "antibody_partial": _write_antibody_partial, + "sc_dropout": _write_sc_dropout, + "tcr_ab": _write_tcr_ab, + "tcr_gd": _write_tcr_gd, +} + + +def write_inputs_and_plans() -> None: + """(Re)write every case's input TSV + plan JSON deterministically.""" + DATA.mkdir(parents=True, exist_ok=True) + rng = random.Random(0) + for case in CASES: + _WRITERS[case](rng) + + +def regenerate_golden() -> None: + """Run the CURRENT pipeline per case and freeze its quantized output to + golden artifacts, using the exact same calls `main.py` makes on the wire. + """ + GOLDEN.mkdir(parents=True, exist_ok=True) + for case in CASES: + reads = read_input_tsv(DATA / f"{case}_input.tsv") + plan = read_plan(DATA / f"{case}_plan.json") + out = run(reads, plan) + write_output_tsv(out["properties"], GOLDEN / f"{case}.properties.tsv", sort_keys=["entity_key"]) + if plan["mode"] == "peptide": + write_output_tsv( + out["aa_fraction"], + GOLDEN / f"{case}.aa_fraction.tsv", + sort_keys=["entity_key", "aminoAcid"], + ) + (GOLDEN / f"{case}.stats.json").write_text(json.dumps(out["stats"], sort_keys=True, separators=(",", ":"))) + + +def main() -> None: + if "--write-golden" not in sys.argv[1:]: + raise SystemExit("usage: python -m tests._corpus_gen --write-golden") + write_inputs_and_plans() + regenerate_golden() + print(f"Wrote inputs + plans + golden for {len(CASES)} cases under {DATA}") + + +if __name__ == "__main__": + main() diff --git a/software/tests/data/characterization/antibody_bulk_full_input.tsv b/software/tests/data/characterization/antibody_bulk_full_input.tsv new file mode 100644 index 0000000..3bebe02 --- /dev/null +++ b/software/tests/data/characterization/antibody_bulk_full_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 FKNMQEVL VHLRTYRV KKIAEYEGQ HLAVTQ EPKEWNIVLI IDTLMINSLWGFAVTM WAFPFGTD HSWHIFIP YWFSEYA YNSRLAIVG SSVMDKFY HMLPCHCMIMRIK GLANWVCFNA CAICAIMD +clone_001 NQFHRQF LGMQPAQ VVRCWEQP ATFYTF MIGIAGVGDQ YEYRFYYCKMPACSDNLF ITNGPMKS ALTLVSCVWV CRPEPNS AKCKWLH TMPKHEWM WVNGFMAWCWF NLLMSPYQGA WCRFMAS +clone_002 YHDVQKGTG GWETVY QKLLAQKK TVMMHQFA FWPNRCVQYIANTG HNSAIWIKGQDWRIR EHGRDQPKK NYMDLASAKH PQPCWRN FWKMAPSTF DWNNAD EVSCMAMPFKQ FYFPLTCGFFSCTCV PGNWDDVGK +clone_003 KMKKTRFR FCWGTCMD RYIRTGM FSVCVDTM DEQYNWRMPT EFMAGFAMYH QCLPCYG DQCRNYYKL RQGARKHP NEEANAG YAMRVSS CVPKATEDMN SCFTLC PMGVFGG +clone_004 IYMASPCI LMGINI QRNWFPW GWAPGF AMTACCEWYF FPAQQWMIFNTHVPDFQ NEQQISPIP SPWDKKTN AYYSIKCY PEVCFPAQ PQERYRGG SQGWLTENNFNS TCHKGWMLPCLVQCQ PHNFFEYN +clone_005 AQWPRDDQ VFGFHGIA FSNYLMEQK MTMVFP LIPNPSTLQQEFFA YTEHYYTEKYGPDC ENSMERN SIGVVDTGA GTQYHRPK WFPGRWC DWPMITR SYEKTSVPSK IVNGLYF DDSPWQVD +clone_006 SIELFNEFC FWVHACPV YSESVNMEA ISLKIA NTMDDLWQINPFI HSNLPYFEP TSINNQKN LESLERFNIG SIEPPRT WYIPTLSI TADSMPI CWCQDKHMGEGNA CAPVAFEY HDRHATYQD +clone_007 IIQPSAQH CYKAWNN RFYTDKEE AFYFPHW HQTTEVESETRS RVMFQKP WTMIRIN QARAVPRIQI SSFIRLN SYFTDHLTE FMCMYG PHQSHGPCMTHMG VYWQFYSWHYRCI YMVHACCF +clone_008 RYIETQLM HGNVNRP MRDFIEFP VHNCAPH QWWQVHAFTS PVVDDYSAH YNLDAIYA FRKEWRKIP TMIWKDC MWWSDTS MPAEQYF AEQICKKPENL YHVVCMTVAG AKMFNEYV +clone_009 DKWETDWFSC INEVHGIP YRNGKNV HRMFKV DMLMCDYALE KHTVNRD TVQEDWRM LIQLCAAK MMVLCI TFKYCPQKW KMVSRE NGTCISPAIDLEG RANTQET SLKPQSC +clone_010 TNHIFILF VKVAGI QGIVFWGIT ARHCQGP PHPVTSEFDVADAI PRKQNPKDGWIHCCTD RIRFYTLAY NNNLTKFA HYPMSLR SLMNYPP PTLWIHQH TGTFYTVTSMKQ RTCHKRDAH EKAHGYGNY +clone_011 AAAYGLIPS NNDWQEAG YVKNHPH GGSFYWT WIACQYCFTDQLM YHCMFMKSSYMKDRG TKVPYRQ YQFEWEGS EQKKENT DACYAFEQP AMFDPWI HQACYPFVLQ VCRVFVFHWGD SLKKHPACT +clone_012 ALFFTFE AMFMWDD HKRMSSVT DMRPPV VQTDIFPTMFDFF PWEDT WLNSPWMY TNITCPNMTA WDSENQ IVATTECWN NGMKSS AISNGCSTGPLWS ARYYGYRM MQWFTQPIN +clone_013 LIHKIVWNRI MYFMIFKV VYERSHG NYIDVSHL KTHNQMACHHYGV DKTLSQMPCECM KYVRLVQ NIDDDTAWTM YEWIACEL WPFWYTH LGGWIFF KSLFYKHDFQG WANVSFKD YKYIAFHWA +clone_014 FFSVCTMIGR RIKHENS NVWLHLVP GPEWET RFQKVHGDKRM TCNPPVFVWFC YRTWQHRH DEVPETRGCH IEYPQD HTHHSDHET YHVWTY YPKSYANNHCHRH HAFIRSDYKLPML DYESNYYDI +clone_015 QIASLFTN FETMMDNH LTQPPCF DLFTHYS FWFWQKHEGFRV GCLSWDNEESI CAWDRKWKV GIEWTGSIAI YFFWYG EKEKYRW WQCDFRC MKQPALIHDWFI THRMAKQWTVFMWVPHLE KIKLDNFR +clone_016 PRCLTYQ YWTHGAK PKHSSKT FYSKGAH KEAQLMYHMQFE NSRGNQQQ WEDVRFEA SIPEQCLYGL TEVMAFA MMRHINW KVGRPAE EMHLVETLDE YLCIEGRGGRKGR WAVPAEELD +clone_017 HKPIRYTVNG AWQYILN MSRKNKGGI MANSYLI ERTGHPYMVF SQRRN HWHDSFD YKHDQAVSMA YLQRTL CDNPQPI SPVAQLS PQPWYLSVNLQ WGNCQRRLDIMIPAYMFG DEGYCIGA +clone_018 QGLPRLVCGS EESLLL CWKMRDCL RLVHPD CCDNALLCACYCF YGSDWFAVFEDDYEHESQ ESRTGVE NQTFDTCV EPNTMV WVPLIWDFY NLNTNAA FKHFGESSFK FCGNMHETS KMHEYIN +clone_019 EKPVNCMIPC VKGKCKD QAKIGKNT EGSHWYWD LCKAAYGMGPIWV HCVHRVRWYMNNEAQNGS PYAHYSQ GFQPRHKDGW LGEFKG KTQMVQSR KYMKKL YTASTTHQKQCAQ DKAASPVNVSTRV SWGWAYNHL +clone_020 DIEGYRFSMR FSKSMTIC TKPVYELF FFDMHCAS DWTCFRWFWKDLN RNCMEEY ELLWNGASP HDQKSQSPFQ QHKDFR NLRMIGRF MISFEVWY MGDMGKNEEPMML AHLGGVGSID TKECPDPRS +clone_021 FCEFEPYE DWWEHLP TRKMGPK GDHTNR MTQLRCFNGS IDFRQENI MFHIYVM MWSTKLAYYG HTVELRG CRTTVYW PQMYMIMS TASFADVAFEC LHDCIGNPTY NQHPCYIWA +clone_022 AQFHDWFMKL MTQSMKT RVMPMPKA RLPQFG HGYHDKTTGWGSE IQGECTCDEWFF HNNEEPSFM YIYWRYLE QEHFGK MGRMVEDP CHMQIM WEMDSYSLIPCNM DVKRAYG VWMRDPK +clone_023 LAPAREFLML FSRAYMFQ LGWGDQT VFQMFVN GDDIEHSHAPMKNW RTCLRQISA YPMHWNLGG HENPFLKYG ADGPQDD WIVDHMRVY GGYWYC KHVCSDACYHGSH LWWDFNVAYMLVI LAWQNDMKN +clone_024 LWFNCAY AQIQWRL GWRVDAC FPPYGI EACPPLGPAMQTHT NYICQSTCHMRGWHHCH LMMLFGW KCPEWSGPIQ FVRYNCY IHTPSIWDV FGLAME YTYGVQKANFC HRYKQCIWKHMEK NLCKENLH +clone_025 IIHHPEVKG PAVCVQE HTLDETQV GFELLGS IVWIDSHNQKME FRNTFDRPSMYLSFLCQ DRWGDEPIL QWLVAWC QTNIIGE VYFLNST QFYYGM AMFETMYIEVE STYSLMYDHCPKTMIW KDRFCERK +clone_026 KRAMMINNN PHAPWVLP AVEMAQFDF VLHNIW IQMLADRKPH RAGAGWC NCIISKL VMSYTCIFH EFECYV WYQRRPA YTMTHFYL LPYTCRWWIPKVLE MWKTKRREWLMDQMAEMW VHWAAAL +clone_027 ETASIKTLKH PYRSLMAW YLEMDFPRK KYIGVRR SEDGVCKNMNCP MGLSCVTLMKYMDNA YGNYLKSA GINIECPRPY MAARKH GREPVCKEF AKQTWL MSWMKMICTI TAPMCTHEVIAV FIWSVSQG +clone_028 KGCNLMKIPA EFYMWG CVGWFDF VGQPAN NWAIWANNMDWN DQHNWKFHTG FTEMWWVYE NKRKRGPQ EMFKGMR NNKNFQH GWRYQW ANFARQWECPL HQAYIPYSSYQPPC GGMETYRG +clone_029 IFKARMSNTI WASFKHN LYQIAIY FKWVHFP SNSYFAYPQNLFLS PYIRHGSRC QHTLQELKG HLFGNHWW CEFKWL GCKWNEQK YVTKEVV AMQYSMWIEQ DWKVNFCAFPN NMTWWPV +clone_030 EYVHHMV FCDCNHQ EQHMDEFKI GHPQHHAT QHTEYNMKTISW WCWEKQEC MMEYLEI WDEPSFMIF FCWCRS VIQIHYEMG LERRHSKS RGYHWPNQKPG YVSEIYN WKALRHPIL +clone_031 ANCQAWVAC QYLRMA PEQKCMCCN EYATEWVC WWQKEEGEFVI SYKPHCTT DIFPRDII FQSQKYH CVPFSWFC PWSRYQMV DLVTIM ITQKLPMMHELL EIQVGMHQ IKTNPDWS +clone_032 EGETKKAPPH YGCQQM PCKSWWAD FYVIGM FTLSIDKFYHIFA ARKLTAPRYGCGATPN MARTIKP QRSGHEV ETHVPMKI YCMKDYWGE ASNKQGC GGQHLYVWCCIAWN KRYLKTMNFDRVEEFGM HVKNNKTA +clone_033 QEWQRDE RFPGQVCN KLFDWRVM HVCIRTQF VCHLVHKCTNLPFC YLHRLACQ VAGENQIK TNWFDMHCPP MHTIWWW SIHKGGR CDIPPQ FYLLCWHLSP YNDRNI WCENRHLLE +clone_034 DYQHSHS PTSGFATR QMGDWYM YHSQCGK VLQWASDSLRVN YDLHQRNRFSND WHWFTLDG VWVRVSK AQNLQC CCIQDCPSY EKAGSY DAKFRGHIIV ACRSMFDCYCCIPKA KEVFDNMT +clone_035 VYYHVTLT GGNDEL QTKTFENP GCQVAW WMIACCAIMEDL RCEQIKKWY EFVENNKNC QLNKAFS IHDQNVD GYIVDLT CNHRWWH TERHNYMMYYAQE PQNIFNPPVL HHDPHGHCM +clone_036 DEYVYCNFGN QMLLMQG FRFQVNME FNMHEL YCERVYIARSDI HHYHNN LKAMYCN QDQVFRSV MANADRSA TCEPEQF WWLWPAV WHGYGRGRHA CTSSPKRMYYVEG CRSSKQICC +clone_037 PSIYWAVHW HFLVVGQ EKPHDYNPF QVSEKQNN NQKAHRNHDMAPQ WQIQMEAH LDHLDHGIW MLGITSAFSY ALRKHT FMLFGMPLN SQGYRHK QSCMSMCMSNRDD VLLYQMRENRQVCPVNAN WSTYQIC +clone_038 TVHYVHETP VCKMMC FTWAYYYQ SKYVILG HSIIKNHWMFD PHHYYHPGSLYDELHAC DRLLSPKI TYEMPNEVF SYWPTNKV FDFDKCI TNPVNETW AHVQAQQWVVIED MQVLHSRGWWLTVSNEAM RFVKCDRG +clone_039 LTFGWDITGR ETNMQN EQSYWTD YWRLAMVS VLEYNCWGLG SIPMVANMWR RQWWMQQ HGSCHCSAGE THEEQH AGPGEQMTI RCEEHTEE DYDAGKRWTFLLWW IMKFSGMHIFYPSP WGYALEC +clone_040 CFHLDHAWRP SHKDNSQ YACEWYSR RTAKIC MTKCPNHIHMA ECDINYEVPKHGRR IWDTVGEM DKQLHFEI HLKCEFVS FDEHYTII RHSYSE QVGAQWYFWFPN TVHHNIYSCQYRCVMH GAMSDVYP +clone_041 WVGIASEHR WLLAYNK DAYARCFDC LDDDIWY MMYYRAVYSP YAINNMGQH CAKYVHLF PKIACYG RYFIYMLP DTSIKKCNW WKILMTL PSEDQHGHKNTR RYQHVRMEHYSLWEA DIVQMTE +clone_042 SLSRCLV VSNYSFVI CRWIIWICL PMQNFW RFQGICNRREMKY LCHTLKI REVSMGL AHINVGN ILHTCPK PKDCAWFI AVFWYLGG SCSYLVINEHIFFK SLRLHMHFRSGTI PKHNLRFQ +clone_043 RQHYCKWTCS CQYYGAAY GDCKMALP PQMHIG RPMRVFWELRYEFL ITMQWMDNSAEFF KKFIGNMTI WCTHVSNG SSHRVPL ETRQYKM EFISVG RGTSFAIGHY MNSLM MMDLPDSYM +clone_044 QEEAFVWNI DVELQP MSCIKMQM DDCMWF LWKYITMVHH YRPFDKP MLQMHAQF DFITFSLWC RVFTPDY IRQVMVICA YPFIIYQ TGFCIEWAIWEDCH FHMQTQDN DMPETMCDC +clone_045 LCGENNLGR QHIDEQGS TRVTGAEAS HMWCDT KFKMQEPALVLQYM LCASFECHKIET HTLQLRS HHAHNGLHCV QVMHFLD CMCFDTE CEFPYL REHYYTVAIG WRKLY GTYPMEGEV +clone_046 SFAYCLHE KRSIKYV RLRIYVQE PHENWIA DAQNHHMQECSQLK YKPWFFADGCM NTRQHPCNT RWYPWKYWEG GVWVPKF WDLQLQYAS TLHAYT EQNPNYPHMACE TQEQMIMMNNFMMD MKLFQWPI +clone_047 IKRFLFK AINMER CKFSHIMDE SRLQNWV DEFPKDVWLTG SQINKKMGKHELPPMR EDVIGRHWN CFANRLWLM AWLGAR EQSDCRQI FWSRDNQ LSSRMWLDEEDIQR WRAVGF QVMGMGISP +clone_048 ENGREFMC ELLKGWHP VAKPWELN VPLLDRIL AIAEAMQFLCEEL LFQKNKQQWTLW QIGRLFEW FKLKEMEA DWMTCK SQEGKHIYM CCAHEC DLHTNQGITVWGR HCTYYQWSFQY TDGWGMYGY +clone_049 NYQQLHPQWQ IRDAWSTC MMSWLWTD DLGKID WHCLQKDTYC FIDQF WYNDHISWD TRGRYNIF LSTCPH RWFPCVAT VWNAFD EIGPNHDQHFISYQ NGKLPNIIH FPLHGCMM diff --git a/software/tests/data/characterization/antibody_bulk_full_plan.json b/software/tests/data/characterization/antibody_bulk_full_plan.json new file mode 100644 index 0000000..e383efd --- /dev/null +++ b/software/tests/data/characterization/antibody_bulk_full_plan.json @@ -0,0 +1,13 @@ +{ + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": [ + "A", + "B" + ], + "fullChains": [ + "A", + "B" + ], + "hasFv": true +} diff --git a/software/tests/data/characterization/antibody_cdr3_only_input.tsv b/software/tests/data/characterization/antibody_cdr3_only_input.tsv new file mode 100644 index 0000000..233526b --- /dev/null +++ b/software/tests/data/characterization/antibody_cdr3_only_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 "" "" "" "" "" YCLHSFCGVHNRQIT "" "" "" "" "" "" GSTYFAY "" +clone_001 "" "" "" "" "" PFCHW "" "" "" "" "" "" HSAPTVEKRCM "" +clone_002 "" "" "" "" "" CSFFYNRMCDHW "" "" "" "" "" "" ADQKHEMIMSVPVHI "" +clone_003 "" "" "" "" "" DSSARHAHILY "" "" "" "" "" "" GDSYPPGACDESY "" +clone_004 "" "" "" "" "" DHPMKC "" "" "" "" "" "" YTWSTVMEMKAKCCPNA "" +clone_005 "" "" "" "" "" WNGIGTYYGKRR "" "" "" "" "" "" HKGEIFD "" +clone_006 "" "" "" "" "" RSNAFHNIC "" "" "" "" "" "" KGWCR "" +clone_007 "" "" "" "" "" NWEPKMPMP "" "" "" "" "" "" MHTLSHCCKN "" +clone_008 "" "" "" "" "" GYWSLPIAFVKGPLYKAN "" "" "" "" "" "" WNMTMY "" +clone_009 "" "" "" "" "" TIKATGMT "" "" "" "" "" "" HYKNIPNVPMMQPLIV "" +clone_010 "" "" "" "" "" EKDMRFVSVDKHGEKM "" "" "" "" "" "" AMTAFNSEYIPIK "" +clone_011 "" "" "" "" "" AYNVW "" "" "" "" "" "" KMATYAWPIRSLFPA "" +clone_012 "" "" "" "" "" IQDDLDMF "" "" "" "" "" "" EEHEGHHVNDT "" +clone_013 "" "" "" "" "" KHTHKDHY "" "" "" "" "" "" SIDWNAWLGHAVRYTN "" +clone_014 "" "" "" "" "" IPFYNTTGSDHSNS "" "" "" "" "" "" YQKGASKCDYCNVSQW "" +clone_015 "" "" "" "" "" SEHCNPKNTYMFMFE "" "" "" "" "" "" NDKNPEMWFPTLKNGW "" +clone_016 "" "" "" "" "" EHSDM "" "" "" "" "" "" IKRGQHVTVRGSFSSVNY "" +clone_017 "" "" "" "" "" TCHGDMPSPYTPCWM "" "" "" "" "" "" GKGWGRE "" +clone_018 "" "" "" "" "" NYYIVYPMFWTYRT "" "" "" "" "" "" PPLMRW "" +clone_019 "" "" "" "" "" MPREFSSRGHGENFW "" "" "" "" "" "" GWVRIIVQSGCYT "" +clone_020 "" "" "" "" "" LTEASKQCK "" "" "" "" "" "" CVSSWTMSQVKY "" +clone_021 "" "" "" "" "" DDIHH "" "" "" "" "" "" MVGVPIITD "" +clone_022 "" "" "" "" "" NAGLP "" "" "" "" "" "" CFNYELFPQSSM "" +clone_023 "" "" "" "" "" WGINMYGWQPMMVQFR "" "" "" "" "" "" LPHATR "" +clone_024 "" "" "" "" "" LAALYACY "" "" "" "" "" "" TMNNDTWK "" +clone_025 "" "" "" "" "" FCDVM "" "" "" "" "" "" WKFNWI "" +clone_026 "" "" "" "" "" AQVMQPPRETARKE "" "" "" "" "" "" DSMTPPEVYA "" +clone_027 "" "" "" "" "" QPNCCYG "" "" "" "" "" "" GRLYSMLENG "" +clone_028 "" "" "" "" "" CRQKNHANIYTE "" "" "" "" "" "" PLGAQYNY "" +clone_029 "" "" "" "" "" GWQLHVKNYMPY "" "" "" "" "" "" WQEPHCHSMTIVIS "" +clone_030 "" "" "" "" "" SIEFFKMFCWWTPGPRC "" "" "" "" "" "" RNLPYQTDQ "" +clone_031 "" "" "" "" "" LIAQLYRWWSPM "" "" "" "" "" "" NYFNLWRFTHLDRFKE "" +clone_032 "" "" "" "" "" NTSWCVY "" "" "" "" "" "" QHKAWNA "" +clone_033 "" "" "" "" "" MMEHDVPCPWLIFVE "" "" "" "" "" "" VHHTQGQAKK "" +clone_034 "" "" "" "" "" DARLKNCQCYCHYAFDHT "" "" "" "" "" "" PQMEEIMDGGVISTY "" +clone_035 "" "" "" "" "" DQWTITMKTPREWA "" "" "" "" "" "" QDNLVCCNGEINTCM "" +clone_036 "" "" "" "" "" IWIMDDCEMSPPS "" "" "" "" "" "" NCFRDVMFQM "" +clone_037 "" "" "" "" "" RQYGN "" "" "" "" "" "" RINWYAWIVDID "" +clone_038 "" "" "" "" "" GQRILYYEASCAFC "" "" "" "" "" "" SFHQFCHGVAHDFVAHP "" +clone_039 "" "" "" "" "" VSYARTPKARGSHDHRA "" "" "" "" "" "" PEPGPYVQLG "" +clone_040 "" "" "" "" "" VKTEQDNAVSL "" "" "" "" "" "" PWRSNSTTIANIILKQ "" +clone_041 "" "" "" "" "" KLFAIFYSHVV "" "" "" "" "" "" SSCYVSCAYEIWDVIFQW "" +clone_042 "" "" "" "" "" LFHKYWRLKFKIVQH "" "" "" "" "" "" CTFKWNIK "" +clone_043 "" "" "" "" "" IATYDSKVHANPQRVNQ "" "" "" "" "" "" KKVLMWLAANEPNYSS "" +clone_044 "" "" "" "" "" NVFGAPWIEFHLKRYK "" "" "" "" "" "" LCTFIYNM "" +clone_045 "" "" "" "" "" WHSESKHPQCQCPDRHMA "" "" "" "" "" "" FRTRQTCFFISDCDVAAV "" +clone_046 "" "" "" "" "" DNCHSVDWHL "" "" "" "" "" "" AISGINCCR "" +clone_047 "" "" "" "" "" VPCCTAGVDI "" "" "" "" "" "" VVTICIRAFCGR "" +clone_048 "" "" "" "" "" SEMVQWRW "" "" "" "" "" "" IDKPSPGL "" +clone_049 "" "" "" "" "" LYKAHIIRADPMDSAN "" "" "" "" "" "" SCKWKHGYVKKT "" diff --git a/software/tests/data/characterization/antibody_cdr3_only_plan.json b/software/tests/data/characterization/antibody_cdr3_only_plan.json new file mode 100644 index 0000000..d608401 --- /dev/null +++ b/software/tests/data/characterization/antibody_cdr3_only_plan.json @@ -0,0 +1,10 @@ +{ + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": [ + "A", + "B" + ], + "fullChains": [], + "hasFv": false +} diff --git a/software/tests/data/characterization/antibody_partial_input.tsv b/software/tests/data/characterization/antibody_partial_input.tsv new file mode 100644 index 0000000..600bfec --- /dev/null +++ b/software/tests/data/characterization/antibody_partial_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 "" KQRRQQQ EFVTVYWQ AMGPTLCM AAINHDHCHY GNVVKQESFQYANG SMAAKHTWV PYSWSHIN WNVQTLQ LSYECAKP PHCPWTAM INVMWYCCIRQSK YDAKDEIVW IGSQENCD +clone_001 EEPSPGMS VHLMWI PTWLKEP NDKNHWQN ASAIDLEWKQ KEWPH NHFRDVPET TERHGMGY LQQCHV SGIRRAP KHWPHD EPHLAENTIW APCFEFDRNHYGKDQMLM VEIHPQFYD +clone_002 ILQLQCCP KAPEAEW LCAPVYV SEDWCQ IIADEHHRNVM NYAQPPWSQMFAHHPP SKVVCQM AQMGEIMP MMGHWIPE WVYILAGW NMTMWQR NDINNHCMTEN KWADKFRKLDVDYFWEDV YCFVMED +clone_003 WGNFKTSK VALHNH SMKAYFKI LFIMFVD SMYDILGCKA DLKMQ FYTEYQRN WSERQEQEL "" FGTDIHN LRSTYCM LQVSWALQAHDI TIAQDWTID EWPPERDA +clone_004 GNSIHLNHF SHSGFM AFKDRTVI IRVCNEQ WWDSNHDGDAQE WMEGMLTEVF QGFCMLHI RATKVEYAH WQAFRP TRMTRSDK QHDDND PQHMSCMTFLLWA CGVKMPY PIHMSMGA +clone_005 FKITWPEQR QQDAHLA WFGPGHQ YTCGACYF KKYQHDDALHVT AMTDVMIYVDRSSVW WRWWRIWEV YCCQYQS AMAQWH WDWEKYQF LHAGQVIN FPRPEPLCKI PWGMITC PQAIDNGS +clone_006 VESTNSA TGRHSF "" PWEIQS LTEDRQPHFNDA IMDFITQCL DYQYDPT YDEHDQY WCPMWPAS LNSCKALLI LIVEHI QEATFGEGPVVHR SHIDLFYCQKLTVKVYS KVMPDWES +clone_007 FMNDQKP LTLWCT TFSHQCHL KDWRGYKD CGGQMMADFNNQ NDWWN YMFREYVRF PSVLSHQMAS FYLMHEH WQCSMKM SEAANKQW PIWDWEKWAPL NPGNPYWEVQMKMEYG IHERGAYG +clone_008 CLIGDVMSY WWMACM EQSPWNK WWCIKHVA SATAADGNRSVEPG RGNRGDRARPYDC WDEWFRGW ECQNMEN SYECHEG ASQVFYAF PEETVIIY EDCYMHNEMF IWCTVSEWFW SFTGSNFC +clone_009 KNMIKWQL MYASLFKH SLSAENI VIPHPEGK LGVFWRQPQI VDDQMLSQQNPSPICELM CMLMSDYWF YEPIALI EPQKDAI VADTWKFI "" ASFNQPMRFY YAHQHDKTEYLRPHP EQCDMML +clone_010 FYAIDLCI QWQMES DPSYCEGSQ HQLGDER LGFPFIEPFH SNWYEAP QKKMTVV HNDNCIT PDYDTDYL PAWTPQE HSGCDHV VKWRIGPSEKK QDVTPNYEEFLIRWHHSH HHKQKVVIK +clone_011 YYFSIAMDSP RCCRII VIPAISHTV TPNTSDCS EHTRFLEWVVC MCKFWDTMPTRWYWL ESNHAET TWHWMQRMKP SDQVKHHF NSNRKLC MNYINAF DNQGHDPWMKW GPAWFECSAI IRMNQVNMW +clone_012 DKLFAHML RFGETWTD PQPCRWL EGHVNPY "" CLWRQIFEIPF WTSHKLHI ICYLLYSVL PQNEAFNE DHKQYSCQA NDRLHMRP AKNESMYLYHWIY HMKAIS QMTFSFDT +clone_013 HNMLDQVDT QIKGFIGE LKCVEQL YMEDPDVG SYIGQQLYPQ DFLDERRLRWMV KGSAQEG ASSVNNTW WRMKICVY DGRMFYVQ DFSFDQK CVMTQCTLWKWAW KFLRRYS ASEFGECI +clone_014 CHPHDTHLVT VHYPFDEY WILWAERW NYQCCHT VCFNMHAHKF FNEAAY KKQWSMF EYTHHVGFW LFFLYCHH GPKLVICFF PAWRDQ VCNQIEEVRF VQRKVCY PYMHNNVQ +clone_015 HRNYPPHFPI HLACQV EMCLYHHI KVADCF PEQIRCGTLYWG IAVNRAECW METNSPPKK HWASFPKLTK YFPTDWCN LIERVGYV GNMASS DGWIQFSAPGH "" EWAAHYNC +clone_016 TWRYDTMM NMKYNRVH QCMNQEHFS RLVNVGCI MDRFQTDHDTGD PHTTDMEAW FAFFGLG WYCYGMMQ WTGWMTLF REGKGNRL CRPALH DYINIGKDMGWWDE VDWHPI NEDYTMVMY +clone_017 WPIRCDDARY NELEDFHI CSEHHRIWF HIHGHDIG EVLRNFGGKKYLV IFGACRGSF MNQHDKH VSDRWRLC NAKIHDI PREDYPL VKWYVSQS GISCEYMGEQDSFP CTITGDY GGMKNQIL +clone_018 TWYQDIGH MLAYQHLM YSGYITADV VQCKMCAS PIQYNFQLNNDNG DNYTIPCEHFPGHESI "" ESGWWRA KSTKTH YQTDDWTD NTTICDR STLYLGFCFGLCM PQGCGQDYMVSWMT GFASGIAMH +clone_019 VWTLYHDHKH VAKESQ WMTYHATKK LPATPD IIFMTPIPQMN LLNGADPR IMWEFNG WIPIISCQC TCNEKAY ALRRHCNPV EEQDIFV HAYKDWFLKGCDP DHPRAAPK DWEWVKE +clone_020 QMHVPMCQAQ TTYYACAI HALEIQYI VNSPWNA DYKWLQTGRQ LYKAKNGMMRN VQGSNLGH GQAASGIVIR YPFQDTA NKTTMGQMI DRAENCMP WGDQVQEQPG DQMWQFMIPTP EHMASPQ +clone_021 TSQFRMKGWV IGDPGSGH PFGCGHED NRGTDRW QPPFYTYPEGI QPAKGQVSGEVT KRTQYLFLV "" RVDWWWNC WGIIQMKCI TLDIYMGP DKLMDVMAWVLR ERSCAFQGEAIP VANYTVWT +clone_022 FSCYHPWKGQ SCRSDATW GMMRNFPQ CQMCWT VRFEFPENWIANA TVNTNC QKNYQEEGN QEQEVDLGT FQILGET LYTCPQEIG DFISMVR RLRAADSKWLQY NIIPLQ YMCRRGMI +clone_023 MPNKKSTSY MQEAALIH VFGESTMQY NNSRLNC ACTMHVTFQPFPL FHFYHPY VTAEKSQLI FCNNLKDA ECCHFIP KYVFQCT GSAFMM MRMMTAVWTHIPG PCCWPKR CQQRCPH +clone_024 QASIFDDDND "" IMYNDKW SEEEPILT KSQHSWRPAFN YKEIAKGYWLGHN CDRTCADV IFWANGA WNEVFT MICALSSCC VNQGDHT SGVEERFAWFH KLISWMLFDPATSICCPD YSTMWRNMH +clone_025 AHMMIVR MGFWFYM PNQNNMA CPWTPT GIILHEYASRKPKQ QNDTCKKDYQY NVVVLVQW DSDLSGGM MYLYHIW FIEADYDD SKCYFQ DYLKYWSHDWE PEKFVHQIHWERWHYY VDYPWQD +clone_026 TDPAGFDAP QVMWFHD PWSWMPEWY NAGEAKW MEWVIERSSIAVIL IMPEHGTVYQ DQRAFAGVE WMRKAIWQML KVRTIQQK PYMHEIRFC IQVHQD QPSGWWQFYG DECTKVH LCWKALG +clone_027 QWKALIRGR CPVSEKMC LVYAGEHR FLTYQC KSCSQTLAWKI WTTVGTPGYR MNYVVNNWN FYWDYAPAR ITQPLPIW "" PWMKAPET NAYRMDTVAKWDV AVVIINRVCACK QPTHLAIG +clone_028 NSHPMEHCYC RIEMGFSK VNAEIRQRY RCPERYDM FGGNQHEQLQHAI EAWNRYHMAKFFWLCHKC WKDWDPQV NWKEWGWLWA VPTIQG FYEWRNVV LGACYYPQ KRNWEMPHHNAW NTHYINFVPD SHPSQYKC +clone_029 WQEWQWIG NSYAGSV DMICYLCQ WNVDMDV CTPDPRTHNATPTR TYWPHCICLLTPR QPEQRLSH WKPVHQD PSVAPRI GVTYVYI HSLTSIEH VINHTRVNLFDLVK PESRYQNWW DPYKEKCKT +clone_030 DIRFPHN KNFSGLF YKPVFCT "" MKISMLWMGYAAK CEPWGGW ELHKFNHV IWCIPCKW SMNSSVE YEKCEVCC DRMSTMSL WMAQPCFSYWHLV KHSNNYSQGVVEVQ DWHKYQHE +clone_031 WIRYVIWKAL PNLAIKP DNAFRMA VDCKPRPV FEAYYQMHVKNTM RTGCRLIMHSMWSC WSNKYLRW VTEDVFYKW SKDDGHMG VKPHTDK CTMGFVR IQAGGKEAVKP ACGIH KTSQATE +clone_032 WIGYIDDRN NGQFHH STLTSASTP LQHYLYDP ACEAEFALGCYI FRCNLRITNPYMS IWDRCHK PNTPLWN YVTWCD ISAQYHIKN MEANGT MLCITIYSEVR VMKWYICRM RGSVEIMT +clone_033 NWQMVVDTV FNISFKD MDKVCAR CNVCQV KASIDVAHMNGTFP CREWRYMCGLGTHIYPFH YRCGNQEE NRVYMENCRN TFLEQET WGEHGIW LLARCHR "" LFLGVNGISA GKPAWTQD +clone_034 PDEHQKFK SGPHLPTH DRNHPQC NVHTHN ISDHNNPRITFTE PQLYNRTVIKNNHDNYFD HECQTNYQ TGKAITSA DVIIPRI NFFMGPV NMPGRPF MGYCCRKIEAYSS IYFWHLVC CVTRQIDN +clone_035 IYHVRDECG VMTSQRFD YKRFVFYY LVPTSMR WRGRQWEQYFYAHH IVRTNGI QPMHTQYEH KWVPAHCSM NFQKQVG QYCRILY LFHEPHDQ ILVYTPGASTWQ RGVTFQ AVALMMMD +clone_036 KNKGGWIF SQRFCNC EHYAFLCI YAECGASY FLLHNTEYNCM "" LFLMDLNM CGGWPCLCQM QKHGRI QCAIQSIM IMKWLPLF VQAPLTEDVW GMYNWWYA REGRCIVI +clone_037 SNDKYGSE WEVVVRFT NTTMWVPEF SIQRTCY GQGTWQGPDK FHRGSICH CKCVCNVHR ENLHIGEQ WRLLFCW AVYDQNRCQ PGHRCEF LSWNRLDIWWVPPM PFQRPLILH CGANPVFHV +clone_038 HAEEHECQKY FFFYINSY RNRNFLINT SHGYPE CYDELMVNVDQEYG SKHNYEWQCERYS GQDKYLAGM KWSHYRWHS DKISNI YMSFMVW SFMHEH RFSKAAHFIYMIH RLSTQILIPDGDPMD AFPLQHT +clone_039 EDVGENIWLN CPGLRG KDYWLFFG CQLWEPM TALGCTAVNLWSSA TLVYWC SRGIASCM FEEGQFCQQI GNGDCFYF LIVGKHAAQ WVMHHC ALEMWNDHMWTIKK MRAWVLNNVICQWCFLYS "" +clone_040 HMQSPERFVV EAVKASEN FCHCGDV WPRGMTW WWHRRTHAAVA PAFCGLNI SMRWYIT MKNFWEARC EHFGYWE ENLFWKKCN RMFGTTF HIDVWWFSTKDGY FLSIG WYNTCIVQV +clone_041 AWAEICANHC SCVIKFML RAAWWKR PQMKCEA SRGIEDFKQAET KVWYLIEGFYAG YDWVLTR AFIWHSQYWQ IWTAPPF MEIDHNQFG DSCMDS PVEIENVTLQ LKFCQMEMWMPHDGN VYCMDQT +clone_042 "" ASPCAPY IKTKRLE QERMGR DMSTIVDKHDSA LHEPL EVLGMCEA HYMQIAIISS TWGMISC YMWYCAMAS VVQTTLAD DYQATKAAICMAKS QKYASKQTSSPQTV LSNNYGW +clone_043 ICEWANFNV VLPRRSAT EQAWVNLMQ CLFKSM YIWWCAVTVNV VRQVVTH PYQAIQA IMSEVYTG KSSMLQNF WRMMESFFL LPGHHQMF CAVYAAPVQIRTDA GRNTTLRKQYA DMSFQSEEH +clone_044 ATEFECI EDFDHV YWSWGYWK HAMWHR DSRNCTHDHSGE RTAAEHDPAFNLICEM HKWPYNV IEYWYVFL YRNNRINM YAFPRNSL VENGVY NQNFIAWVSF YQITWKFCYVSSCWPF QECADHFWT +clone_045 KRFLMMHKN CPTVSR HYDSEKRYI QVNPWVKC MFTGCDIIVV VSSTENEMHRDDESFS DRFYTKTF DCCLKSIR "" DWIQLHL GKNEFN QEHNPGGPKYFCNA SMYGYHDCH SYNPSDD +clone_046 IEIQHHR AIGAPG VNQDNQA QNMMQNN METVVEYVQDIEL KKHRWF AWSLVEEL HKIMWLH ETVRSNFQ PQECIRM LPSAAWCI MIFRSAVKCALV DQVQMPH KDKQGFG +clone_047 KKMPLGEM HFRSVIL CMMYIAG SMVRINDL YLEFTYWASG RIGYIYGPAWFI PNYSDTWT EHREARAFS DFQYWT GQNNYYM CIKFLGLL TAAMQWCYHMIMT KDWDFMTGNWFAK MMFDMELCD +clone_048 ANWWEKPNWW GEFLKANM "" ADEDPVIS PSIAYPGTWN MHLCYDMSCGKDPSFWK FVCGKFPA RWDCWHWG VVNGHASV RVNWNSDQS YRPYFPQ PKCLHCYLLN MHIFKWWQDDPGRK CVMARCANN +clone_049 KFKEKKM PRFMIFWT CHKQKCMI DREVMWH DMCKWCDAHHE DYCWWSA EQRRDPIRC MLRTCWNYSS YHDLKC DMSTGGIV PGKNTH TVDHWHYFFFF HPTQTLICSFNEQ VYDWYMI diff --git a/software/tests/data/characterization/antibody_partial_plan.json b/software/tests/data/characterization/antibody_partial_plan.json new file mode 100644 index 0000000..e383efd --- /dev/null +++ b/software/tests/data/characterization/antibody_partial_plan.json @@ -0,0 +1,13 @@ +{ + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": [ + "A", + "B" + ], + "fullChains": [ + "A", + "B" + ], + "hasFv": true +} diff --git a/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv b/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv new file mode 100644 index 0000000..8c39868 --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion charge_Fv chargeShift_Fv pi_Fv eox_Fv ered_Fv mw_Fv +clone_000 -1.1 -0.22 1.16875 -0.105 -0.219 0.86 -1.786 5.819 0.06308 7668.8663 19480.0 19480.0 16.0262 98.9231 0.13846 0.273 7.693 0.52381 7407.7532 22710.0 22460.0 46.7906 85.2381 0.1746 -1.513 -3.323 6.328 42190.0 41940.0 15076.6195 +clone_001 -0.197 -0.473 -0.46667 -0.108 -0.227 0.02 0.185 7.604 -0.26094 7497.5497 13075.0 12950.0 48.6628 50.3125 0.1875 2.209 9.386 0.12833 7168.4875 40240.0 39990.0 48.8483 60.1667 0.18333 2.394 -2.567 8.968 53315.0 52940.0 14666.0372 +clone_002 2.051 -0.994 -0.95333 -0.119 -0.254 1.41333 5.247 9.468 -0.95362 8213.2778 26470.0 26470.0 15.3087 56.5217 0.13043 -1.826 5.562 -0.33134 7781.8167 25230.0 24980.0 75.4882 32.0896 0.19403 3.421 -3.102 8.785 51700.0 51450.0 15995.0945 +clone_003 -0.89 -0.892 0.46 -0.109 -0.228 1.68333 2.179 9.247 -0.36897 7071.2847 17210.0 16960.0 33.3983 26.8966 0.18966 1.182 8.836 -0.65556 6003.7662 4720.0 4470.0 53.2981 39.8148 0.09259 3.361 -2.408 9.067 21930.0 21430.0 13075.0509 +clone_004 -0.885 -0.854 -0.27647 1.039 -1.011 0.23333 -2.803 5.037 0.0746 7471.5967 30605.0 30480.0 66.1429 62.0635 0.20635 0.21 7.509 -0.88358 7939.8264 24200.0 23950.0 55.0149 34.9254 0.1791 -2.593 -2.61 5.932 54805.0 54430.0 15411.4231 +clone_005 -1.974 -1.129 -1.69286 -0.108 -0.227 1.44286 -5.771 4.749 -0.71061 7890.7192 12950.0 12950.0 37.9561 42.8788 0.18182 -0.822 6.13 -0.60893 6476.1842 26470.0 26470.0 35.2232 55.5357 0.16071 -6.593 -2.689 5.16 39420.0 39420.0 14366.9034 +clone_006 -0.89 -0.892 -0.98889 -1.109 -0.266 0.8375 -5.774 4.634 -0.12419 7252.1106 14105.0 13980.0 43.1935 81.7742 0.14516 -5.738 4.877 -0.47419 7113.8895 15595.0 15470.0 60.0935 63.0645 0.1129 -11.512 -3.184 4.763 29700.0 29450.0 14366.0001 +clone_007 1.835 -0.351 -0.65714 1.09 -0.904 -0.28462 0.248 7.583 -0.94286 6978.7774 20970.0 20970.0 57.5486 43.5714 0.17857 1.343 9.69 -0.05758 7986.2072 20190.0 19940.0 55.2305 53.1818 0.19697 1.591 -3.952 8.756 41160.0 40910.0 14964.9846 +clone_008 -1.891 -0.88 -0.41111 0.104 -0.871 1.32 -3.682 5.539 -0.45614 6802.5204 16960.0 16960.0 60.0351 68.4211 0.15789 0.173 7.398 -0.40339 7197.3867 26595.0 26470.0 37.1797 52.8814 0.16949 -3.509 -3.365 5.908 43555.0 43430.0 13999.9071 +clone_009 1.051 -0.994 -2.15714 -0.098 -0.241 -2.08571 -2.758 5.611 -0.86964 6889.7488 19605.0 19480.0 8.8607 55.5357 0.125 2.141 9.026 -0.06607 6279.424 7240.0 6990.0 52.6946 80.1786 0.05357 -0.616 -2.738 6.588 26845.0 26470.0 13169.1728 +clone_010 0.981 -1.16 -1.575 2.263 -1.637 -2.13333 1.267 8.838 -0.01594 7868.0023 14105.0 13980.0 29.948 84.7826 0.13043 4.321 9.785 -0.98333 7092.9512 12950.0 12950.0 59.4018 42.3333 0.13333 5.588 -4.129 9.386 27055.0 26930.0 14960.9535 +clone_011 1.969 -1.176 -0.84 0.111 -0.863 0.74545 1.232 8.729 -0.59242 7794.7757 27055.0 26930.0 33.5335 40.0 0.19697 -3.755 5.35 -0.47377 7289.1607 21220.0 20970.0 66.5082 46.3934 0.19672 -2.523 -3.36 6.092 48275.0 47900.0 15083.9364 +clone_012 -2.097 -0.25 -2.04 1.878 -0.261 -1.2 -5.806 4.164 -0.14444 6690.6107 23490.0 23490.0 66.1056 41.4815 0.24074 -0.839 5.984 -0.50656 6929.6984 26595.0 26470.0 33.377 43.2787 0.13115 -6.646 -2.104 4.677 50085.0 49960.0 13620.3091 +clone_013 -1.169 -0.407 -0.40833 -0.164 -0.359 -0.475 3.319 9.299 -0.14308 7885.2709 13075.0 12950.0 53.2615 86.7692 0.12308 -3.762 5.251 -0.215 7508.3717 45950.0 45950.0 25.2435 58.6667 0.31667 -0.444 -3.995 6.753 59025.0 58900.0 15393.6426 +clone_014 -0.109 -0.228 0.97273 1.044 -1.011 -0.01538 4.301 9.798 -0.59016 7527.6423 23615.0 23490.0 32.8967 54.0984 0.16393 -6.532 5.494 -1.33939 8164.7084 19035.0 18910.0 68.0894 41.3636 0.16667 -2.232 -5.415 6.36 42650.0 42400.0 15692.3507 +clone_015 -3.098 -0.288 -0.55455 1.269 -1.65 -0.33333 -3.752 5.324 -0.45161 7606.5037 29115.0 28990.0 24.9 50.3226 0.20968 2.229 8.886 -0.40147 8645.0111 43095.0 42970.0 28.05 64.5588 0.23529 -1.523 -3.441 6.337 72210.0 71960.0 16251.5148 +clone_016 0.899 -0.211 -2.9 2.827 -0.406 -1.0 2.264 9.004 -1.32679 6763.4468 16960.0 16960.0 25.5805 26.25 0.16071 -4.764 5.108 -0.29365 7234.2798 14105.0 13980.0 69.3794 82.0635 0.07937 -2.5 -3.541 6.124 31065.0 30940.0 13997.7266 +clone_017 1.899 -0.211 -3.36 0.889 -0.245 -0.02222 4.265 9.856 -0.9 6623.4423 16960.0 16960.0 64.4873 56.7273 0.14545 -1.811 5.554 -0.27015 7663.7041 18575.0 18450.0 61.5761 78.6567 0.1194 2.454 -2.972 9.024 35535.0 35410.0 14287.1464 +clone_018 -5.887 -0.995 -1.2 -0.886 -0.884 -0.54444 -8.762 4.371 -0.18824 7856.7945 15970.0 15470.0 59.7031 73.0882 0.11765 -3.752 5.309 -0.43393 6636.3747 14105.0 13980.0 36.0484 46.9643 0.17857 -12.514 -3.24 4.715 30075.0 29450.0 14493.1692 +clone_019 1.322 -1.535 -1.20556 0.835 -0.36 -0.23846 3.251 9.084 -0.71972 8215.3548 29700.0 29450.0 33.5437 49.4366 0.12676 7.208 9.772 -0.96923 7499.4489 20970.0 20970.0 40.5262 43.5385 0.12308 10.459 -3.612 9.449 50670.0 50420.0 15714.8037 +clone_020 -1.106 -0.296 -1.7 -0.885 -0.854 0.56 -2.811 5.303 -0.36984 7781.8367 26720.0 26470.0 53.1413 44.9206 0.22222 -1.758 5.919 -0.8 7443.4219 6990.0 6990.0 63.3875 47.1875 0.09375 -4.569 -3.008 5.639 33710.0 33460.0 15225.2586 +clone_021 -1.097 -0.25 -0.8375 -0.896 -0.88 -0.34 -2.745 5.62 -0.79811 6583.4315 14105.0 13980.0 39.417 42.2642 0.16981 -1.748 5.884 -0.0629 7274.2983 25690.0 25440.0 29.4919 59.8387 0.17742 -4.494 -3.321 5.74 39795.0 39420.0 13857.7298 +clone_022 -3.101 -0.297 -0.075 0.828 -0.376 -1.08571 -2.725 5.726 -0.79385 7742.7231 18115.0 17990.0 58.1277 25.5385 0.15385 -1.784 5.808 -0.63273 6865.9605 24075.0 23950.0 29.7818 53.0909 0.16364 -4.509 -3.444 5.76 42190.0 41940.0 14608.6836 +clone_023 1.895 -0.22 -0.15556 -1.107 -0.236 1.3 -0.749 6.323 -0.22188 7519.5852 19480.0 19480.0 51.7297 65.625 0.15625 -4.699 5.296 -0.44091 7911.8157 36565.0 36440.0 23.8758 63.4848 0.19697 -5.448 -3.693 5.678 56045.0 55920.0 15431.4009 +clone_024 1.743 -2.792 -0.85294 3.13 -1.955 -1.76154 0.313 8.125 -0.03231 7683.927 32345.0 31970.0 64.2877 60.1538 0.16923 2.259 8.912 -0.54844 7784.9566 24200.0 23950.0 15.6031 59.375 0.17188 2.572 -3.958 8.871 56545.0 55920.0 15468.8836 +clone_025 0.889 -0.245 -0.25294 0.033 -1.036 -0.25625 -6.71 4.909 -0.31739 8055.0366 12615.0 12490.0 44.6536 84.6377 0.10145 -2.811 5.282 -0.17581 7641.7745 25565.0 25440.0 41.8227 61.2903 0.20968 -9.521 -3.249 5.042 38180.0 37930.0 15696.8111 +clone_026 0.895 -0.22 -0.01429 0.778 -0.559 -1.16667 2.237 9.337 0.01071 6455.6119 16625.0 16500.0 31.1393 90.7143 0.08929 1.247 8.812 -0.20145 8860.3881 47565.0 47440.0 55.632 63.6232 0.23188 3.484 -3.323 9.083 64190.0 63940.0 15316.0 +clone_027 -0.175 -0.385 0.51333 -0.886 -0.884 0.93333 4.16 9.264 -0.45507 8020.3601 14565.0 14440.0 39.9 63.6232 0.11594 2.209 9.145 0.08033 7014.3382 18240.0 17990.0 84.6049 71.9672 0.09836 6.369 -2.744 9.22 32805.0 32430.0 15034.6983 +clone_028 0.267 -1.629 -2.0 0.09 -0.904 -0.83571 -3.785 5.06 -0.46333 7350.2281 47105.0 46980.0 20.0985 40.6667 0.25 6.204 10.042 -1.42787 7427.3096 24075.0 23950.0 40.9787 17.7049 0.18033 2.419 -2.885 8.781 71180.0 70930.0 14777.5377 +clone_029 2.103 -0.871 -1.03333 -0.168 -0.368 -0.25455 5.26 10.102 -0.30794 7596.6659 18450.0 18450.0 32.4063 72.8571 0.20635 -0.794 6.257 -0.4614 7202.2386 47105.0 46980.0 22.7228 49.4737 0.24561 4.466 -3.316 9.103 65555.0 65430.0 14798.9045 +clone_030 -1.166 -0.428 -1.4 -1.112 -0.274 -0.24286 -7.589 5.249 -0.96034 7309.1589 21220.0 20970.0 46.7793 38.6207 0.13793 2.274 9.248 -0.63729 7325.3584 28085.0 27960.0 61.1254 64.4068 0.18644 -5.315 -4.842 5.889 49305.0 48930.0 14634.5173 +clone_031 1.04 -1.011 -1.2125 -0.883 -0.875 -0.4375 -3.807 5.021 -0.30847 7068.0792 26845.0 26470.0 49.6593 56.2712 0.15254 -0.759 6.32 -0.15439 6929.0975 19605.0 19480.0 42.7649 75.0877 0.14035 -4.566 -2.931 5.615 46450.0 45950.0 13997.1767 +clone_032 2.824 -0.376 -0.7125 0.771 -0.576 -0.92941 4.193 9.413 -0.37424 7528.7168 17085.0 16960.0 35.8336 50.4545 0.15152 2.257 8.813 -0.79286 8284.4152 22710.0 22460.0 47.5986 47.2857 0.12857 6.449 -3.45 9.117 39795.0 39420.0 15813.132 +clone_033 1.104 -0.871 -0.075 -0.107 -0.236 -1.96667 2.272 9.254 -0.32131 7374.5451 12865.0 12490.0 43.1705 76.5574 0.11475 -1.688 6.053 -0.57818 6895.8474 36230.0 35980.0 83.2856 63.8182 0.18182 0.584 -3.952 7.689 49095.0 48470.0 14270.3925 +clone_034 0.109 -0.88 -2.1 0.814 -0.411 0.48 -1.693 6.041 -1.06066 7365.9152 27960.0 27960.0 50.9525 38.3607 0.18033 1.169 8.606 -0.05574 6975.1083 10470.0 9970.0 46.5984 63.9344 0.11475 -0.524 -3.244 6.673 38430.0 37930.0 14341.0235 +clone_035 1.763 -0.546 -1.61111 -0.101 -0.211 -0.0 -3.812 5.021 -0.38793 6865.7803 21220.0 20970.0 37.6138 65.5172 0.13793 -3.597 5.783 -0.89167 7301.0603 17085.0 16960.0 73.9917 56.8333 0.13333 -7.409 -4.052 5.52 38305.0 37930.0 14166.8406 +clone_036 0.538 -2.131 -2.98333 0.82 -0.423 -0.78462 -2.713 5.708 -0.48214 6945.8559 9065.0 8940.0 36.1589 60.8929 0.17857 2.217 9.407 -0.65 7259.1533 26720.0 26470.0 90.7823 39.3548 0.14516 -0.496 -3.319 6.689 35785.0 35410.0 14205.0092 +clone_037 -0.883 -0.875 -0.8 0.891 -0.266 -0.36111 -3.604 5.791 -1.03175 7635.3661 24980.0 24980.0 22.3937 61.9048 0.12698 3.211 9.796 -0.27 8205.5071 11710.0 11460.0 67.7971 61.2857 0.11429 -0.393 -4.164 6.781 36690.0 36440.0 15840.8732 +clone_038 -1.26 -2.846 -0.95294 0.117 -0.875 -0.06111 -0.614 6.573 -0.28333 8016.1788 23045.0 22920.0 63.7758 73.7879 0.18182 -3.78 5.19 -0.44366 8510.5249 30605.0 30480.0 21.8014 62.9577 0.15493 -4.393 -4.282 5.923 53650.0 53400.0 16526.7037 +clone_039 0.899 -0.211 0.3 1.043 -1.002 0.34286 -1.831 5.342 -0.52414 7166.0792 42970.0 42970.0 32.7586 58.7931 0.18966 -7.642 5.083 -0.68235 7946.7532 26720.0 26470.0 94.8428 40.2941 0.14706 -9.473 -3.447 5.134 69690.0 69440.0 15112.8324 +clone_040 0.047 -1.079 -1.58571 1.524 -2.165 -0.3375 0.331 7.584 -0.9 7704.6904 21220.0 20970.0 72.8016 45.7812 0.10938 -3.627 5.736 -0.42727 8070.979 20065.0 19940.0 49.2455 60.4545 0.19697 -3.296 -4.976 6.155 41285.0 40910.0 15775.6694 +clone_041 0.107 -0.862 -0.8 0.322 -1.573 -1.23333 -1.756 5.878 -0.17458 7163.1164 28545.0 28420.0 53.0644 72.8814 0.22034 1.265 8.594 -0.72615 7989.159 24075.0 23950.0 46.7938 66.0 0.13846 -0.491 -3.559 6.708 52620.0 52370.0 15152.2754 +clone_042 1.047 -0.994 0.97143 2.33 -1.48 -0.1 5.182 10.245 0.32105 6943.331 19730.0 19480.0 38.0842 102.4561 0.14035 4.321 9.804 0.08923 7611.8578 14105.0 13980.0 55.0523 90.0 0.15385 9.503 -3.451 10.059 33835.0 33460.0 14555.1888 +clone_043 -2.097 -0.25 -0.05385 -0.101 -0.211 0.66 2.206 9.113 -0.31765 8270.6399 24200.0 23950.0 47.2941 50.2941 0.19118 -0.749 6.321 -0.27885 6000.8431 9970.0 9970.0 54.4192 56.1538 0.11538 1.457 -3.06 8.562 34170.0 33920.0 14271.483 +clone_044 0.828 -0.376 -1.94286 -0.885 -0.854 -1.65 -3.755 5.286 -0.22778 6768.9263 19605.0 19480.0 91.0204 63.1481 0.16667 -7.767 4.086 0.16508 7669.7879 21345.0 20970.0 40.9365 66.5079 0.19048 -11.522 -3.09 4.622 40950.0 40450.0 14438.7142 +clone_045 -0.951 -1.062 0.19167 1.828 -0.367 -1.36 -2.718 5.737 -0.46308 7423.3879 7240.0 6990.0 73.6031 69.0769 0.06154 -4.649 5.482 -0.2537 6479.3434 13200.0 12950.0 44.6296 61.2963 0.16667 -7.366 -4.342 5.593 20440.0 19940.0 13902.7313 +clone_046 -0.175 -0.385 0.01818 -2.097 -0.25 -0.35 1.3 8.7 -0.86562 7799.7844 17210.0 16960.0 40.8141 51.875 0.14062 -2.783 5.442 -0.60606 8281.4835 40450.0 40450.0 48.9833 48.7879 0.21212 -1.483 -3.621 6.369 57660.0 57410.0 16081.2679 +clone_047 2.925 -1.295 -1.3875 0.899 -0.211 0.5 2.227 8.794 -0.77385 7913.1326 16500.0 16500.0 48.5892 68.9231 0.10769 -0.832 6.019 -0.39153 7080.04 27625.0 27500.0 65.0525 69.4915 0.13559 1.395 -2.754 8.386 44125.0 44000.0 14993.1726 +clone_048 1.771 -0.49 -1.15833 0.09 -0.904 -1.01818 -2.808 5.397 -0.18615 7886.16 27625.0 27500.0 28.1031 99.0769 0.13846 -2.728 5.711 -0.70484 7498.409 31190.0 30940.0 35.7726 33.0645 0.19355 -5.536 -3.301 5.573 58815.0 58440.0 15384.569 +clone_049 -1.1 -0.22 0.62 1.05 -0.985 -0.36667 -5.756 4.467 -0.7625 7113.8491 43095.0 42970.0 47.3607 57.5 0.21429 0.308 7.829 -0.25254 6929.8834 14105.0 13980.0 46.0966 66.1017 0.15254 -5.448 -3.619 5.66 57200.0 56950.0 14043.7325 diff --git a/software/tests/data/characterization/golden/antibody_bulk_full.stats.json b/software/tests/data/characterization/golden/antibody_bulk_full.stats.json new file mode 100644 index 0000000..927bdad --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_bulk_full.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":10.5,"B":11.0}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/antibody_cdr3_only.properties.tsv b/software/tests/data/characterization/golden/antibody_cdr3_only.properties.tsv new file mode 100644 index 0000000..e22c026 --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_cdr3_only.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 +clone_000 1.315 -1.514 -0.05333 -0.115 -0.244 0.01429 +clone_001 0.111 -0.854 -0.08 1.05 -1.024 -0.70909 +clone_002 0.101 -0.888 -0.43333 -0.73 -1.658 -0.01333 +clone_003 0.324 -1.506 -0.49091 -3.114 -0.3 -1.10769 +clone_004 0.047 -1.003 -1.3 0.76 -0.554 -0.24706 +clone_005 2.821 -0.384 -1.44167 -0.946 -1.024 -1.02857 +clone_006 1.11 -0.854 -0.43333 1.831 -0.359 -1.44 +clone_007 -0.162 -0.38 -1.42222 1.258 -1.637 -0.46 +clone_008 1.757 -0.524 0.17222 -0.108 -0.227 -0.43333 +clone_009 0.835 -0.351 0.225 1.043 -1.002 0.08125 +clone_010 -0.07 -1.342 -1.225 -0.169 -0.397 0.15385 +clone_011 -0.108 -0.227 0.06 1.828 -0.367 0.20667 +clone_012 -3.099 -0.237 -0.125 -3.444 -2.213 -2.18182 +clone_013 1.411 -2.42 -2.8625 0.108 -0.871 -0.44375 +clone_014 -0.892 -0.871 -0.96429 0.75 -0.55 -1.01875 +clone_015 -0.954 -1.07 -0.90667 -0.225 -0.529 -1.43125 +clone_016 -1.882 -0.884 -1.82 3.043 -1.002 -0.46667 +clone_017 -0.899 -0.888 -0.5 0.838 -0.381 -2.0 +clone_018 0.871 -0.277 -0.26429 0.899 -0.211 -0.48333 +clone_019 0.12 -0.905 -1.34 0.888 -0.236 0.56923 +clone_020 0.77 -0.529 -0.91111 0.824 -0.376 0.00833 +clone_021 -1.668 -1.498 -1.78 -1.1 -0.22 1.45556 +clone_022 -0.101 -0.211 0.02 -1.109 -0.266 -0.1 +clone_023 0.892 -0.228 -0.20625 1.114 -0.845 -0.73333 +clone_024 -0.119 -0.253 1.6125 -0.164 -0.359 -1.85 +clone_025 -1.104 -0.228 1.58 0.835 -0.351 -0.31667 +clone_026 0.841 -0.41 -1.50714 -2.104 -0.266 -0.51 +clone_027 -0.116 -0.245 -0.75714 -0.105 -0.257 -0.49 +clone_028 1.043 -1.04 -1.56667 -0.115 -0.244 -0.75 +clone_029 1.036 -1.018 -0.80833 -0.671 -1.518 -0.04286 +clone_030 0.83 -0.398 0.05882 -0.107 -0.236 -2.03333 +clone_031 0.892 -0.228 0.19167 1.047 -1.041 -1.0625 +clone_032 -0.112 -0.236 -0.07143 1.05 -0.985 -1.62857 +clone_033 -2.882 -0.923 0.53333 2.202 -1.76 -1.63 +clone_034 0.243 -1.697 -0.8 -3.101 -0.296 -0.14667 +clone_035 -0.161 -0.389 -1.15714 -2.108 -0.276 -0.01333 +clone_036 -3.1 -0.267 -0.06923 -0.104 -0.228 0.11 +clone_037 0.892 -0.228 -2.64 -1.107 -0.245 0.11667 +clone_038 -0.119 -0.291 0.31429 -0.242 -2.767 0.01765 +clone_039 3.26 -1.645 -1.37059 -1.105 -0.257 -0.59 +clone_040 -1.161 -0.389 -0.49091 1.835 -0.351 -0.33125 +clone_041 1.043 -1.002 1.35455 -2.118 -0.3 0.53889 +clone_042 4.131 -1.916 -0.42667 1.767 -0.499 -0.3875 +clone_043 1.044 -1.011 -1.0 0.767 -0.537 -0.4 +clone_044 1.982 -1.172 -0.425 -0.112 -0.236 1.25 +clone_045 0.478 -2.311 -1.61111 -0.107 -0.246 0.45556 +clone_046 -1.672 -1.506 -0.81 0.891 -0.228 0.73333 +clone_047 -1.108 -0.237 1.35 1.891 -0.228 1.40833 +clone_048 -0.098 -0.241 -1.0 -0.164 -0.359 -0.4375 +clone_049 0.045 -1.02 -0.35625 3.847 -1.43 -1.35 diff --git a/software/tests/data/characterization/golden/antibody_cdr3_only.stats.json b/software/tests/data/characterization/golden/antibody_cdr3_only.stats.json new file mode 100644 index 0000000..57c08d4 --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_cdr3_only.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":12.0,"B":11.5}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/antibody_partial.properties.tsv b/software/tests/data/characterization/golden/antibody_partial.properties.tsv new file mode 100644 index 0000000..2911692 --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_partial.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion charge_Fv chargeShift_Fv pi_Fv eox_Fv ered_Fv mw_Fv +clone_000 -0.169 -0.397 -0.80714 -2.167 -0.415 -0.67778 -2.785 5.412 -0.47705 7282.213 33710.0 33460.0 55.8246 63.9344 0.14754 +clone_001 0.053 -1.015 -2.62 -0.955 -1.058 -0.82778 -4.721 5.287 -1.22075 6418.0657 27500.0 27500.0 46.8604 51.5094 0.11321 -3.624 5.747 -0.94375 7702.5616 15595.0 15470.0 58.2861 50.3125 0.125 -8.346 -4.611 5.544 43095.0 42970.0 14120.6273 +clone_002 0.323 -1.497 -1.125 -1.294 -0.712 -0.95556 -3.717 5.456 -0.24677 7206.2727 19730.0 19480.0 54.6935 70.8065 0.09677 -6.778 4.532 -0.42985 8312.4976 37595.0 37470.0 28.8522 56.7164 0.1791 -10.495 -3.418 4.962 57325.0 56950.0 15518.7703 +clone_003 -0.164 -0.359 -1.04 -2.1 -0.229 -0.22222 2.191 8.927 -0.19038 6274.2956 11460.0 11460.0 26.9444 69.4231 0.19231 +clone_004 -2.095 -0.271 0.56 0.824 -0.376 0.2 -5.681 5.155 -0.30833 7054.8531 16625.0 16500.0 34.08 63.3333 0.13333 1.267 8.838 -0.68947 6711.6968 14105.0 13980.0 80.6193 39.4737 0.10526 -4.413 -3.966 5.875 30730.0 30480.0 13766.5499 +clone_005 -1.107 -0.245 0.44667 -0.105 -0.219 0.75714 -0.729 6.389 -0.54478 8277.2915 44585.0 44460.0 30.494 58.209 0.20896 -1.784 5.761 -0.43148 6388.2544 26720.0 26470.0 28.0333 56.1111 0.16667 -2.513 -3.269 6.083 71305.0 70930.0 14665.5459 +clone_006 -1.104 -0.228 1.36667 0.969 -1.176 0.17059 -4.724 5.263 -0.15588 7997.1014 22585.0 22460.0 51.7663 87.3529 0.13235 +clone_007 -1.1 -0.22 -2.46 -1.173 -0.444 -1.30625 -0.792 6.235 -0.92364 6905.7294 26595.0 26470.0 30.76 28.3636 0.21818 -2.723 5.735 -0.77015 8056.0955 38960.0 38960.0 43.9912 45.2239 0.16418 -3.514 -3.377 5.908 65555.0 65430.0 14961.8249 +clone_008 1.89 -0.254 -2.14615 -1.102 -0.249 0.63 -0.812 6.159 -0.71077 7697.5881 47230.0 46980.0 66.1277 40.6154 0.16923 -10.75 4.023 -0.2431 7014.6815 22710.0 22460.0 76.0484 38.6207 0.22414 -11.562 -2.784 4.657 69940.0 69440.0 14712.2696 +clone_009 -3.1 -0.267 -0.37222 0.471 -2.326 -1.96 -1.791 5.785 -0.11471 8044.3977 19605.0 19480.0 47.0882 84.5588 0.11765 +clone_010 -1.105 -0.257 -1.4 -1.454 -2.2 -1.2 -5.774 4.663 -0.51852 6398.1086 15595.0 15470.0 62.6613 57.7778 0.16667 -2.617 5.992 -1.17015 8042.8242 21095.0 20970.0 54.9328 58.0597 0.10448 -8.391 -4.363 5.48 36690.0 36440.0 14440.9328 +clone_011 0.825 -0.385 -0.4 -1.102 -0.249 0.62 -2.743 5.599 -0.11667 7877.9676 26720.0 26470.0 35.9167 63.4848 0.15152 2.262 9.145 -0.86935 7680.7559 34615.0 34490.0 26.0161 39.3548 0.16129 -0.481 -3.52 6.71 61335.0 60960.0 15558.7235 +clone_012 -0.102 -0.249 0.62727 1.05 -0.985 0.05 -1.726 5.99 -0.57213 7423.3606 14565.0 14440.0 48.6607 62.459 0.16393 +clone_013 -0.096 -0.259 -0.7 2.828 -0.367 -1.2 -5.809 4.551 -0.6377 7119.955 9970.0 9970.0 47.9328 78.1967 0.09836 2.144 9.115 -0.26102 7249.3169 32220.0 31970.0 42.529 51.1864 0.22034 -3.665 -2.274 5.543 42190.0 41940.0 14369.2719 +clone_014 -1.105 -0.257 -0.31667 1.824 -0.376 -0.32857 -1.636 6.173 -0.51429 7048.9378 28210.0 27960.0 62.4768 43.5714 0.23214 0.308 7.708 -0.15439 7075.1255 17210.0 16960.0 51.5491 71.5789 0.21053 -1.327 -4.652 6.496 45420.0 44920.0 14124.0633 +clone_015 -0.102 -0.249 0.26667 0.306 7.632 -0.39 7081.1756 15720.0 15470.0 60.125 66.6667 0.11667 +clone_016 -1.882 -0.884 -1.15556 -0.885 -0.854 -0.08333 -3.717 5.425 -0.67377 7335.1777 14105.0 13980.0 5.6131 36.7213 0.14754 -3.78 5.091 -0.59322 7312.308 40575.0 40450.0 25.3847 51.1864 0.20339 -7.497 -3.297 5.279 54680.0 54430.0 14647.4857 +clone_017 0.895 -0.22 0.92222 -1.111 -0.245 0.05714 -1.603 6.227 -0.64844 7690.5788 14105.0 13980.0 33.0969 67.0312 0.14062 -2.816 5.203 -0.52034 6815.6356 17085.0 16960.0 61.4881 69.322 0.11864 -4.418 -4.066 5.888 31190.0 30940.0 14506.2144 +clone_018 -2.674 -1.574 -0.78125 -1.111 -0.245 -0.43571 -2.786 5.341 -0.37031 7296.132 26720.0 26470.0 61.0594 39.6875 0.15625 +clone_019 -0.101 -0.22 -0.5125 1.051 -0.994 -1.8375 -0.734 6.387 -0.42632 6721.7213 19480.0 19480.0 22.8175 71.9298 0.12281 -2.76 5.627 -0.70667 7188.1071 25230.0 24980.0 48.4 61.8333 0.13333 -3.494 -3.64 5.955 44710.0 44460.0 13909.8284 +clone_020 2.764 -0.507 -1.05455 -1.1 -0.22 -0.38182 2.237 9.372 -0.50645 7173.1614 18575.0 18450.0 49.2742 67.7419 0.1129 -4.804 4.485 -0.69839 6997.8172 12490.0 12490.0 44.2032 42.5806 0.08065 -2.568 -2.794 5.982 31065.0 30940.0 14170.9786 +clone_021 -0.162 -0.38 -0.675 -1.099 -0.279 -0.36667 0.213 7.553 -0.80769 7314.0678 15470.0 15470.0 41.0125 43.3846 0.13846 +clone_022 -0.105 -0.219 -0.28333 -0.101 -0.211 0.7 -0.809 6.162 -0.895 7172.9479 25230.0 24980.0 39.9083 21.1667 0.16667 -1.834 5.421 -0.18276 6839.8739 10095.0 9970.0 79.2914 94.1379 0.10345 -2.643 -2.223 5.804 35325.0 34950.0 14012.8218 +clone_023 0.316 -1.513 -0.71429 1.827 -0.368 -1.07143 0.273 7.622 -0.26452 7230.2442 6085.0 5960.0 72.1856 58.2258 0.14516 3.236 9.729 -0.12364 6485.7489 12990.0 12490.0 38.5564 37.2727 0.12727 3.509 -3.473 9.151 19075.0 18450.0 13715.9931 +clone_024 0.975 -1.188 -0.93846 -1.171 -0.386 0.6 -3.745 5.277 0.09403 7794.8603 29240.0 28990.0 37.8269 61.194 0.16418 +clone_025 -0.245 -0.55 -2.37273 0.474 -2.347 -1.4375 2.201 9.169 -0.44833 7223.3673 22585.0 22460.0 44.6202 61.6667 0.15 -9.689 4.477 -0.99524 8178.8468 46410.0 46410.0 70.873 47.9365 0.28571 -7.487 -3.57 5.363 68995.0 68870.0 15402.2141 +clone_026 -0.89 -0.892 -0.36 -0.949 -1.033 -1.15714 -7.765 4.376 -0.28154 7612.4818 35980.0 35980.0 55.2815 63.0769 0.16923 3.229 9.304 -0.53333 7065.2406 30605.0 30480.0 62.8493 66.6667 0.15789 -4.536 -3.169 5.69 66585.0 66460.0 14677.7224 +clone_027 0.892 -0.228 -0.7 1.827 -0.368 1.525 5.172 9.994 -0.3377 7188.3037 28210.0 27960.0 23.377 67.0492 0.14754 +clone_028 2.191 -1.824 -0.49444 -0.892 -0.871 -0.58 0.336 7.581 -0.98919 9219.3989 28210.0 27960.0 51.75 39.5946 0.16216 0.273 7.616 -0.85 7681.5385 46075.0 45950.0 60.2598 48.7097 0.22581 0.609 -4.329 7.596 74285.0 73910.0 16900.9374 +clone_029 1.1 -0.879 0.2 -0.105 -0.257 -2.27778 -2.743 5.557 -0.62769 7750.6841 32220.0 31970.0 27.0569 58.4615 0.12308 1.262 8.607 -0.66721 7321.2509 22460.0 22460.0 30.6623 79.6721 0.13115 -1.481 -3.526 6.36 54680.0 54430.0 15071.935 +clone_030 -1.102 -0.249 -0.74286 0.046 -1.032 -1.09286 -2.725 5.723 -0.50455 8008.0726 33835.0 33460.0 50.55 51.5152 0.15152 +clone_031 2.107 -0.863 0.09286 0.111 -0.854 1.04 7.193 10.076 -0.29254 8223.7227 33585.0 33460.0 49.3881 71.3433 0.16418 0.223 7.419 -0.63333 5921.6776 7115.0 6990.0 45.2706 48.7037 0.07407 7.416 -3.136 9.284 40700.0 40450.0 14145.4003 +clone_032 1.888 -0.236 -0.37692 1.824 -0.376 0.48889 -1.718 5.981 -0.4375 7555.4207 18700.0 18450.0 52.8328 67.1875 0.15625 0.188 7.607 -0.03214 6662.7815 22585.0 22460.0 20.4661 80.0 0.125 -1.531 -3.039 6.289 41285.0 40910.0 14218.2022 +clone_033 1.312 -1.561 -0.41667 -0.101 -0.211 1.58 -0.757 6.331 -0.2913 8095.2441 15845.0 15470.0 23.8942 56.3768 0.13043 +clone_034 0.038 -1.036 -1.42778 0.104 -0.871 1.55 -2.575 6.032 -1.63676 8171.6913 4595.0 4470.0 35.0691 37.2059 0.08824 2.181 9.521 0.23448 6632.7802 10220.0 9970.0 31.1948 78.9655 0.13793 -0.393 -4.171 6.781 14815.0 14440.0 14804.4715 +clone_035 0.899 -0.211 0.58571 0.899 -0.211 -0.35 2.312 9.56 -0.80806 7860.8255 21430.0 21430.0 63.0681 50.1613 0.20968 0.243 7.67 -0.05965 6673.7244 15595.0 15470.0 12.3526 73.5088 0.14035 2.555 -3.681 9.127 37025.0 36900.0 14534.5499 +clone_036 -0.115 -0.244 -0.575 1.185 9.069 0.20172 6860.1745 30730.0 30480.0 36.5276 87.4138 0.13793 +clone_037 1.326 -1.489 -0.2875 1.114 -0.845 0.05556 1.242 8.838 -0.64407 6910.7299 19730.0 19480.0 3.1203 42.7119 0.13559 -0.711 6.395 -0.23175 7671.8087 29240.0 28990.0 47.3984 83.4921 0.15873 0.531 -3.514 7.712 48970.0 48470.0 14582.5386 +clone_038 0.039 -1.087 -2.16923 -2.099 -0.237 -0.34 -5.711 5.155 -1.1058 8491.177 19035.0 18910.0 31.4972 39.5652 0.2029 0.358 7.665 -0.36349 7721.8162 20970.0 20970.0 65.0905 65.0794 0.1746 -5.353 -4.554 5.844 40005.0 39880.0 16212.9932 +clone_039 -0.112 -0.236 1.26667 0.884 -0.245 0.72778 -2.836 4.809 0.25254 6664.6449 30730.0 30480.0 48.8525 81.0169 0.15254 +clone_040 -0.105 -0.219 1.2375 -0.101 -0.211 1.98 1.283 9.258 -0.23793 6861.8362 29115.0 28990.0 63.4293 55.5172 0.15517 -0.789 6.26 -0.31186 7398.3755 37595.0 37470.0 11.1729 49.4915 0.27119 0.493 -3.337 7.619 66710.0 66460.0 14260.2117 +clone_041 -0.176 -0.414 0.45 -0.949 -1.033 -0.61333 1.177 8.594 -0.17937 7506.6697 32220.0 31970.0 53.6763 69.8413 0.1746 -8.739 4.162 -0.35156 7702.6688 25105.0 24980.0 75.1609 53.2812 0.15625 -7.562 -2.978 5.16 57325.0 56950.0 15209.3385 +clone_042 -0.883 -0.875 -0.14 1.764 -0.507 -1.35714 1.169 8.766 -0.03188 7728.8108 25565.0 25440.0 60.9304 65.2174 0.13043 +clone_043 1.114 -0.845 0.1 2.828 -0.367 -1.58182 1.19 9.404 0.3193 6713.8114 25105.0 24980.0 27.8193 97.3684 0.14035 -0.749 6.336 -0.35821 7851.9118 9970.0 9970.0 61.1821 55.3731 0.13433 0.441 -2.826 7.685 35075.0 34950.0 14565.7232 +clone_044 -1.882 -0.923 -0.19375 0.813 -0.401 0.225 -6.62 5.273 -0.9629 7595.233 32095.0 31970.0 53.9452 36.2903 0.17742 -0.807 6.148 -0.16 8171.1554 38055.0 37930.0 38.8417 62.9231 0.29231 -7.427 -3.834 5.473 70150.0 69900.0 15766.3884 +clone_045 -3.874 -0.953 -1.48125 -0.687 -1.531 -1.03333 0.235 7.477 -0.63939 7986.0458 10095.0 9970.0 78.1759 50.0 0.13636 +clone_046 2.986 -1.125 -2.26667 -0.885 -0.854 -1.31429 -4.741 5.134 -0.62593 6390.1198 12490.0 12490.0 49.1333 84.8148 0.07407 2.234 9.147 -0.13036 6542.7076 11125.0 11000.0 63.025 76.6071 0.08929 -2.507 -3.383 6.101 23615.0 23490.0 12932.8274 +clone_047 0.885 -0.244 0.64167 -0.228 -0.508 -0.91538 0.185 7.604 0.13833 7106.2722 25440.0 25440.0 36.7367 79.6667 0.2 -3.785 5.091 -0.26154 8015.2104 28085.0 27960.0 12.4231 43.6923 0.21538 -3.6 -2.625 5.702 53525.0 53400.0 15121.4826 +clone_048 -0.026 -1.177 -0.49412 0.988 -1.143 -1.47143 3.264 9.577 -0.73231 7922.9695 37720.0 37470.0 57.8323 50.9231 0.16923 +clone_049 -1.111 -0.245 -0.44286 -0.886 -0.884 -0.56923 2.254 8.794 -1.00702 7366.6112 29365.0 28990.0 62.0789 29.1228 0.15789 -2.695 5.766 -0.32131 7414.3088 24075.0 23950.0 48.2443 52.623 0.21311 -0.441 -4.039 6.756 53440.0 52940.0 14780.92 diff --git a/software/tests/data/characterization/golden/antibody_partial.stats.json b/software/tests/data/characterization/golden/antibody_partial.stats.json new file mode 100644 index 0000000..e181b53 --- /dev/null +++ b/software/tests/data/characterization/golden/antibody_partial.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":11.0,"B":10.0}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/peptide.aa_fraction.tsv b/software/tests/data/characterization/golden/peptide.aa_fraction.tsv new file mode 100644 index 0000000..90fd78b --- /dev/null +++ b/software/tests/data/characterization/golden/peptide.aa_fraction.tsv @@ -0,0 +1,1141 @@ +entity_key aminoAcid value +pep_000 A 0.0 +pep_000 C 0.05 +pep_000 D 0.0 +pep_000 E 0.05 +pep_000 F 0.1 +pep_000 G 0.0 +pep_000 H 0.05 +pep_000 I 0.0 +pep_000 K 0.1 +pep_000 L 0.1 +pep_000 M 0.0 +pep_000 N 0.05 +pep_000 P 0.05 +pep_000 Q 0.05 +pep_000 R 0.0 +pep_000 S 0.1 +pep_000 T 0.1 +pep_000 V 0.05 +pep_000 W 0.05 +pep_000 Y 0.1 +pep_001 A 0.0 +pep_001 C 0.0 +pep_001 D 0.08333 +pep_001 E 0.16667 +pep_001 F 0.0 +pep_001 G 0.0 +pep_001 H 0.08333 +pep_001 I 0.0 +pep_001 K 0.0 +pep_001 L 0.08333 +pep_001 M 0.16667 +pep_001 N 0.08333 +pep_001 P 0.0 +pep_001 Q 0.08333 +pep_001 R 0.0 +pep_001 S 0.08333 +pep_001 T 0.0 +pep_001 V 0.08333 +pep_001 W 0.0 +pep_001 Y 0.08333 +pep_002 A 0.08 +pep_002 C 0.04 +pep_002 D 0.16 +pep_002 E 0.0 +pep_002 F 0.04 +pep_002 G 0.0 +pep_002 H 0.04 +pep_002 I 0.12 +pep_002 K 0.04 +pep_002 L 0.0 +pep_002 M 0.08 +pep_002 N 0.0 +pep_002 P 0.04 +pep_002 Q 0.0 +pep_002 R 0.08 +pep_002 S 0.08 +pep_002 T 0.04 +pep_002 V 0.08 +pep_002 W 0.04 +pep_002 Y 0.04 +pep_003 A 0.0 +pep_003 C 0.0 +pep_003 D 0.05556 +pep_003 E 0.11111 +pep_003 F 0.0 +pep_003 G 0.0 +pep_003 H 0.05556 +pep_003 I 0.0 +pep_003 K 0.0 +pep_003 L 0.16667 +pep_003 M 0.05556 +pep_003 N 0.0 +pep_003 P 0.0 +pep_003 Q 0.0 +pep_003 R 0.05556 +pep_003 S 0.05556 +pep_003 T 0.05556 +pep_003 V 0.22222 +pep_003 W 0.05556 +pep_003 Y 0.11111 +pep_004 A 0.0 +pep_004 C 0.1 +pep_004 D 0.15 +pep_004 E 0.0 +pep_004 F 0.1 +pep_004 G 0.1 +pep_004 H 0.05 +pep_004 I 0.05 +pep_004 K 0.05 +pep_004 L 0.05 +pep_004 M 0.05 +pep_004 N 0.0 +pep_004 P 0.05 +pep_004 Q 0.0 +pep_004 R 0.0 +pep_004 S 0.05 +pep_004 T 0.05 +pep_004 V 0.05 +pep_004 W 0.05 +pep_004 Y 0.05 +pep_005 A 0.0 +pep_005 C 0.0 +pep_005 D 0.0625 +pep_005 E 0.0625 +pep_005 F 0.0 +pep_005 G 0.0 +pep_005 H 0.0625 +pep_005 I 0.0625 +pep_005 K 0.0625 +pep_005 L 0.0 +pep_005 M 0.0625 +pep_005 N 0.0625 +pep_005 P 0.0 +pep_005 Q 0.0625 +pep_005 R 0.0625 +pep_005 S 0.125 +pep_005 T 0.0625 +pep_005 V 0.0 +pep_005 W 0.1875 +pep_005 Y 0.0625 +pep_006 A 0.07143 +pep_006 C 0.07143 +pep_006 D 0.07143 +pep_006 E 0.14286 +pep_006 F 0.03571 +pep_006 G 0.03571 +pep_006 H 0.07143 +pep_006 I 0.10714 +pep_006 K 0.03571 +pep_006 L 0.0 +pep_006 M 0.07143 +pep_006 N 0.03571 +pep_006 P 0.03571 +pep_006 Q 0.03571 +pep_006 R 0.0 +pep_006 S 0.0 +pep_006 T 0.0 +pep_006 V 0.03571 +pep_006 W 0.07143 +pep_006 Y 0.07143 +pep_007 A 0.10526 +pep_007 C 0.10526 +pep_007 D 0.10526 +pep_007 E 0.15789 +pep_007 F 0.0 +pep_007 G 0.05263 +pep_007 H 0.10526 +pep_007 I 0.05263 +pep_007 K 0.05263 +pep_007 L 0.0 +pep_007 M 0.0 +pep_007 N 0.0 +pep_007 P 0.0 +pep_007 Q 0.05263 +pep_007 R 0.0 +pep_007 S 0.05263 +pep_007 T 0.0 +pep_007 V 0.05263 +pep_007 W 0.0 +pep_007 Y 0.10526 +pep_008 A 0.0 +pep_008 C 0.10714 +pep_008 D 0.0 +pep_008 E 0.07143 +pep_008 F 0.0 +pep_008 G 0.17857 +pep_008 H 0.07143 +pep_008 I 0.0 +pep_008 K 0.07143 +pep_008 L 0.03571 +pep_008 M 0.03571 +pep_008 N 0.07143 +pep_008 P 0.03571 +pep_008 Q 0.03571 +pep_008 R 0.07143 +pep_008 S 0.03571 +pep_008 T 0.07143 +pep_008 V 0.0 +pep_008 W 0.03571 +pep_008 Y 0.07143 +pep_009 A 0.0 +pep_009 C 0.0 +pep_009 D 0.0 +pep_009 E 0.0 +pep_009 F 0.0 +pep_009 G 0.0 +pep_009 H 0.0 +pep_009 I 0.0 +pep_009 K 0.125 +pep_009 L 0.125 +pep_009 M 0.0 +pep_009 N 0.125 +pep_009 P 0.125 +pep_009 Q 0.125 +pep_009 R 0.0 +pep_009 S 0.125 +pep_009 T 0.125 +pep_009 V 0.0 +pep_009 W 0.125 +pep_009 Y 0.0 +pep_010 A 0.08333 +pep_010 C 0.08333 +pep_010 D 0.08333 +pep_010 E 0.0 +pep_010 F 0.08333 +pep_010 G 0.0 +pep_010 H 0.0 +pep_010 I 0.08333 +pep_010 K 0.08333 +pep_010 L 0.0 +pep_010 M 0.08333 +pep_010 N 0.08333 +pep_010 P 0.0 +pep_010 Q 0.0 +pep_010 R 0.08333 +pep_010 S 0.08333 +pep_010 T 0.0 +pep_010 V 0.16667 +pep_010 W 0.0 +pep_010 Y 0.0 +pep_011 A 0.03704 +pep_011 C 0.07407 +pep_011 D 0.03704 +pep_011 E 0.0 +pep_011 F 0.03704 +pep_011 G 0.07407 +pep_011 H 0.03704 +pep_011 I 0.07407 +pep_011 K 0.0 +pep_011 L 0.07407 +pep_011 M 0.03704 +pep_011 N 0.03704 +pep_011 P 0.11111 +pep_011 Q 0.11111 +pep_011 R 0.07407 +pep_011 S 0.0 +pep_011 T 0.0 +pep_011 V 0.0 +pep_011 W 0.11111 +pep_011 Y 0.07407 +pep_012 A 0.1 +pep_012 C 0.1 +pep_012 D 0.0 +pep_012 E 0.0 +pep_012 F 0.0 +pep_012 G 0.1 +pep_012 H 0.0 +pep_012 I 0.0 +pep_012 K 0.1 +pep_012 L 0.0 +pep_012 M 0.0 +pep_012 N 0.0 +pep_012 P 0.0 +pep_012 Q 0.0 +pep_012 R 0.1 +pep_012 S 0.2 +pep_012 T 0.1 +pep_012 V 0.1 +pep_012 W 0.0 +pep_012 Y 0.1 +pep_013 A 0.22222 +pep_013 C 0.05556 +pep_013 D 0.05556 +pep_013 E 0.0 +pep_013 F 0.05556 +pep_013 G 0.0 +pep_013 H 0.11111 +pep_013 I 0.0 +pep_013 K 0.0 +pep_013 L 0.05556 +pep_013 M 0.05556 +pep_013 N 0.0 +pep_013 P 0.05556 +pep_013 Q 0.11111 +pep_013 R 0.05556 +pep_013 S 0.0 +pep_013 T 0.05556 +pep_013 V 0.05556 +pep_013 W 0.0 +pep_013 Y 0.05556 +pep_014 A 0.0 +pep_014 C 0.0 +pep_014 D 0.09091 +pep_014 E 0.18182 +pep_014 F 0.0 +pep_014 G 0.09091 +pep_014 H 0.18182 +pep_014 I 0.0 +pep_014 K 0.09091 +pep_014 L 0.09091 +pep_014 M 0.0 +pep_014 N 0.0 +pep_014 P 0.09091 +pep_014 Q 0.0 +pep_014 R 0.0 +pep_014 S 0.09091 +pep_014 T 0.0 +pep_014 V 0.0 +pep_014 W 0.0 +pep_014 Y 0.09091 +pep_015 A 0.0 +pep_015 C 0.0 +pep_015 D 0.0 +pep_015 E 0.25 +pep_015 F 0.125 +pep_015 G 0.0 +pep_015 H 0.0 +pep_015 I 0.0 +pep_015 K 0.25 +pep_015 L 0.0 +pep_015 M 0.0 +pep_015 N 0.125 +pep_015 P 0.0 +pep_015 Q 0.0 +pep_015 R 0.125 +pep_015 S 0.0 +pep_015 T 0.125 +pep_015 V 0.0 +pep_015 W 0.0 +pep_015 Y 0.0 +pep_016 A 0.08333 +pep_016 C 0.25 +pep_016 D 0.0 +pep_016 E 0.0 +pep_016 F 0.0 +pep_016 G 0.0 +pep_016 H 0.08333 +pep_016 I 0.0 +pep_016 K 0.16667 +pep_016 L 0.0 +pep_016 M 0.08333 +pep_016 N 0.08333 +pep_016 P 0.0 +pep_016 Q 0.0 +pep_016 R 0.0 +pep_016 S 0.0 +pep_016 T 0.0 +pep_016 V 0.08333 +pep_016 W 0.08333 +pep_016 Y 0.08333 +pep_017 A 0.03571 +pep_017 C 0.07143 +pep_017 D 0.03571 +pep_017 E 0.0 +pep_017 F 0.10714 +pep_017 G 0.07143 +pep_017 H 0.03571 +pep_017 I 0.0 +pep_017 K 0.07143 +pep_017 L 0.07143 +pep_017 M 0.10714 +pep_017 N 0.10714 +pep_017 P 0.03571 +pep_017 Q 0.03571 +pep_017 R 0.03571 +pep_017 S 0.03571 +pep_017 T 0.0 +pep_017 V 0.03571 +pep_017 W 0.07143 +pep_017 Y 0.03571 +pep_018 A 0.0 +pep_018 C 0.05 +pep_018 D 0.05 +pep_018 E 0.15 +pep_018 F 0.05 +pep_018 G 0.05 +pep_018 H 0.0 +pep_018 I 0.05 +pep_018 K 0.0 +pep_018 L 0.2 +pep_018 M 0.05 +pep_018 N 0.0 +pep_018 P 0.05 +pep_018 Q 0.05 +pep_018 R 0.0 +pep_018 S 0.1 +pep_018 T 0.05 +pep_018 V 0.1 +pep_018 W 0.0 +pep_018 Y 0.0 +pep_019 A 0.04348 +pep_019 C 0.08696 +pep_019 D 0.13043 +pep_019 E 0.13043 +pep_019 F 0.04348 +pep_019 G 0.04348 +pep_019 H 0.04348 +pep_019 I 0.04348 +pep_019 K 0.0 +pep_019 L 0.04348 +pep_019 M 0.13043 +pep_019 N 0.0 +pep_019 P 0.08696 +pep_019 Q 0.04348 +pep_019 R 0.0 +pep_019 S 0.08696 +pep_019 T 0.0 +pep_019 V 0.0 +pep_019 W 0.04348 +pep_019 Y 0.0 +pep_020 A 0.05 +pep_020 C 0.0 +pep_020 D 0.1 +pep_020 E 0.05 +pep_020 F 0.0 +pep_020 G 0.05 +pep_020 H 0.1 +pep_020 I 0.05 +pep_020 K 0.05 +pep_020 L 0.05 +pep_020 M 0.0 +pep_020 N 0.1 +pep_020 P 0.0 +pep_020 Q 0.1 +pep_020 R 0.05 +pep_020 S 0.05 +pep_020 T 0.05 +pep_020 V 0.05 +pep_020 W 0.1 +pep_020 Y 0.0 +pep_021 A 0.0 +pep_021 C 0.04167 +pep_021 D 0.0 +pep_021 E 0.125 +pep_021 F 0.125 +pep_021 G 0.0 +pep_021 H 0.16667 +pep_021 I 0.0 +pep_021 K 0.04167 +pep_021 L 0.0 +pep_021 M 0.0 +pep_021 N 0.04167 +pep_021 P 0.125 +pep_021 Q 0.0 +pep_021 R 0.08333 +pep_021 S 0.08333 +pep_021 T 0.0 +pep_021 V 0.04167 +pep_021 W 0.04167 +pep_021 Y 0.08333 +pep_022 A 0.03571 +pep_022 C 0.07143 +pep_022 D 0.07143 +pep_022 E 0.0 +pep_022 F 0.07143 +pep_022 G 0.0 +pep_022 H 0.03571 +pep_022 I 0.03571 +pep_022 K 0.03571 +pep_022 L 0.03571 +pep_022 M 0.14286 +pep_022 N 0.0 +pep_022 P 0.03571 +pep_022 Q 0.03571 +pep_022 R 0.0 +pep_022 S 0.14286 +pep_022 T 0.10714 +pep_022 V 0.03571 +pep_022 W 0.03571 +pep_022 Y 0.07143 +pep_023 A 0.1 +pep_023 C 0.1 +pep_023 D 0.0 +pep_023 E 0.0 +pep_023 F 0.2 +pep_023 G 0.1 +pep_023 H 0.0 +pep_023 I 0.1 +pep_023 K 0.0 +pep_023 L 0.1 +pep_023 M 0.1 +pep_023 N 0.0 +pep_023 P 0.0 +pep_023 Q 0.0 +pep_023 R 0.2 +pep_023 S 0.0 +pep_023 T 0.0 +pep_023 V 0.0 +pep_023 W 0.0 +pep_023 Y 0.0 +pep_024 A 0.0 +pep_024 C 0.05263 +pep_024 D 0.10526 +pep_024 E 0.0 +pep_024 F 0.0 +pep_024 G 0.0 +pep_024 H 0.05263 +pep_024 I 0.0 +pep_024 K 0.0 +pep_024 L 0.05263 +pep_024 M 0.0 +pep_024 N 0.0 +pep_024 P 0.10526 +pep_024 Q 0.10526 +pep_024 R 0.0 +pep_024 S 0.05263 +pep_024 T 0.21053 +pep_024 V 0.05263 +pep_024 W 0.05263 +pep_024 Y 0.15789 +pep_025 A 0.07692 +pep_025 C 0.11538 +pep_025 D 0.03846 +pep_025 E 0.0 +pep_025 F 0.03846 +pep_025 G 0.07692 +pep_025 H 0.0 +pep_025 I 0.07692 +pep_025 K 0.07692 +pep_025 L 0.11538 +pep_025 M 0.07692 +pep_025 N 0.0 +pep_025 P 0.07692 +pep_025 Q 0.07692 +pep_025 R 0.0 +pep_025 S 0.07692 +pep_025 T 0.03846 +pep_025 V 0.0 +pep_025 W 0.03846 +pep_025 Y 0.0 +pep_026 A 0.0 +pep_026 C 0.16667 +pep_026 D 0.08333 +pep_026 E 0.0 +pep_026 F 0.08333 +pep_026 G 0.0 +pep_026 H 0.0 +pep_026 I 0.0 +pep_026 K 0.0 +pep_026 L 0.0 +pep_026 M 0.0 +pep_026 N 0.08333 +pep_026 P 0.08333 +pep_026 Q 0.08333 +pep_026 R 0.16667 +pep_026 S 0.08333 +pep_026 T 0.0 +pep_026 V 0.0 +pep_026 W 0.0 +pep_026 Y 0.16667 +pep_027 A 0.09091 +pep_027 C 0.09091 +pep_027 D 0.0 +pep_027 E 0.09091 +pep_027 F 0.18182 +pep_027 G 0.0 +pep_027 H 0.0 +pep_027 I 0.0 +pep_027 K 0.0 +pep_027 L 0.0 +pep_027 M 0.09091 +pep_027 N 0.09091 +pep_027 P 0.0 +pep_027 Q 0.0 +pep_027 R 0.0 +pep_027 S 0.09091 +pep_027 T 0.0 +pep_027 V 0.09091 +pep_027 W 0.0 +pep_027 Y 0.18182 +pep_028 A 0.0 +pep_028 C 0.07143 +pep_028 D 0.0 +pep_028 E 0.14286 +pep_028 F 0.07143 +pep_028 G 0.0 +pep_028 H 0.0 +pep_028 I 0.0 +pep_028 K 0.0 +pep_028 L 0.14286 +pep_028 M 0.07143 +pep_028 N 0.0 +pep_028 P 0.14286 +pep_028 Q 0.0 +pep_028 R 0.07143 +pep_028 S 0.07143 +pep_028 T 0.0 +pep_028 V 0.0 +pep_028 W 0.0 +pep_028 Y 0.21429 +pep_029 A 0.03448 +pep_029 C 0.10345 +pep_029 D 0.06897 +pep_029 E 0.06897 +pep_029 F 0.03448 +pep_029 G 0.0 +pep_029 H 0.10345 +pep_029 I 0.03448 +pep_029 K 0.03448 +pep_029 L 0.03448 +pep_029 M 0.0 +pep_029 N 0.06897 +pep_029 P 0.03448 +pep_029 Q 0.03448 +pep_029 R 0.06897 +pep_029 S 0.03448 +pep_029 T 0.17241 +pep_029 V 0.0 +pep_029 W 0.06897 +pep_029 Y 0.0 +pep_030 A 0.09524 +pep_030 C 0.0 +pep_030 D 0.19048 +pep_030 E 0.14286 +pep_030 F 0.04762 +pep_030 G 0.0 +pep_030 H 0.0 +pep_030 I 0.0 +pep_030 K 0.04762 +pep_030 L 0.0 +pep_030 M 0.04762 +pep_030 N 0.04762 +pep_030 P 0.0 +pep_030 Q 0.2381 +pep_030 R 0.04762 +pep_030 S 0.04762 +pep_030 T 0.0 +pep_030 V 0.0 +pep_030 W 0.04762 +pep_030 Y 0.0 +pep_031 A 0.26316 +pep_031 C 0.0 +pep_031 D 0.05263 +pep_031 E 0.05263 +pep_031 F 0.05263 +pep_031 G 0.05263 +pep_031 H 0.10526 +pep_031 I 0.05263 +pep_031 K 0.0 +pep_031 L 0.05263 +pep_031 M 0.0 +pep_031 N 0.21053 +pep_031 P 0.0 +pep_031 Q 0.0 +pep_031 R 0.0 +pep_031 S 0.0 +pep_031 T 0.0 +pep_031 V 0.0 +pep_031 W 0.0 +pep_031 Y 0.10526 +pep_032 A 0.06667 +pep_032 C 0.0 +pep_032 D 0.0 +pep_032 E 0.06667 +pep_032 F 0.13333 +pep_032 G 0.06667 +pep_032 H 0.06667 +pep_032 I 0.0 +pep_032 K 0.06667 +pep_032 L 0.13333 +pep_032 M 0.06667 +pep_032 N 0.13333 +pep_032 P 0.0 +pep_032 Q 0.0 +pep_032 R 0.06667 +pep_032 S 0.13333 +pep_032 T 0.0 +pep_032 V 0.0 +pep_032 W 0.0 +pep_032 Y 0.0 +pep_033 A 0.04 +pep_033 C 0.04 +pep_033 D 0.04 +pep_033 E 0.04 +pep_033 F 0.12 +pep_033 G 0.12 +pep_033 H 0.0 +pep_033 I 0.12 +pep_033 K 0.0 +pep_033 L 0.08 +pep_033 M 0.08 +pep_033 N 0.04 +pep_033 P 0.04 +pep_033 Q 0.04 +pep_033 R 0.0 +pep_033 S 0.0 +pep_033 T 0.04 +pep_033 V 0.04 +pep_033 W 0.08 +pep_033 Y 0.04 +pep_034 A 0.0 +pep_034 C 0.05 +pep_034 D 0.15 +pep_034 E 0.0 +pep_034 F 0.1 +pep_034 G 0.0 +pep_034 H 0.0 +pep_034 I 0.05 +pep_034 K 0.0 +pep_034 L 0.1 +pep_034 M 0.0 +pep_034 N 0.1 +pep_034 P 0.05 +pep_034 Q 0.2 +pep_034 R 0.05 +pep_034 S 0.0 +pep_034 T 0.05 +pep_034 V 0.05 +pep_034 W 0.05 +pep_034 Y 0.0 +pep_035 A 0.0 +pep_035 C 0.0 +pep_035 D 0.125 +pep_035 E 0.0 +pep_035 F 0.0 +pep_035 G 0.0 +pep_035 H 0.125 +pep_035 I 0.0 +pep_035 K 0.0 +pep_035 L 0.125 +pep_035 M 0.125 +pep_035 N 0.125 +pep_035 P 0.0 +pep_035 Q 0.125 +pep_035 R 0.125 +pep_035 S 0.125 +pep_035 T 0.0 +pep_035 V 0.0 +pep_035 W 0.0 +pep_035 Y 0.0 +pep_036 A 0.0 +pep_036 C 0.0 +pep_036 D 0.0 +pep_036 E 0.15385 +pep_036 F 0.07692 +pep_036 G 0.15385 +pep_036 H 0.0 +pep_036 I 0.07692 +pep_036 K 0.07692 +pep_036 L 0.0 +pep_036 M 0.0 +pep_036 N 0.0 +pep_036 P 0.07692 +pep_036 Q 0.15385 +pep_036 R 0.07692 +pep_036 S 0.0 +pep_036 T 0.0 +pep_036 V 0.07692 +pep_036 W 0.0 +pep_036 Y 0.07692 +pep_037 A 0.09091 +pep_037 C 0.0 +pep_037 D 0.04545 +pep_037 E 0.04545 +pep_037 F 0.09091 +pep_037 G 0.0 +pep_037 H 0.09091 +pep_037 I 0.0 +pep_037 K 0.0 +pep_037 L 0.13636 +pep_037 M 0.04545 +pep_037 N 0.04545 +pep_037 P 0.0 +pep_037 Q 0.04545 +pep_037 R 0.13636 +pep_037 S 0.04545 +pep_037 T 0.04545 +pep_037 V 0.04545 +pep_037 W 0.0 +pep_037 Y 0.09091 +pep_038 A 0.06667 +pep_038 C 0.06667 +pep_038 D 0.1 +pep_038 E 0.06667 +pep_038 F 0.0 +pep_038 G 0.0 +pep_038 H 0.03333 +pep_038 I 0.03333 +pep_038 K 0.16667 +pep_038 L 0.06667 +pep_038 M 0.0 +pep_038 N 0.03333 +pep_038 P 0.1 +pep_038 Q 0.0 +pep_038 R 0.03333 +pep_038 S 0.06667 +pep_038 T 0.03333 +pep_038 V 0.06667 +pep_038 W 0.03333 +pep_038 Y 0.03333 +pep_039 A 0.0 +pep_039 C 0.0 +pep_039 D 0.05556 +pep_039 E 0.11111 +pep_039 F 0.05556 +pep_039 G 0.05556 +pep_039 H 0.05556 +pep_039 I 0.05556 +pep_039 K 0.0 +pep_039 L 0.11111 +pep_039 M 0.0 +pep_039 N 0.0 +pep_039 P 0.05556 +pep_039 Q 0.11111 +pep_039 R 0.0 +pep_039 S 0.0 +pep_039 T 0.16667 +pep_039 V 0.11111 +pep_039 W 0.05556 +pep_039 Y 0.0 +pep_040 A 0.0 +pep_040 C 0.0 +pep_040 D 0.0 +pep_040 E 0.125 +pep_040 F 0.125 +pep_040 G 0.0625 +pep_040 H 0.0 +pep_040 I 0.0 +pep_040 K 0.0 +pep_040 L 0.125 +pep_040 M 0.0 +pep_040 N 0.125 +pep_040 P 0.125 +pep_040 Q 0.0 +pep_040 R 0.125 +pep_040 S 0.0 +pep_040 T 0.0 +pep_040 V 0.0625 +pep_040 W 0.125 +pep_040 Y 0.0 +pep_041 A 0.10714 +pep_041 C 0.10714 +pep_041 D 0.07143 +pep_041 E 0.0 +pep_041 F 0.03571 +pep_041 G 0.0 +pep_041 H 0.07143 +pep_041 I 0.0 +pep_041 K 0.0 +pep_041 L 0.03571 +pep_041 M 0.03571 +pep_041 N 0.07143 +pep_041 P 0.07143 +pep_041 Q 0.03571 +pep_041 R 0.03571 +pep_041 S 0.21429 +pep_041 T 0.07143 +pep_041 V 0.0 +pep_041 W 0.0 +pep_041 Y 0.03571 +pep_042 A 0.09091 +pep_042 C 0.0 +pep_042 D 0.0 +pep_042 E 0.09091 +pep_042 F 0.09091 +pep_042 G 0.0 +pep_042 H 0.09091 +pep_042 I 0.09091 +pep_042 K 0.09091 +pep_042 L 0.18182 +pep_042 M 0.0 +pep_042 N 0.0 +pep_042 P 0.0 +pep_042 Q 0.09091 +pep_042 R 0.0 +pep_042 S 0.0 +pep_042 T 0.0 +pep_042 V 0.0 +pep_042 W 0.09091 +pep_042 Y 0.09091 +pep_043 A 0.0 +pep_043 C 0.0 +pep_043 D 0.0 +pep_043 E 0.0 +pep_043 F 0.13636 +pep_043 G 0.09091 +pep_043 H 0.09091 +pep_043 I 0.04545 +pep_043 K 0.0 +pep_043 L 0.0 +pep_043 M 0.13636 +pep_043 N 0.04545 +pep_043 P 0.13636 +pep_043 Q 0.13636 +pep_043 R 0.09091 +pep_043 S 0.04545 +pep_043 T 0.04545 +pep_043 V 0.0 +pep_043 W 0.0 +pep_043 Y 0.0 +pep_044 A 0.0 +pep_044 C 0.18182 +pep_044 D 0.09091 +pep_044 E 0.0 +pep_044 F 0.0 +pep_044 G 0.09091 +pep_044 H 0.04545 +pep_044 I 0.09091 +pep_044 K 0.0 +pep_044 L 0.09091 +pep_044 M 0.0 +pep_044 N 0.09091 +pep_044 P 0.04545 +pep_044 Q 0.04545 +pep_044 R 0.04545 +pep_044 S 0.0 +pep_044 T 0.04545 +pep_044 V 0.0 +pep_044 W 0.04545 +pep_044 Y 0.09091 +pep_045 A 0.07143 +pep_045 C 0.10714 +pep_045 D 0.03571 +pep_045 E 0.0 +pep_045 F 0.03571 +pep_045 G 0.03571 +pep_045 H 0.03571 +pep_045 I 0.07143 +pep_045 K 0.07143 +pep_045 L 0.03571 +pep_045 M 0.03571 +pep_045 N 0.03571 +pep_045 P 0.03571 +pep_045 Q 0.07143 +pep_045 R 0.07143 +pep_045 S 0.07143 +pep_045 T 0.03571 +pep_045 V 0.07143 +pep_045 W 0.03571 +pep_045 Y 0.03571 +pep_046 A 0.07143 +pep_046 C 0.07143 +pep_046 D 0.07143 +pep_046 E 0.07143 +pep_046 F 0.0 +pep_046 G 0.0 +pep_046 H 0.0 +pep_046 I 0.0 +pep_046 K 0.07143 +pep_046 L 0.0 +pep_046 M 0.28571 +pep_046 N 0.0 +pep_046 P 0.0 +pep_046 Q 0.0 +pep_046 R 0.07143 +pep_046 S 0.0 +pep_046 T 0.07143 +pep_046 V 0.07143 +pep_046 W 0.0 +pep_046 Y 0.14286 +pep_047 A 0.0 +pep_047 C 0.0 +pep_047 D 0.07143 +pep_047 E 0.0 +pep_047 F 0.0 +pep_047 G 0.0 +pep_047 H 0.21429 +pep_047 I 0.0 +pep_047 K 0.14286 +pep_047 L 0.14286 +pep_047 M 0.0 +pep_047 N 0.14286 +pep_047 P 0.07143 +pep_047 Q 0.0 +pep_047 R 0.0 +pep_047 S 0.07143 +pep_047 T 0.14286 +pep_047 V 0.0 +pep_047 W 0.0 +pep_047 Y 0.0 +pep_048 A 0.05263 +pep_048 C 0.10526 +pep_048 D 0.15789 +pep_048 E 0.10526 +pep_048 F 0.0 +pep_048 G 0.0 +pep_048 H 0.0 +pep_048 I 0.10526 +pep_048 K 0.0 +pep_048 L 0.05263 +pep_048 M 0.0 +pep_048 N 0.0 +pep_048 P 0.0 +pep_048 Q 0.05263 +pep_048 R 0.21053 +pep_048 S 0.10526 +pep_048 T 0.0 +pep_048 V 0.05263 +pep_048 W 0.0 +pep_048 Y 0.0 +pep_049 A 0.0 +pep_049 C 0.08333 +pep_049 D 0.08333 +pep_049 E 0.0 +pep_049 F 0.0 +pep_049 G 0.08333 +pep_049 H 0.08333 +pep_049 I 0.08333 +pep_049 K 0.0 +pep_049 L 0.0 +pep_049 M 0.0 +pep_049 N 0.0 +pep_049 P 0.08333 +pep_049 Q 0.16667 +pep_049 R 0.08333 +pep_049 S 0.08333 +pep_049 T 0.0 +pep_049 V 0.08333 +pep_049 W 0.0 +pep_049 Y 0.08333 +pep_below_floor A 0.16667 +pep_below_floor C 0.16667 +pep_below_floor D 0.16667 +pep_below_floor E 0.16667 +pep_below_floor F 0.16667 +pep_below_floor G 0.16667 +pep_below_floor H 0.0 +pep_below_floor I 0.0 +pep_below_floor K 0.0 +pep_below_floor L 0.0 +pep_below_floor M 0.0 +pep_below_floor N 0.0 +pep_below_floor P 0.0 +pep_below_floor Q 0.0 +pep_below_floor R 0.0 +pep_below_floor S 0.0 +pep_below_floor T 0.0 +pep_below_floor V 0.0 +pep_below_floor W 0.0 +pep_below_floor Y 0.0 +pep_empty A +pep_empty C +pep_empty D +pep_empty E +pep_empty F +pep_empty G +pep_empty H +pep_empty I +pep_empty K +pep_empty L +pep_empty M +pep_empty N +pep_empty P +pep_empty Q +pep_empty R +pep_empty S +pep_empty T +pep_empty V +pep_empty W +pep_empty Y +pep_homopolymer A 1.0 +pep_homopolymer C 0.0 +pep_homopolymer D 0.0 +pep_homopolymer E 0.0 +pep_homopolymer F 0.0 +pep_homopolymer G 0.0 +pep_homopolymer H 0.0 +pep_homopolymer I 0.0 +pep_homopolymer K 0.0 +pep_homopolymer L 0.0 +pep_homopolymer M 0.0 +pep_homopolymer N 0.0 +pep_homopolymer P 0.0 +pep_homopolymer Q 0.0 +pep_homopolymer R 0.0 +pep_homopolymer S 0.0 +pep_homopolymer T 0.0 +pep_homopolymer V 0.0 +pep_homopolymer W 0.0 +pep_homopolymer Y 0.0 +pep_no_aromatic A 0.05882 +pep_no_aromatic C 0.05882 +pep_no_aromatic D 0.05882 +pep_no_aromatic E 0.05882 +pep_no_aromatic F 0.0 +pep_no_aromatic G 0.05882 +pep_no_aromatic H 0.05882 +pep_no_aromatic I 0.05882 +pep_no_aromatic K 0.05882 +pep_no_aromatic L 0.05882 +pep_no_aromatic M 0.05882 +pep_no_aromatic N 0.05882 +pep_no_aromatic P 0.05882 +pep_no_aromatic Q 0.05882 +pep_no_aromatic R 0.05882 +pep_no_aromatic S 0.05882 +pep_no_aromatic T 0.05882 +pep_no_aromatic V 0.05882 +pep_no_aromatic W 0.0 +pep_no_aromatic Y 0.0 +pep_nonstandard_only A +pep_nonstandard_only C +pep_nonstandard_only D +pep_nonstandard_only E +pep_nonstandard_only F +pep_nonstandard_only G +pep_nonstandard_only H +pep_nonstandard_only I +pep_nonstandard_only K +pep_nonstandard_only L +pep_nonstandard_only M +pep_nonstandard_only N +pep_nonstandard_only P +pep_nonstandard_only Q +pep_nonstandard_only R +pep_nonstandard_only S +pep_nonstandard_only T +pep_nonstandard_only V +pep_nonstandard_only W +pep_nonstandard_only Y +pep_single_residue A 1.0 +pep_single_residue C 0.0 +pep_single_residue D 0.0 +pep_single_residue E 0.0 +pep_single_residue F 0.0 +pep_single_residue G 0.0 +pep_single_residue H 0.0 +pep_single_residue I 0.0 +pep_single_residue K 0.0 +pep_single_residue L 0.0 +pep_single_residue M 0.0 +pep_single_residue N 0.0 +pep_single_residue P 0.0 +pep_single_residue Q 0.0 +pep_single_residue R 0.0 +pep_single_residue S 0.0 +pep_single_residue T 0.0 +pep_single_residue V 0.0 +pep_single_residue W 0.0 +pep_single_residue Y 0.0 +pep_stop_codon A +pep_stop_codon C +pep_stop_codon D +pep_stop_codon E +pep_stop_codon F +pep_stop_codon G +pep_stop_codon H +pep_stop_codon I +pep_stop_codon K +pep_stop_codon L +pep_stop_codon M +pep_stop_codon N +pep_stop_codon P +pep_stop_codon Q +pep_stop_codon R +pep_stop_codon S +pep_stop_codon T +pep_stop_codon V +pep_stop_codon W +pep_stop_codon Y diff --git a/software/tests/data/characterization/golden/peptide.properties.tsv b/software/tests/data/characterization/golden/peptide.properties.tsv new file mode 100644 index 0000000..3ccf097 --- /dev/null +++ b/software/tests/data/characterization/golden/peptide.properties.tsv @@ -0,0 +1,58 @@ +entity_key charge_peptide chargeShift_peptide gravy_peptide mw_peptide pi_peptide eox_peptide ered_peptide instability_peptide aliphatic_peptide aromaticity_peptide +pep_000 0.972 -1.197 -0.485 2491.8156 7.754 8480.0 8480.0 28.74 53.5 0.25 +pep_001 -2.886 -0.931 -0.91667 1495.6329 4.056 1490.0 1490.0 85.5667 56.6667 0.08333 +pep_002 -0.957 -1.046 -0.064 2970.404 5.517 6990.0 6990.0 17.444 78.0 0.12 +pep_003 -1.893 -0.947 0.38333 2252.586 4.659 8480.0 8480.0 62.1111 129.4444 0.16667 +pep_004 -1.961 -1.046 0.065 2334.6468 4.295 7115.0 6990.0 29.435 53.5 0.2 +pep_005 0.047 -1.041 -1.59375 2167.3608 7.061 17990.0 17990.0 85.25 24.375 0.25 +pep_006 -4.741 -1.808 -0.42143 3458.8727 4.34 14105.0 13980.0 66.5893 59.2857 0.17857 +pep_007 -3.744 -1.778 -0.93684 2197.3174 4.49 3105.0 2980.0 33.3684 46.3158 0.10526 +pep_008 2.183 -1.879 -1.31429 3213.5677 8.195 8605.0 8480.0 21.8393 13.9286 0.10714 +pep_009 0.835 -0.351 -1.3875 973.0833 8.056 5500.0 5500.0 48.75 0.125 +pep_010 0.832 -0.368 0.475 1382.6508 8.023 0.0 0.0 0.4833 89.1667 0.08333 +pep_011 1.094 -0.905 -0.46296 3379.8918 8.252 19605.0 19480.0 35.163 61.4815 0.22222 +pep_012 1.824 -0.376 -0.39 1071.2081 8.672 1490.0 1490.0 60.59 39.0 0.1 +pep_013 0.32 -1.514 -0.14444 2059.3306 7.343 1490.0 1490.0 26.9556 60.0 0.11111 +pep_014 -1.734 -1.705 -1.91818 1311.3562 5.504 1490.0 1490.0 48.6182 35.4545 0.09091 +pep_015 0.777 -0.55 -2.5875 1051.1539 7.788 0.0 0.0 0.0 0.125 +pep_016 1.968 -1.168 -0.10833 1485.8185 8.231 7115.0 6990.0 76.9667 32.5 0.16667 +pep_017 1.973 -1.168 -0.23571 3425.0167 8.257 12615.0 12490.0 37.7964 41.7857 0.21429 +pep_018 -4.095 -0.318 0.675 2223.5616 3.276 0.0 0.0 72.605 126.5 0.05 +pep_019 -5.881 -0.979 -0.44348 2671.9749 3.584 5625.0 5500.0 84.8435 38.2609 0.08696 +pep_020 -0.729 -1.667 -1.435 2434.5807 6.416 11000.0 11000.0 92.335 58.5 0.1 +pep_021 0.689 -3.021 -1.4375 3130.4126 7.392 8480.0 8480.0 80.7167 12.0833 0.25 +pep_022 -0.969 -1.054 0.16071 3324.8648 5.362 8605.0 8480.0 43.0357 41.7857 0.17857 +pep_023 1.895 -0.22 1.07 1213.5176 10.31 0.0 0.0 28.26 88.0 0.2 +pep_024 -1.909 -0.922 -0.96316 2318.4721 4.035 9970.0 9970.0 68.7737 35.7895 0.21053 +pep_025 0.761 -0.525 0.48462 2842.4454 7.747 5625.0 5500.0 59.7846 82.6923 0.07692 +pep_026 0.878 -0.27 -1.39167 1551.7044 8.239 3105.0 2980.0 5.725 0.0 0.25 +pep_027 -1.116 -0.283 0.50909 1373.551 3.742 2980.0 2980.0 60.2545 35.4545 0.36364 +pep_028 -1.12 -0.329 -0.32857 1811.0842 4.53 4470.0 4470.0 95.8643 55.7143 0.28571 +pep_029 -0.521 -2.358 -1.18276 3530.8415 6.693 11125.0 11000.0 32.5586 30.3448 0.10345 +pep_030 -5.152 -0.476 -2.25238 2598.6263 3.806 5500.0 5500.0 60.9619 9.5238 0.09524 +pep_031 -1.68 -1.552 -0.54211 2105.1819 5.373 2980.0 2980.0 -7.3737 67.3684 0.15789 +pep_032 1.053 -1.015 -0.48 1750.9744 8.076 0.0 0.0 77.38 58.6667 0.13333 +pep_033 -2.108 -0.275 0.848 2952.4677 3.389 12490.0 12490.0 42.5044 93.6 0.24 +pep_034 -2.102 -0.246 -0.74 2480.7085 3.758 5500.0 5500.0 23.145 73.0 0.15 +pep_035 0.115 -0.854 -1.6625 1000.0904 7.194 0.0 0.0 48.75 0.0 +pep_036 -0.166 -0.427 -1.12308 1550.7131 6.27 1490.0 1490.0 26.5308 52.3077 0.15385 +pep_037 1.32 -1.552 -0.51364 2768.115 8.381 2980.0 2980.0 4.2 75.4545 0.18182 +pep_038 0.789 -1.665 -0.94667 3471.9582 7.452 7115.0 6990.0 32.94 65.0 0.06667 +pep_039 -2.879 -0.914 -0.13333 2113.3251 4.056 5500.0 5500.0 25.1611 97.2222 0.11111 +pep_040 -0.095 -0.271 -0.6875 2060.3152 6.383 11000.0 11000.0 60.5813 66.875 0.25 +pep_041 -0.686 -1.54 -0.63214 3031.2538 6.417 1615.0 1490.0 95.2071 24.6429 0.07143 +pep_042 0.046 -1.032 0.03636 1447.6777 7.06 6990.0 6990.0 51.6273 115.4545 0.27273 +pep_043 2.33 -1.48 -0.81364 2645.0519 11.493 0.0 0.0 85.3136 17.7273 0.13636 +pep_044 -0.912 -0.931 -0.23636 2587.9295 5.366 8730.0 8480.0 5.0409 70.9091 0.13636 +pep_045 2.969 -1.177 -0.125 3255.8162 8.651 7115.0 6990.0 37.9964 69.6429 0.10714 +pep_046 -0.179 -0.431 -0.18571 1772.1608 6.163 2980.0 2980.0 35.6714 27.8571 0.14286 +pep_047 1.418 -2.403 -1.72143 1641.7859 7.858 0.0 0.0 4.0143 55.7143 0.0 +pep_048 -1.1 -0.315 -0.88421 2264.4996 4.842 125.0 0.0 137.6684 82.1053 0.0 +pep_049 0.104 -0.88 -0.925 1402.5346 7.168 1490.0 1490.0 19.025 56.6667 0.08333 +pep_below_floor -2.101 -0.258 -0.05 640.6626 3.389 0.0 0.0 16.6667 0.16667 +pep_empty +pep_homopolymer -0.101 -0.211 1.8 728.7943 5.462 0.0 0.0 9.0 100.0 0.0 +pep_no_aromatic 0.051 -1.033 -0.61176 1899.1565 7.066 0.0 0.0 97.9294 68.8235 0.0 +pep_nonstandard_only +pep_single_residue -0.101 -0.211 1.8 89.0932 5.462 0.0 0.0 100.0 0.0 +pep_stop_codon diff --git a/software/tests/data/characterization/golden/peptide.stats.json b/software/tests/data/characterization/golden/peptide.stats.json new file mode 100644 index 0000000..a3fd817 --- /dev/null +++ b/software/tests/data/characterization/golden/peptide.stats.json @@ -0,0 +1 @@ +{"hasPeptideBelowInstabilityFloor":true,"medianCdr3Length":{}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/sc_dropout.properties.tsv b/software/tests/data/characterization/golden/sc_dropout.properties.tsv new file mode 100644 index 0000000..573cd49 --- /dev/null +++ b/software/tests/data/characterization/golden/sc_dropout.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion charge_Fv chargeShift_Fv pi_Fv eox_Fv ered_Fv mw_Fv +clone_000 -0.164 -0.359 -1.66 -5.839 4.476 -0.86154 6292.0732 12740.0 12490.0 54.9596 50.5769 0.09615 +clone_001 1.831 -0.359 -1.0375 0.323 -1.497 -0.56364 -2.723 5.714 -1.13 6715.2272 11710.0 11460.0 50.615 34.1667 0.08333 -4.711 5.237 -1.00794 7433.9677 32095.0 31970.0 34.1905 41.746 0.1746 -7.434 -3.836 5.467 43805.0 43430.0 14149.1949 +clone_002 1.899 -0.211 -0.12 2.915 -1.281 -0.0 0.205 7.5 -0.64906 6609.4951 36105.0 35980.0 12.0208 53.3962 0.20755 5.255 9.57 -0.5873 7455.6765 8730.0 8480.0 28.4968 78.8889 0.06349 5.46 -3.364 9.138 44835.0 44460.0 14065.1716 +clone_003 0.836 -0.368 -2.76 3.032 -1.027 -0.57778 -1.759 5.863 -0.52881 7007.7221 26930.0 26930.0 27.3136 62.7119 0.18644 3.291 9.355 -0.80154 7785.7825 13075.0 12950.0 37.2277 62.9231 0.12308 1.532 -3.666 8.475 40005.0 39880.0 14793.5046 +clone_004 -0.116 -0.245 0.75455 -0.849 6.027 -0.07869 6922.9126 22585.0 22460.0 23.718 75.082 0.14754 +clone_005 -2.11 -0.292 -1.625 -0.897 -0.947 0.11111 -2.773 5.379 -0.17571 8401.5326 31190.0 30940.0 55.2071 61.2857 0.2 -8.747 4.4 -1.10139 8144.6844 22585.0 22460.0 36.7972 39.3056 0.11111 -11.52 -3.135 4.702 53775.0 53400.0 16546.217 +clone_006 -0.171 -0.376 0.47 -1.692 -1.606 -0.56154 1.177 8.697 -0.35862 7464.7551 24075.0 23950.0 36.7088 37.069 0.24138 -12.694 4.0 -0.22121 7637.3073 31065.0 30940.0 56.1729 56.2121 0.19697 -11.517 -3.211 4.785 55140.0 54890.0 15102.0624 +clone_007 -0.169 -0.397 -0.75455 -0.167 -0.377 -0.94 -0.754 6.337 -0.85625 7436.2358 4595.0 4470.0 40.6094 67.0312 0.07812 1.142 8.595 0.09508 7167.7103 12865.0 12490.0 37.2016 70.3279 0.08197 0.388 -2.727 7.484 17460.0 16960.0 14603.9461 +clone_008 0.114 -0.845 0.59167 3.234 9.643 0.01111 7407.6095 19605.0 19480.0 56.0206 72.6984 0.14286 +clone_009 -3.89 -0.927 -0.30625 -1.098 -0.241 0.325 -3.725 5.403 -0.32188 7759.8195 22710.0 22460.0 73.4562 65.625 0.15625 0.213 7.497 -0.72667 7314.2264 25105.0 24980.0 27.2218 55.1667 0.16667 -3.512 -3.362 5.906 47815.0 47440.0 15074.0459 +clone_010 0.827 -0.406 -0.65556 0.899 -0.211 0.26667 -1.691 6.06 -0.19857 7769.8116 17210.0 16960.0 44.0043 71.0 0.08571 -3.802 4.834 0.00517 6729.7795 36105.0 35980.0 41.5638 55.5172 0.17241 -5.493 -3.305 5.581 53315.0 52940.0 14499.5911 +clone_011 2.04 -1.02 -0.20556 -0.115 -0.244 0.52 3.256 9.247 -0.77246 8075.1751 17085.0 16960.0 32.2567 56.3768 0.11594 3.231 9.337 -0.41569 5937.7692 19480.0 19480.0 21.6863 65.098 0.15686 6.487 -3.553 9.288 36565.0 36440.0 14012.9443 +clone_012 1.764 -0.555 -1.18235 1.235 8.501 -1.06667 8236.2684 43220.0 42970.0 68.4515 57.7273 0.15152 +clone_013 -0.681 -1.543 -1.02 0.891 -0.228 -1.0625 -8.704 4.502 -0.2 7517.5797 41730.0 41480.0 41.746 69.8413 0.14286 0.24 7.678 -0.63519 6266.9321 26720.0 26470.0 24.5465 25.3704 0.18519 -8.464 -3.509 5.221 68450.0 67950.0 13784.5118 +clone_014 1.33 -1.48 -1.07 0.042 -1.037 -1.925 4.271 10.106 -0.07833 6785.0074 14105.0 13980.0 38.21 78.0 0.06667 -4.654 5.523 -0.93429 8243.1535 22835.0 22460.0 61.4186 36.2857 0.1 -0.383 -4.287 6.789 36940.0 36440.0 15028.1609 +clone_015 1.892 -0.228 -0.27692 0.047 -1.003 0.63077 -2.775 5.427 -0.615 6869.5887 7115.0 6990.0 62.155 45.5 0.1 -2.783 5.456 -0.42121 7856.7972 28085.0 27960.0 50.7744 65.0 0.19697 -5.558 -2.91 5.442 35200.0 34950.0 14726.3859 +clone_016 0.1 -0.879 1.84286 -3.682 5.528 0.67049 7295.5074 35075.0 34950.0 49.1934 104.0984 0.21311 +clone_017 0.888 -0.236 0.78125 0.831 -0.359 0.9875 0.243 7.651 0.03077 7583.6675 26595.0 26470.0 37.2094 87.0769 0.15385 2.149 9.326 -0.64038 6113.0212 17460.0 16960.0 40.1596 30.1923 0.15385 2.392 -2.558 8.936 44055.0 43430.0 13696.6887 +clone_018 1.043 -1.003 0.21176 -1.888 -0.872 -0.4 3.261 9.341 -0.01159 7901.3123 22250.0 22000.0 46.4623 70.7246 0.11594 -1.683 6.054 -0.83667 6948.8491 14105.0 13980.0 65.305 30.8333 0.08333 1.578 -3.952 8.578 36355.0 35980.0 14850.1614 +clone_019 1.043 -1.002 -0.85556 -0.026 -1.176 -0.85556 -4.716 5.331 -1.14561 6665.2924 15720.0 15470.0 47.5754 34.2105 0.12281 1.26 8.619 -0.29718 8267.5072 24325.0 23950.0 18.5239 57.7465 0.15493 -3.457 -3.928 6.007 40045.0 39420.0 14932.7996 +clone_020 -0.101 -0.22 -1.55 5.22 9.66 -0.79643 6309.1602 3105.0 2980.0 62.9446 41.7857 0.07143 +clone_021 -0.116 -0.245 0.44444 -0.171 -0.376 -0.41667 -4.782 4.828 -0.11429 7518.5624 28210.0 27960.0 27.4333 61.9048 0.1746 -3.782 5.138 -0.48361 7194.0596 19480.0 19480.0 44.1952 46.3934 0.18033 -8.564 -2.946 4.982 47690.0 47440.0 14712.622 +clone_022 -0.162 -0.38 0.58333 -3.238 -0.619 -1.25333 -6.71 4.868 0.10175 6520.621 18115.0 17990.0 83.4561 76.8421 0.07018 1.295 8.685 -0.36349 7573.7453 11585.0 11460.0 47.4937 74.127 0.12698 -5.415 -4.074 5.76 29700.0 29450.0 14094.3663 +clone_023 -0.948 -1.003 0.09091 1.323 -1.544 -1.46111 -0.699 6.444 0.02381 7566.7744 30855.0 30480.0 37.6635 85.0794 0.14286 -2.66 5.849 -0.62273 7727.5873 14690.0 14440.0 68.4426 32.5758 0.15152 -3.359 -4.363 6.084 45545.0 44920.0 15294.3617 +clone_024 -0.892 -0.91 -0.05333 -6.773 4.305 -0.10615 7353.1717 22585.0 22460.0 34.0092 78.0 0.13846 +clone_025 -1.457 -2.141 -1.1 0.103 -0.871 0.27857 1.32 8.717 -0.37121 7727.9185 15720.0 15470.0 48.4045 62.1212 0.09091 5.217 9.667 -0.475 7492.7394 22250.0 22000.0 25.5016 54.8438 0.10938 6.537 -3.901 9.306 37970.0 37470.0 15220.6579 +clone_026 -2.883 -0.952 -2.01818 1.707 -0.639 -0.59 -4.679 5.401 -1.41846 8268.8688 32555.0 32430.0 30.5292 30.0 0.2 3.216 9.071 -0.95614 7010.0101 23615.0 23490.0 44.8649 56.3158 0.12281 -1.463 -3.853 6.396 56170.0 55920.0 15278.8789 +clone_027 -2.226 -0.606 -2.2 -0.107 -0.236 -0.52778 -1.686 6.09 -1.27941 8319.1015 14690.0 14440.0 30.7956 34.4118 0.14706 -1.784 5.801 -0.56286 8098.1088 22585.0 22460.0 64.4044 43.1429 0.15714 -3.469 -3.698 5.973 37275.0 36900.0 16417.2103 +clone_028 2.918 -1.274 -0.52941 0.263 7.47 -0.80299 7575.4591 14105.0 13980.0 38.6746 65.6716 0.07463 +clone_029 -1.886 -0.922 -0.53636 -0.162 -0.38 0.03333 -2.748 5.603 -0.7254 7499.2915 33585.0 33460.0 29.3095 55.7143 0.14286 2.294 9.12 -0.32807 6945.0539 22125.0 22000.0 43.6825 66.8421 0.15789 -0.453 -3.795 6.737 55710.0 55460.0 14444.3454 +clone_030 -0.9 -0.917 -0.74167 -0.889 -0.863 0.68 -6.665 5.088 -0.41639 7717.7312 30940.0 30940.0 62.9902 59.0164 0.21311 -2.783 5.37 -0.00968 7034.0164 12615.0 12490.0 50.1903 84.8387 0.09677 -9.448 -3.611 5.173 43555.0 43430.0 14751.7476 +clone_031 1.107 -0.862 -0.45 -0.119 -0.253 0.62222 0.193 7.714 -0.16377 8092.2783 26720.0 26470.0 29.5783 67.8261 0.14493 -0.817 6.148 -0.00339 7088.1741 33585.0 33460.0 23.5188 54.5763 0.22034 -0.624 -2.388 6.545 60305.0 59930.0 15180.4524 +clone_032 -0.108 -0.227 -0.97143 3.301 9.796 -0.20517 6847.846 11460.0 11460.0 15.2224 107.4138 0.10345 +clone_033 -0.886 -0.884 -0.9 -1.1 -0.22 -0.55714 -3.75 5.344 0.08906 7872.1726 37720.0 37470.0 58.8609 75.9375 0.20312 1.232 8.732 -0.58 6426.2015 21220.0 20970.0 61.6964 48.0 0.16364 -2.518 -3.341 6.091 58940.0 58440.0 14298.3741 +clone_034 -2.103 -0.237 -0.6 -2.097 -0.25 -0.52857 -5.841 3.815 -0.01538 7027.8531 22125.0 22000.0 28.1769 73.6923 0.10769 -3.777 5.162 -0.44035 6973.9745 22125.0 22000.0 46.2544 68.4211 0.12281 -9.619 -2.437 4.549 44250.0 44000.0 14001.8276 +clone_035 -3.889 -0.977 -0.97692 -2.099 -0.318 0.01333 -0.772 6.342 -0.85323 7400.3873 22585.0 22460.0 31.2935 58.2258 0.12903 -7.795 4.038 -0.33235 7777.6314 32220.0 31970.0 32.0485 55.8824 0.13235 -8.567 -2.996 5.024 54805.0 54430.0 15178.0187 +clone_036 1.051 -0.994 -0.73889 4.255 9.49 -0.95417 9035.2428 50085.0 49960.0 31.3014 56.8056 0.18056 +clone_037 1.104 -0.88 -0.53125 0.768 -0.508 -0.42941 2.272 9.455 -0.82121 7884.7611 22585.0 22460.0 43.3926 56.2121 0.16667 0.2 7.466 0.10588 8173.5096 29700.0 29450.0 52.7222 74.5588 0.20588 2.472 -3.256 8.791 52285.0 51910.0 16058.2707 +clone_038 0.046 -1.032 -1.95 1.771 -0.49 -0.8 4.286 9.572 -1.28246 6784.5998 12950.0 12950.0 23.9421 37.7193 0.14035 5.257 9.775 -0.66071 7026.0937 24075.0 23950.0 52.1554 69.4643 0.17857 9.543 -3.963 9.678 37025.0 36900.0 13810.6935 +clone_039 0.899 -0.211 -0.8 0.81 -0.409 -0.275 6.287 10.29 -0.48814 7118.2233 10095.0 9970.0 35.5051 47.7966 0.16949 -3.755 5.277 -0.30526 6815.8239 21680.0 21430.0 74.2105 41.0526 0.19298 2.532 -3.699 8.8 31775.0 31400.0 13934.0472 +clone_040 -0.899 -0.888 -0.52857 -2.718 5.676 0.06613 7355.4419 19730.0 19480.0 39.6226 81.7742 0.16129 +clone_041 1.761 -0.525 -0.85882 0.835 -0.351 0.66 3.161 9.055 -0.56 7854.2327 26720.0 26470.0 48.5015 63.0769 0.12308 -2.788 5.455 -1.116 6094.5839 13980.0 13980.0 42.37 50.8 0.16 0.373 -2.771 7.424 40700.0 40450.0 13948.8166 +clone_042 -5.097 -0.255 -0.26667 0.814 -0.401 -0.42143 -3.772 5.296 -0.96324 8199.066 23740.0 23490.0 57.7926 64.5588 0.11765 6.277 9.708 -0.74769 8273.5659 36690.0 36440.0 41.52 40.4615 0.23077 2.504 -3.85 8.482 60430.0 59930.0 16472.6319 +clone_043 0.765 -0.516 0.99286 -5.096 -0.344 -1.03889 -0.789 6.24 0.02 7224.2721 23045.0 22920.0 25.5367 74.6667 0.25 -4.681 5.405 -0.37941 8085.0208 16960.0 16960.0 43.7457 80.2941 0.16176 -5.471 -3.599 5.657 40005.0 39880.0 15309.2929 +clone_044 0.053 -1.015 -1.225 -3.72 5.43 0.29474 6426.2955 18115.0 17990.0 9.014 81.9298 0.15789 +clone_045 -0.169 -0.397 -0.04 -2.886 -0.931 -1.16 -2.75 5.615 -0.7629 7151.896 16960.0 16960.0 54.1387 48.871 0.1129 -4.744 4.979 -0.57258 6971.5642 14105.0 13980.0 25.8873 50.3226 0.14516 -7.494 -3.359 5.313 31065.0 30940.0 14123.4602 +clone_046 -1.175 -0.423 -0.52143 -2.1 -0.229 0.35833 0.223 7.422 -0.56825 7560.4915 29450.0 29450.0 36.3095 63.3333 0.20635 -0.749 6.34 -0.68814 7292.1362 15595.0 15470.0 57.7847 51.0169 0.18644 -0.526 -3.412 6.683 45045.0 44920.0 14852.6277 +clone_047 -0.101 -0.211 -0.33333 -0.949 -1.033 -1.28667 -0.784 6.259 -0.47018 6627.388 11460.0 11460.0 63.1544 65.0877 0.15789 2.267 8.967 -0.8971 8366.4512 25565.0 25440.0 39.4957 46.6667 0.17391 1.483 -3.437 8.362 37025.0 36900.0 14993.8392 +clone_048 1.899 -0.211 -1.075 0.243 7.592 -0.18036 7216.3232 40450.0 40450.0 56.8 78.2143 0.26786 +clone_049 -0.957 -1.05 0.44667 0.831 -0.359 0.12857 2.171 8.987 -0.66515 7525.5923 15845.0 15470.0 46.8955 45.9091 0.09091 3.193 9.22 -0.58246 6887.0247 40115.0 39990.0 58.2298 39.2982 0.17544 5.365 -2.688 9.108 55960.0 55460.0 14412.617 diff --git a/software/tests/data/characterization/golden/sc_dropout.stats.json b/software/tests/data/characterization/golden/sc_dropout.stats.json new file mode 100644 index 0000000..56c0d20 --- /dev/null +++ b/software/tests/data/characterization/golden/sc_dropout.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":12.0,"B":12.0}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/tcr_ab.properties.tsv b/software/tests/data/characterization/golden/tcr_ab.properties.tsv new file mode 100644 index 0000000..c71ae12 --- /dev/null +++ b/software/tests/data/characterization/golden/tcr_ab.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion +clone_000 2.835 -3.706 -2.12353 1.322 -1.497 -0.05 -2.567 6.036 -0.61014 8071.0758 6085.0 5960.0 31.2928 60.8696 0.08696 4.215 9.16 -1.25538 7692.6782 12740.0 12490.0 47.1323 36.0 0.09231 +clone_001 -2.095 -0.271 0.4 -0.101 -0.211 0.57 -4.764 5.016 -0.97193 6668.1917 16500.0 16500.0 26.907 39.2982 0.14035 -0.759 6.322 -0.36508 7317.2418 10095.0 9970.0 38.2667 57.3016 0.14286 +clone_002 -2.094 -0.279 -1.85833 0.824 -0.414 -0.70833 -2.685 5.791 -0.85806 7459.2522 16960.0 16960.0 53.3742 37.9032 0.16129 -1.776 5.779 -0.27097 7533.3595 24535.0 24410.0 37.6355 53.2258 0.25806 +clone_003 -0.171 -0.385 -0.97333 2.134 -1.946 -1.36923 -1.726 5.991 -0.29851 7890.9926 15845.0 15470.0 57.6672 53.8806 0.14925 2.264 9.174 -0.03559 6926.1269 12740.0 12490.0 35.8898 74.2373 0.10169 +clone_004 2.315 -1.515 0.64545 -1.882 -0.884 -0.67 1.24 8.699 -0.80328 7412.436 22710.0 22460.0 34.8279 52.7869 0.13115 -1.748 5.89 -0.97213 7245.0142 30730.0 30480.0 69.0279 47.8689 0.13115 +clone_005 1.771 -0.49 0.17857 2.885 -0.244 -1.425 6.284 10.197 -0.35625 7862.1583 40240.0 39990.0 55.4453 48.75 0.1875 0.193 7.684 -0.72131 6777.4703 11460.0 11460.0 20.1279 40.1639 0.09836 +clone_006 0.097 -0.887 -0.58 0.316 -1.513 -0.73333 -1.789 5.801 -0.61833 7328.2762 29575.0 29450.0 62.135 42.1667 0.21667 -0.664 6.491 -0.41 7300.4944 20065.0 19940.0 79.6018 39.0 0.16667 +clone_007 -2.099 -0.279 0.95 -1.108 -0.237 0.36667 -5.741 4.758 0.18644 6968.9427 32220.0 31970.0 59.5356 74.4068 0.18644 0.175 7.479 -0.37458 6727.5553 39335.0 38960.0 31.5966 48.1356 0.18644 +clone_008 1.835 -0.351 -0.375 -0.162 -0.38 -0.0625 0.24 7.59 -0.46102 6995.8671 22585.0 22460.0 26.961 61.0169 0.16949 6.252 9.779 -0.49167 7286.6094 14355.0 13980.0 81.5167 48.8333 0.11667 +clone_009 -0.101 -0.211 0.7 0.824 -0.376 -1.11667 0.328 7.597 -0.82881 7073.9495 33710.0 33460.0 33.622 44.5763 0.15254 -3.77 5.157 0.04068 7094.217 26470.0 26470.0 73.4458 71.0169 0.18644 +clone_010 -2.886 -0.931 -0.92667 1.76 -0.516 -0.01111 -9.738 4.463 -0.89692 7694.4835 7115.0 6990.0 41.6354 60.1538 0.07692 4.18 9.793 -0.45522 7879.098 14230.0 13980.0 59.2582 64.0299 0.10448 +clone_011 0.1 -0.879 -0.46 0.053 -1.015 0.05 0.195 7.425 -0.44 6942.8123 30035.0 29910.0 22.4236 62.0 0.29091 -0.749 6.336 -0.33182 7634.5869 27625.0 27500.0 37.8879 47.2727 0.15152 +clone_012 -0.167 -0.377 -0.33333 -1.097 -0.25 -1.03077 -4.694 5.387 -0.94531 7778.7736 28085.0 27960.0 52.125 35.1562 0.14062 0.26 7.509 -0.83226 7252.1109 19605.0 19480.0 20.2565 45.4839 0.12903 +clone_013 1.107 -0.862 1.2 0.987 -1.134 -0.70769 1.265 8.715 0.05424 7218.4533 17085.0 16960.0 28.1644 79.1525 0.20339 -0.802 6.234 -0.49394 7862.0387 39210.0 38960.0 36.8106 40.0 0.18182 +clone_014 -0.164 -0.359 -1.2 -0.169 -0.398 -0.64 3.206 8.854 -1.07895 7044.2204 23740.0 23490.0 37.014 42.807 0.12281 6.167 9.779 -0.76949 6721.7365 14230.0 13980.0 55.2205 29.8305 0.11864 +clone_015 1.11 -0.854 -0.67647 -1.899 -0.926 -1.62727 -1.806 5.554 0.27015 7645.821 18240.0 17990.0 16.5672 82.8358 0.13433 -8.734 4.363 -0.755 7017.6061 20190.0 19940.0 54.2468 40.6667 0.18333 +clone_016 -0.105 -0.219 0.21429 0.115 -0.854 -1.55 -3.742 5.242 -0.5931 7134.8533 34615.0 34490.0 30.9431 52.069 0.2069 0.208 7.563 -0.68596 6808.6774 23740.0 23490.0 29.6789 35.7895 0.17544 +clone_017 0.818 -0.402 -0.87143 1.767 -0.499 0.0125 4.321 9.521 -0.82727 8410.7451 27055.0 26930.0 41.5061 51.6667 0.18182 3.226 9.196 -0.50345 6466.5608 6990.0 6990.0 40.5069 70.5172 0.03448 +clone_018 -0.885 -0.854 0.26 -0.108 -0.227 -0.87143 1.22 9.196 -0.58462 6272.1262 21095.0 20970.0 38.6846 69.4231 0.13462 2.299 9.116 -0.82373 7556.606 32095.0 31970.0 49.4695 49.661 0.20339 +clone_019 1.326 -1.489 0.325 0.899 -0.22 -0.74615 0.404 7.868 -0.44154 7832.8882 26595.0 26470.0 64.4848 40.7692 0.2 1.275 8.687 -0.80156 7785.0348 29115.0 28990.0 81.5578 51.7187 0.09375 +clone_020 -1.894 -0.939 0.075 0.107 -0.862 -0.51429 0.248 7.652 -0.33182 8177.1653 41035.0 40910.0 29.7879 50.303 0.27273 0.298 7.627 -0.45862 6740.5392 26470.0 26470.0 41.5397 80.8621 0.13793 +clone_021 -0.883 -0.875 -1.15714 4.912 -1.299 -2.82 1.21 8.838 -0.36842 6765.7189 7240.0 6990.0 57.9263 47.7193 0.15789 3.233 9.355 -1.03676 8194.2115 13200.0 12950.0 24.7809 47.3529 0.11765 +clone_022 -0.166 -0.389 -0.8 -1.174 -0.432 -0.55 -0.789 6.238 0.12593 6393.4628 32220.0 31970.0 48.8852 59.6296 0.18519 -5.741 4.924 -0.44714 8549.7081 25690.0 25440.0 47.8459 58.5714 0.17143 +clone_023 1.774 -0.52 -0.10556 -0.101 -0.211 0.78 3.153 9.577 0.00303 8071.4957 26595.0 26470.0 32.0106 81.2121 0.18182 -4.784 4.926 -0.67818 6449.0628 23950.0 23950.0 49.7418 54.9091 0.2 +clone_024 -0.101 -0.211 0.26 1.821 -0.384 -2.1 1.182 8.833 0.14815 6588.6547 24075.0 23950.0 15.9204 95.5556 0.22222 2.201 9.14 -0.79836 7294.192 28085.0 27960.0 31.0525 51.1475 0.16393 +clone_025 0.895 -0.22 0.66 0.032 -1.065 0.16923 -4.769 4.891 -0.37818 6235.8638 15720.0 15470.0 71.0909 46.1818 0.16364 0.238 7.489 -0.36667 7472.5783 30035.0 29910.0 45.4768 65.0 0.25 +clone_026 -1.889 -0.901 0.58182 -0.893 -0.901 0.07778 -4.746 5.134 -0.23103 6365.2995 125.0 0.0 36.7052 55.5172 0.03448 -1.683 6.052 -0.14328 7834.8447 28085.0 27960.0 36.4522 65.5224 0.14925 +clone_027 -1.163 -0.419 -0.47692 -0.164 -0.359 -0.475 -5.668 5.314 -0.42239 8187.2385 18700.0 18450.0 47.4716 50.8955 0.19403 -0.762 6.315 -0.89344 7186.9265 31970.0 31970.0 39.0557 52.7869 0.16393 +clone_028 0.831 -0.359 0.425 0.265 -1.658 -0.64444 4.318 9.499 -0.55424 7103.4777 16875.0 16500.0 27.9424 56.1017 0.0678 -7.735 4.548 -0.80909 7781.4542 31970.0 31970.0 20.3864 60.4545 0.13636 +clone_029 0.895 -0.22 0.15 -1.879 -0.914 -2.55714 1.247 9.026 -0.03519 6667.8338 17210.0 16960.0 38.1131 61.2963 0.2037 -2.65 5.883 -0.43115 7249.3472 10095.0 9970.0 54.8689 87.8689 0.08197 +clone_030 0.835 -0.351 -0.86 -4.088 -0.339 -0.95882 -4.807 4.784 -0.53871 7442.3176 24325.0 23950.0 43.0484 53.5484 0.17742 -2.803 5.338 -0.425 8044.1216 18700.0 18450.0 74.4368 56.0294 0.16176 +clone_031 -2.166 -0.466 -0.48 0.259 -1.646 -0.40833 0.218 7.637 -0.33175 7705.7221 22585.0 22460.0 42.4303 44.9206 0.2381 -1.611 6.211 -0.585 7378.2085 24200.0 23950.0 37.5517 44.0 0.23333 +clone_032 -3.1 -0.267 0.53529 0.047 -1.041 -1.38333 -6.801 4.523 -0.01791 8009.0619 17085.0 16960.0 36.0345 65.3731 0.19403 -1.764 5.892 -0.38065 7473.5019 19940.0 19940.0 32.8258 45.6452 0.19355 +clone_033 0.097 -0.887 0.8125 0.827 -0.368 -0.225 -4.711 5.237 -0.96296 6782.4035 32095.0 31970.0 55.9444 52.2222 0.18519 2.179 8.695 -0.87302 7614.6537 22585.0 22460.0 50.681 68.0952 0.14286 +clone_034 -0.164 -0.359 -1.86 -2.097 -0.25 -1.23333 0.361 7.68 -0.83455 7014.7525 40575.0 40450.0 25.1564 54.7273 0.23636 0.313 7.773 -0.52034 6996.0167 5750.0 5500.0 58.6441 59.322 0.0678 +clone_035 -0.105 -0.219 0.31429 -0.101 -0.22 -0.77 1.242 9.112 -0.64237 6910.7975 32220.0 31970.0 63.1407 34.9153 0.15254 1.21 8.7 -0.92222 7503.4822 21345.0 20970.0 43.3905 34.127 0.12698 +clone_036 -1.168 -0.406 -0.56 -0.107 -0.236 0.55714 -0.807 6.151 -0.30469 7751.7706 31065.0 30940.0 42.5031 47.1875 0.21875 1.217 9.248 -0.34561 7094.0379 27960.0 27960.0 34.3914 61.5789 0.24561 +clone_037 -0.174 -0.394 -0.94667 -0.101 -0.211 0.98571 1.235 8.712 -0.75231 7755.6587 32680.0 32430.0 52.5215 44.9231 0.16923 -2.75 5.536 -0.15079 7646.6332 18450.0 18450.0 41.6652 71.2698 0.19048 +clone_038 0.052 -1.003 -1.06154 -0.883 -0.875 -0.78333 1.235 8.672 -1.00328 7212.0972 7115.0 6990.0 52.0344 68.6885 0.04918 -4.749 4.979 -0.28909 5981.7475 3230.0 2980.0 84.4964 39.0909 0.07273 +clone_039 0.772 -0.508 0.07692 -1.1 -0.22 -0.37143 -0.644 6.536 -0.21538 7903.2681 21095.0 20970.0 86.3723 61.5385 0.13846 0.153 7.496 -0.07091 6219.2042 18240.0 17990.0 12.9418 79.6364 0.10909 +clone_040 0.765 -0.516 -1.35455 -2.106 -0.296 -1.48571 2.176 9.329 0.04839 7120.3122 26720.0 26470.0 43.0758 80.3226 0.16129 -1.741 5.922 -0.36667 6464.3309 6210.0 5960.0 54.6754 59.8246 0.10526 +clone_041 -1.107 -0.236 -0.01176 -1.107 -0.245 -1.54444 1.262 8.73 -0.53731 7915.848 25690.0 25440.0 20.3716 52.3881 0.1791 -5.801 4.407 -0.48793 6804.5711 17085.0 16960.0 34.2655 53.9655 0.15517 +clone_042 4.205 -1.79 -1.67222 0.828 -0.367 -1.05714 6.189 9.634 -0.42113 8101.3601 12615.0 12490.0 42.0958 82.3944 0.08451 3.168 9.526 -0.15263 6791.1208 11460.0 11460.0 61.923 75.2632 0.15789 +clone_043 -0.68 -1.514 -1.73333 0.256 -1.654 -0.24444 3.404 9.173 -1.26 7290.2619 14105.0 13980.0 38.5333 48.6667 0.08333 -2.755 5.581 -0.68551 8280.2407 24075.0 23950.0 19.2232 46.5217 0.17391 +clone_044 -1.098 -0.241 -0.82 1.114 -0.884 -1.31818 -3.802 5.007 -0.02679 6809.7083 10095.0 9970.0 57.1821 59.2857 0.23214 1.25 8.965 -0.54688 7644.6762 23615.0 23490.0 54.4438 50.3125 0.14062 +clone_045 0.107 -0.862 0.48 0.835 -0.351 -2.5 -5.683 5.191 -0.61803 7159.8582 21095.0 20970.0 48.5016 60.6557 0.13115 3.171 9.152 -0.22414 7163.6369 10220.0 9970.0 77.5 70.6897 0.12069 +clone_046 2.33 -1.48 -1.325 -0.007 -1.185 -1.07143 6.234 10.253 -0.63571 6756.9342 4595.0 4470.0 51.8446 48.75 0.125 -4.757 5.07 -0.44219 7332.181 21220.0 20970.0 34.75 64.0625 0.125 +clone_047 0.892 -0.228 -0.52 1.989 -1.155 -1.86364 3.203 9.426 -0.74655 7201.259 23950.0 23950.0 22.269 68.7931 0.17241 -2.808 5.32 0.09385 7491.7526 21095.0 20970.0 58.7769 78.0 0.12308 +clone_048 1.899 -0.211 -1.61667 1.835 -0.351 -0.51667 -1.746 5.898 -0.36111 6408.3071 15470.0 15470.0 66.4981 70.3704 0.12963 -4.759 5.026 -1.12593 6309.772 10095.0 9970.0 26.9296 41.6667 0.12963 +clone_049 -1.111 -0.245 0.37778 -1.109 -0.304 -0.8375 0.2 7.458 -0.57746 8482.5576 26595.0 26470.0 15.6593 48.0282 0.19718 2.262 8.86 -1.4459 7802.767 35075.0 34950.0 55.8721 46.3934 0.18033 diff --git a/software/tests/data/characterization/golden/tcr_ab.stats.json b/software/tests/data/characterization/golden/tcr_ab.stats.json new file mode 100644 index 0000000..f5cbde0 --- /dev/null +++ b/software/tests/data/characterization/golden/tcr_ab.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":10.5,"B":10.0}} \ No newline at end of file diff --git a/software/tests/data/characterization/golden/tcr_gd.properties.tsv b/software/tests/data/characterization/golden/tcr_gd.properties.tsv new file mode 100644 index 0000000..ea84166 --- /dev/null +++ b/software/tests/data/characterization/golden/tcr_gd.properties.tsv @@ -0,0 +1,51 @@ +entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion +clone_000 -1.1 -0.22 -0.07143 -0.883 -0.875 -0.84 0.208 7.561 -0.17692 6304.199 27960.0 27960.0 41.9444 82.5 0.19231 1.335 9.107 -0.41324 7965.2314 13075.0 12950.0 47.2588 43.0882 0.11765 +clone_001 0.262 -1.667 0.29231 1.77 -0.529 -2.66 -3.697 5.629 -0.69254 7902.0572 23740.0 23490.0 37.5284 52.5373 0.1194 2.196 9.005 -0.89808 6198.038 15595.0 15470.0 36.0519 65.5769 0.11538 +clone_002 0.103 -0.909 0.44118 -0.105 -0.219 0.56 5.3 10.394 -0.25 7369.3365 14230.0 13980.0 41.4333 57.7273 0.13636 2.186 9.729 -0.62143 6423.2489 8605.0 8480.0 30.6214 40.1786 0.08929 +clone_003 0.114 -0.845 -0.03333 -0.883 -0.875 -0.54286 0.333 7.725 -0.10175 7001.0225 26470.0 26470.0 70.637 87.193 0.19298 -2.692 5.798 -0.10339 7401.6428 27750.0 27500.0 10.7034 54.4068 0.20339 +clone_004 -0.741 -1.684 -0.68 -0.885 -0.854 -0.85714 -2.725 5.702 -0.40462 7468.3832 11585.0 11460.0 45.8015 76.4615 0.10769 1.232 8.701 -0.81967 7207.1399 14230.0 13980.0 58.9377 40.0 0.11475 +clone_005 -0.112 -0.237 1.15 3.749 -0.579 -1.86875 -3.775 5.042 0.39524 7043.2449 12990.0 12490.0 36.6302 65.0794 0.11111 2.174 8.987 -0.84 8004.0856 15595.0 15470.0 79.0815 36.0 0.18462 +clone_006 -1.895 -0.928 -0.70625 0.712 -0.729 -1.68889 -7.74 4.448 -0.79846 7449.0806 15845.0 15470.0 48.7923 42.0 0.09231 -0.767 6.331 -0.86825 7801.9259 25105.0 24980.0 35.1063 46.3492 0.12698 +clone_007 0.771 -0.499 -1.28889 -2.158 -0.419 -1.27143 -2.763 5.642 -0.84407 7200.1558 16625.0 16500.0 61.9814 34.7458 0.15254 -0.684 6.492 -1.38113 6456.0125 12950.0 12950.0 68.4021 55.283 0.13208 +clone_008 -2.095 -0.271 -2.98333 1.979 -1.142 -0.93125 4.235 9.648 -0.75179 6918.8543 19940.0 19940.0 36.3786 53.9286 0.19643 3.218 9.217 -0.42727 7598.742 15720.0 15470.0 42.8561 53.3333 0.13636 +clone_009 -0.114 -0.253 -0.7 0.989 -1.155 -1.33636 0.213 7.559 -0.37164 8364.4743 54095.0 53970.0 14.7254 50.8955 0.25373 2.224 8.902 -0.05238 7799.0674 38500.0 38500.0 55.0238 88.254 0.22222 +clone_010 0.828 -0.367 0.04545 -0.108 -0.227 0.91667 1.262 8.619 -0.57121 7559.6681 17085.0 16960.0 70.303 59.3939 0.09091 0.218 7.722 -0.39298 6793.7275 22585.0 22460.0 28.0719 59.6491 0.14035 +clone_011 0.835 -0.351 -0.01429 -0.669 -1.489 0.82857 4.173 9.735 -0.73455 6204.0526 16750.0 16500.0 57.8873 35.6364 0.09091 -0.656 6.49 -0.26102 7057.0531 18240.0 17990.0 54.8576 64.2373 0.10169 +clone_012 -0.183 -0.43 -2.725 1.982 -1.172 -0.175 -4.779 4.926 -0.91404 6965.6856 21095.0 20970.0 63.0579 59.8246 0.17544 -1.726 6.013 -0.49841 7955.1656 18700.0 18450.0 49.5 71.2698 0.1746 +clone_013 -0.176 -0.414 -1.875 0.885 -0.244 0.97143 1.212 8.858 -0.12333 7137.1639 17085.0 16960.0 23.9133 79.6667 0.18333 7.291 10.273 -1.21964 7306.2422 26470.0 26470.0 49.0393 38.3929 0.23214 +clone_014 -3.107 -0.284 -0.65625 -0.116 -0.245 1.25714 1.265 8.717 -0.1791 7970.1371 25105.0 24980.0 26.8791 77.0149 0.14925 -1.723 5.975 0.05789 6458.3395 17210.0 16960.0 35.4684 87.193 0.10526 +clone_015 0.828 -0.367 0.31429 1.107 -0.862 0.07692 -3.785 5.19 -0.86727 6369.9083 17085.0 16960.0 14.9584 42.5455 0.16364 1.242 9.115 0.27097 6987.1849 14230.0 13980.0 53.4452 84.6774 0.08065 +clone_016 -0.669 -1.489 -1.26667 0.32 -1.552 -0.96429 1.247 9.196 -0.61346 6060.841 15595.0 15470.0 46.5904 61.9231 0.09615 -5.698 5.136 -0.7597 8226.01 33585.0 33460.0 56.8015 67.0149 0.16418 +clone_017 -1.102 -0.249 1.34 0.831 -0.359 0.85 0.243 7.573 -1.34107 7053.7496 43095.0 42970.0 72.15 34.8214 0.17857 0.173 7.476 0.28036 6610.7448 21345.0 20970.0 5.5821 67.8571 0.19643 +clone_018 0.112 -0.872 0.24375 1.203 -1.768 -0.19167 -0.726 6.396 -0.63333 7903.9302 10220.0 9970.0 63.4591 42.7273 0.13636 -6.715 4.909 -0.60635 7387.1766 8730.0 8480.0 33.9762 50.9524 0.11111 +clone_019 -0.108 -0.227 1.4 1.05 -1.024 -0.2 1.21 8.714 -0.73455 6790.7662 7700.0 7450.0 63.442 40.7273 0.2 1.272 8.671 -1.10159 7561.4173 15720.0 15470.0 42.673 33.9683 0.12698 +clone_020 0.111 -0.854 -0.39 -0.669 -1.489 -0.95 0.183 7.622 0.05179 6664.8666 21470.0 20970.0 37.5375 55.5357 0.14286 -3.717 5.47 -1.00597 7811.595 8480.0 8480.0 64.5716 69.8507 0.0597 +clone_021 -0.885 -0.854 0.95 2.888 -0.236 -2.31429 -2.785 5.48 0.02632 6730.9598 6990.0 6990.0 52.2123 77.0175 0.10526 5.225 9.437 -0.9375 7813.7662 24075.0 23950.0 37.7345 45.7812 0.1875 +clone_022 1.269 -1.65 -1.34706 1.767 -0.499 0.31 4.203 9.566 -0.575 8063.2674 10095.0 9970.0 62.6676 64.7059 0.13235 -1.794 5.769 -0.12154 7853.1248 31315.0 30940.0 23.9246 72.0 0.2 +clone_023 0.333 -1.51 0.125 1.761 -0.525 -0.44286 4.225 9.414 0.07538 7648.0909 10220.0 9970.0 33.7538 88.6154 0.13846 3.141 9.526 -0.36618 8335.6005 39210.0 38960.0 37.7193 51.6176 0.22059 +clone_024 1.11 -0.854 0.47857 -2.164 -0.407 -0.77143 3.181 9.808 -0.46308 7608.6587 19605.0 19480.0 26.6585 55.5385 0.15385 -0.824 6.168 -0.56875 7702.7405 29115.0 28990.0 9.1344 56.25 0.14062 +clone_025 -3.157 -0.428 -1.53 0.114 -0.845 -0.01667 -4.807 4.804 -0.72667 7468.2822 29450.0 29450.0 42.14 81.1667 0.18333 -6.765 4.539 -0.64655 6711.1632 19480.0 19480.0 39.495 32.069 0.2069 +clone_026 1.04 -1.011 -0.52857 -0.101 -0.211 2.18 -4.744 5.183 -0.49848 7859.7073 15470.0 15470.0 66.7712 66.5152 0.15152 1.27 9.112 -0.13333 6982.9853 21095.0 20970.0 26.9493 61.5789 0.21053 +clone_027 -1.677 -1.544 0.46667 1.036 -1.018 -1.175 -2.743 5.635 -0.98451 8340.2204 19605.0 19480.0 63.0507 40.0 0.11268 1.272 8.857 -0.56102 7612.6004 29450.0 29450.0 27.1088 44.5763 0.28814 +clone_028 0.545 -2.115 -0.15 -2.227 -0.556 -0.26667 -3.755 5.344 -0.11613 7539.5417 34615.0 34490.0 0.2774 84.8387 0.19355 -5.756 4.859 -0.01622 8892.2281 34740.0 34490.0 38.7824 100.1351 0.14865 +clone_029 -0.182 -0.401 0.60714 -1.104 -0.228 0.65 0.18 7.52 -0.10656 7051.1177 11585.0 11460.0 55.7836 73.6066 0.14754 -1.814 5.586 0.24516 7311.7042 18240.0 17990.0 44.0774 72.2581 0.12903 +clone_030 -2.095 -0.271 -2.47143 -0.162 -0.38 -0.2375 -3.832 4.575 -0.70179 6444.0944 17085.0 16960.0 51.4696 38.3929 0.14286 0.188 7.619 -0.13793 6540.5065 18115.0 17990.0 64.5621 52.2414 0.12069 +clone_031 -0.883 -0.875 -1.46 -0.667 -1.51 0.36875 0.245 7.661 -0.27931 6855.9442 7700.0 7450.0 31.0431 72.2414 0.12069 -1.728 5.977 0.29275 7904.102 17085.0 16960.0 52.9101 87.3913 0.14493 +clone_032 -0.896 -0.88 0.46364 -0.883 -0.875 0.3 -3.777 4.996 -0.0 7181.2529 18700.0 18450.0 63.5018 53.2258 0.16129 0.268 7.557 -0.77895 7140.1348 37720.0 37470.0 48.3561 34.2105 0.21053 +clone_033 -0.899 -0.888 -0.15833 -6.154 -0.455 -1.52 1.245 8.407 -0.72951 7165.1299 20970.0 20970.0 39.3016 80.0 0.11475 -9.683 4.581 -0.94615 7838.6481 16960.0 16960.0 59.5294 37.5385 0.15385 +clone_034 0.114 -0.845 0.08333 -0.89 -0.892 -2.275 -3.755 5.187 0.23929 6602.4637 19480.0 19480.0 8.3911 64.2857 0.25 0.283 8.1 0.06034 6916.0456 17210.0 16960.0 62.5621 80.6897 0.13793 +clone_035 -1.668 -1.548 0.3 0.327 -1.545 -1.7125 -2.693 5.818 -0.78906 7734.5055 21095.0 20970.0 44.4391 53.2812 0.17188 -5.771 4.619 -0.5 8446.3281 43555.0 43430.0 45.3118 54.2647 0.23529 +clone_036 1.11 -0.854 -0.43 -0.892 -0.91 -0.15294 2.199 9.025 -0.22154 7554.7737 15720.0 15470.0 43.7323 54.0 0.13846 0.366 7.753 -0.42857 7591.787 24325.0 23950.0 63.9571 33.9683 0.15873 +clone_037 0.982 -1.172 -0.5 0.895 -0.22 -0.66 0.178 7.538 0.18281 7723.1366 37595.0 37470.0 43.4205 57.8125 0.20312 3.304 9.808 -0.56964 6863.925 36355.0 35980.0 50.7732 43.5714 0.16071 +clone_038 -0.168 -0.368 -0.6625 -0.108 -0.227 0.72 0.24 7.592 -0.45179 6735.6714 23740.0 23490.0 39.2839 38.3929 0.17857 -5.703 5.012 -0.30339 6938.7046 24075.0 23950.0 64.6356 61.0169 0.18644 +clone_039 -0.123 -0.261 0.45 -0.171 -0.376 0.02778 3.269 10.035 0.25283 6166.3485 21345.0 20970.0 14.3547 62.6415 0.13208 -2.806 5.361 -0.37571 8906.268 49055.0 48930.0 55.3886 65.4286 0.22857 +clone_040 -0.109 -0.228 2.38333 -1.902 -0.905 0.09091 1.333 9.113 -0.33226 7547.6861 21095.0 20970.0 44.6469 56.6129 0.19355 -3.69 5.524 0.38939 7822.9709 13075.0 12950.0 47.5606 88.4848 0.18182 +clone_041 -0.162 -0.38 -0.61429 -1.105 -0.257 -0.87143 -0.787 6.247 0.37143 6109.0174 7240.0 6990.0 48.8518 69.6429 0.125 0.21 7.499 -1.12632 7147.9367 38960.0 38960.0 55.1861 37.5439 0.21053 +clone_042 3.258 -1.637 -1.90833 -0.667 -1.51 -0.82 1.393 9.032 -0.97797 7012.7293 29240.0 28990.0 31.0119 39.661 0.11864 -2.682 5.818 -0.60164 7266.2404 17990.0 17990.0 56.8477 62.1311 0.08197 +clone_043 -2.115 -0.292 -0.88571 3.05 -0.985 -0.32308 -3.833 4.616 -0.75469 8022.8618 40575.0 40450.0 24.4 45.625 0.23438 8.165 10.333 -0.04921 7359.9102 11000.0 11000.0 41.9127 113.1746 0.04762 +clone_044 0.114 -0.845 0.4 0.758 -0.533 0.04545 2.277 9.983 -0.04697 7787.0433 25105.0 24980.0 56.1441 57.5758 0.16667 0.213 7.547 -0.33667 7035.0593 8730.0 8480.0 37.9552 68.1667 0.1 +clone_045 -0.171 -0.376 -0.25556 -1.108 -0.237 -0.77 -1.766 5.906 -0.93103 6788.4664 30480.0 30480.0 35.2621 48.6207 0.13793 -3.742 5.324 -0.83167 7013.6771 22710.0 22460.0 36.8667 34.1667 0.16667 +clone_046 -0.098 -0.241 0.56 0.044 -1.011 0.32667 -3.782 5.016 0.38909 6349.2959 17990.0 17990.0 39.4527 104.3636 0.16364 -3.755 5.2 -0.03692 7342.525 14230.0 13980.0 36.9831 67.5385 0.07692 +clone_047 -1.108 -0.237 -0.10833 1.04 -1.019 -0.71538 -2.625 5.913 -0.56984 7421.3382 22585.0 22460.0 68.5508 40.1587 0.12698 -2.67 5.869 -0.94571 8451.2062 21680.0 21430.0 48.2473 47.4286 0.17143 +clone_048 -0.098 -0.241 0.85556 -0.101 -0.211 1.17857 -7.76 4.478 -0.01636 6368.2503 18115.0 17990.0 61.3273 83.2727 0.09091 -1.603 6.214 -0.21967 7290.3785 16500.0 16500.0 44.1984 71.9672 0.14754 +clone_049 -0.164 -0.359 -0.625 -1.119 -0.291 -0.88571 2.139 9.007 -0.54426 6828.7718 18115.0 17990.0 31.6721 51.1475 0.11475 -1.696 6.06 -0.20556 6350.4914 14355.0 13980.0 114.3852 56.1111 0.11111 diff --git a/software/tests/data/characterization/golden/tcr_gd.stats.json b/software/tests/data/characterization/golden/tcr_gd.stats.json new file mode 100644 index 0000000..fd08116 --- /dev/null +++ b/software/tests/data/characterization/golden/tcr_gd.stats.json @@ -0,0 +1 @@ +{"medianCdr3Length":{"A":10.0,"B":11.0}} \ No newline at end of file diff --git a/software/tests/data/characterization/peptide_input.tsv b/software/tests/data/characterization/peptide_input.tsv new file mode 100644 index 0000000..6ab94ad --- /dev/null +++ b/software/tests/data/characterization/peptide_input.tsv @@ -0,0 +1,58 @@ +entity_key sequence +pep_000 QCKTSPLSNWHTFLFEYKVY +pep_001 LEDMSVENQMYH +pep_002 SRTKCVADPAYSMIMDHWIIFVRDD +pep_003 TSELVLEVMVHYVWLRDY +pep_004 MWILGHGCYKSDDFFCDVPT +pep_005 TIHWQWKRSNDMYESW +pep_006 MHIAKEINGMQCEFICWVYDAEHYWEPD +pep_007 ECYAHGESHCAVQYEKDID +pep_008 LNQGCTRCYEPHKNSWGHCGGMTKEYRG +pep_009 SQWTLNPK +pep_010 VARDMCVKFISN +pep_011 LNWYFLPQDAYHMGIIRPWQCPWQCGR +pep_012 KGRTSVYACS +pep_013 LRCQHVDFAPQMAHAATY +pep_014 HEYHLKGESPD +pep_015 KREKFTNE +pep_016 KACCHKVMNWCY +pep_017 SRQNVGHPWLAFFKMMNDMYCCKGFWLN +pep_018 VFLESICLGTDLPMLQEEVS +pep_019 MMESESQCLMFGWPDDDHICPAE +pep_020 VTLRSWHQDNIKWGQHNEDA +pep_021 RHESPKHCHYFEHRPNVFEYSFWP +pep_022 QTSMSSHVYIAMMMCTFKYFPWLSDDTC +pep_023 IFCLARMGFR +pep_024 TPTTCWDTYDQHLVYQSPY +pep_025 IAAGLTWKMDSKLQPPCGFILMCCSQ +pep_026 SYDFNQCYRPRC +pep_027 SFACYYFMEVN +pep_028 PSECYRYMEYLFPL +pep_029 ETHCPRNHRNDCCSKATWWHIDTTQTLEF +pep_030 WQDEQDEQFARQQASMKDNDE +pep_031 ANNGAINDYFHAHEALNAY +pep_032 FGRESNKFAHNMSLL +pep_033 MGWDEVWLGPFFIMTIIGLNQCFYA +pep_034 DDFQLVQFWQLNDIRNTCPQ +pep_035 QMRHNLSD +pep_036 EKEVYFRPGQQGI +pep_037 MTFNRDSHLARYRAHLELVYFQ +pep_038 SDSIVPKAEKCAKPTWPREKNLHYDCDKLV +pep_039 ETIGDQLLTFWTHVEQVP +pep_040 LRNWFGEEPPWRFVLN +pep_041 SQHSSTMSCRLFSCYHANSPATDDPANC +pep_042 YAKLIFWLHEQ +pep_043 MPGMQQFRFTMFHGRNPQSPIH +pep_044 HWCPCIDGNCGIYLYDTLNQRC +pep_045 TVQWRSKSHMKCCCGNALAFDQIYPVIR +pep_046 MYEYDMMVRMKATC +pep_047 NDHTNHHKLLTPKS +pep_048 ICLVDARSRCQSRREDDIE +pep_049 QHRYDQVPCGIS +pep_empty "" +pep_stop_codon ACDE*FGHI +pep_nonstandard_only BZXJ +pep_single_residue A +pep_homopolymer AAAAAAAAAA +pep_below_floor ACDEFG +pep_no_aromatic ACDEGHIKLMNPQRSTV diff --git a/software/tests/data/characterization/peptide_plan.json b/software/tests/data/characterization/peptide_plan.json new file mode 100644 index 0000000..4324ba0 --- /dev/null +++ b/software/tests/data/characterization/peptide_plan.json @@ -0,0 +1,3 @@ +{ + "mode": "peptide" +} diff --git a/software/tests/data/characterization/sc_dropout_input.tsv b/software/tests/data/characterization/sc_dropout_input.tsv new file mode 100644 index 0000000..d68d102 --- /dev/null +++ b/software/tests/data/characterization/sc_dropout_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 ECCLQNEA ECKNTY NKEDILKWT PTFELC IWFCRDMENM QDKVP QETVNNMN "" "" "" "" "" "" "" +clone_001 DRGISKSRE GMWPHNN SHTKCEC DLTQPEV AAYSAPVGYHCDYS PNKLTCRG DHYTGNES ETDNKEGGLI YCPVENFG WTNQNAGR CLGGFA HVDDRSTEWDW QWGFHWPVHAY RRPNYPDHV +clone_002 MDEMNKG NCYWQI GNKVWWYW TGIAII HRFNPQAWEVDTKK FRLRA FDWHDCC CICRDRK EKGFNPCA HVSTRKHE DLLTQR SHLPTYNVTEIRQQ YLKTKIALHMMSK WIGQVLC +clone_003 TAINQHD GWFTHKT SQHDCVMW ASPYLYI YYGIYYNVGDMVV PQWRNADDRK TSYVSTL QDYYHRCI NMKWPM RFDHVHR IGVQDI RDIMKTAFAGD YCINRQSKGLSRSYHLVT VGYHGKEQN +clone_004 QWWVEQKYGP AKTMGL RYNMVVQYP GFIEVT IDSSALGASVEF LYISNWPLTCC NEGKAKC "" "" "" "" "" "" "" +clone_005 LLGCGGWG YILCRWF MWCVTFQVH YQHNMYL PVYIQMCFGEPSIA YQDYSLGDRNQNMEAT TRRCDAFWD KLFKSSRPNW NTGPDTS SDDIDNKG HQDYPVAH GCDPTCNDWYGD LYVRATYEGAHLEMGNIC PPEEPWTTE +clone_006 TYTRDNI YKTYCAF YHRFWMLL CADIWTEQ MEMKMRAVKW TMMDYFFKMF MERPFTNT SPHGMYVS LDGIMIAI EWTEAMSWA FGSEWMVD ITCCGDHSGGEF YEAWLYLHYHYEF DYEQDGGT +clone_007 IFGLVNY HAMQKLSR PDDFVEIC YEDQNENS PSHTCPRHLSTRNC IAKTGYPQNLE KIQNNRSI GQLDRMFVEK WKGMIR KELMAAWA SMCPACCD LMCMVCMIPF TVVCKDNRTD YRIMIQPKE +clone_008 WRNRVYIYA KRNPKIS KMIGMFQ HDRSQSC VVHACLGLFTFV TNHWTAFSAVAI CWQVDMPDM "" "" "" "" "" "" "" +clone_009 MAVKMHS PPRDCYK AWRIYRINL IYDCCMH HQSCDALFFN YDEMWNDDVHIQFCLA DQWTAQGI CNEVEHMSM FHLVVN KENARVKDL ENRADQGA RSVKGWWCFNDN TLGEMGWF YWFKRLYR +clone_010 GQYPHDDVYG APKIMSGA MHAVQVVA WTHHCT SLCESWCGETTRL CPNNPKPRISIYLVQPGE ACTHYVL WIWGPDL EEQTAG KYMMPYWM WTMGCN SLEFPIMTFAGTMC VPPWVLSGR SIHWGETA +clone_011 IKVYGKCPG QNSDQH LYPSWVR MPRNAW VRMVNKDDFYNEKM FPGCRKIPYHTGVLGRID NHVHSPCAD CGRVTTPA GIFKMF WGEAIKI QGKLKKH WTSEAWNTER YVYNI GSFAHIHQ +clone_012 NVAEHYWC VPLAYQR EWEAWCR KSIDGE SPSEWQLKCHILW WWMRKREQLDKCTINYA RHNKKADN "" "" "" "" "" "" "" +clone_013 ETKHLGEWCC DTWIEMP IWPCAANAP CQGIAHEE MMWLDPGAIQ MLHYHPEWYW LSDSMIWNI IDAGMYV TGSGNT NFYDNWW FTKHMPH QNAGYQGCHC RGCWTNNC WFTKASMIN +clone_014 TSANLHG YGLKGG YRDPWGMR IMPTICPL RETLVLRVMVLK HMASRNTHGM STWSMHACI PPEEGMKEGD APQVHECN ADAHEYEYR HHRVMCV KCCARWMSTLC WYKRNHRSCDTGADDW THVGMAYEI +clone_015 MPFTNQR NAACCDV EGGFVSP FHLDDTE EVQVKNCTTRI TTRMFIAYAQSGR HWGDDRMN IINQCYPAYD GQQGRHGW FQWKEES TEAQFGI RWIFYWPLMFRYI GVHDVGCPSLIFK DVKPSEDA +clone_016 WNVMCQY HTHAAYIC YPWIHAFE IPLTDAA APVVTAWLWL IFTMHAFIIMLLYY VPAENWH "" "" "" "" "" "" "" +clone_017 DDKFLVECKD ISIFYN MRISITS THAAAV CIRLRHYHNLP RAATIPAWYWGVCGLF WDQWVQTPG SIFCYNS PCPCRV QCAKEPQ YQCEGQD YRCPWNYCPK FIKTCWAA MAAQPRQ +clone_018 WMKGLKTHLV SKEEII CEMCPLLRL AWHGVA QGSTMPPATCKQAH FCNLWTKIHGPLNCTTF WRGFFPT YWQGFMQDHG CGQQVM PSSIEAMRG PHRMPYH TCPPWQMVEPTAAP DDQCVHV HNRRPSM +clone_019 YMCIDGPPS CEGKTN FHLCKNN GERWEHG ESNPTYCKFEEN SPKPIPYHA EENWIHV MFKDVHCQ QCCPGEPI MPSLVYTSH LIAKCIC ANAAEYRFKGHQTV DLKYKWHAWYTDPPVSFW ACQGPYR +clone_020 TAPGATFRL KMESVRSK NKCNNSLT ISFDKTY GSCHQKKYMP DNSSLR VHTTGHMC "" "" "" "" "" "" "" +clone_021 AHVEAVKW VSDWCYFS WGQMWEMKN ALSPKQYD YNCLDHVAIIIN QCPCMVQYF CFMNAETTE HFEWIFT QIFYMLK SVEENNGGQ HMRDWM PLFKPADVATSE AYSWNDFKPFTL DGGMGMSR +clone_022 TDDGEECPE ETLHYN MMHILKWV WCTSTGM VLMPEAMVVGPS EKLVWL TMAHHGICM KSMVIGM GWIHDI QFCRTIY VPFHVR HVRMHFGVIDKMRN KECEIDDKYYSNMID NGYIRHAG +clone_023 WAWHTLGL TRHVYI CNTVKGQH LDDLYWE MAARCSCLVKFMHC QDHKVLIDWVF GRIWCQEAC NGMTYHQI HLSYCS HYPQDDV CAQGCTY MSMFSQADFVMFY HSDNARPYRRHPWIGCME KCDGASH +clone_024 PSLVIPDMC STATTME ISNYYYQ AGGVPH WGVRDSSLIASKEF FGDSRLLAYWPHCEV PIIWDDD "" "" "" "" "" "" "" +clone_025 LYTASNI AGQHYNGN RWRCINDGD AMIMIAR KSVLMCYCVHKKMM DHAMHWCNAQMSHD DVKFVHQ DSKSFQQC RCIFTS IPTWHTPI TMGAPGI VDLWVDKASRKW PQGCIWQHLCCMNF KGHKKRMQQ +clone_026 GVNCYDENH SVRDDYWH MDVFKDRNA CHSFTHER GDIRYLWMYYRDW EHEEYTANQWG RARYNQM PKLSWMW TNHQNKD MHFRTTDWK WDMCRL ITVNKRHSEQV LGTFKKGDVK QYNIDIC +clone_027 YCNRMHW ENQARGRY QNQFGNQC ICHAFFHE HVSHIYYDIEGERC DNGKKYQVEPENEMN QLGKYCRT IDESYMS SPTFIRPY WNGFPICG KIQPFCMG VHNNKGKECWGYE GNMNDRAFYWSMVIQGSQ PMGTELNH +clone_028 HEHAAVK VQESDEQA RRGELHQR DCVQYRLV WLWDPFLAESD TKKALTGKMGACGNHAP YKQKAALG "" "" "" "" "" "" "" +clone_029 DTQCEYPSV CPYTKI PIAVNGT HWAHYAD DQDREMSWQWRMWR WGVHTIGEYET ASVAQKRVI SHFQMNCVP IKRGQLGI LTHRRADAD SMAWHN CRHCFEGWDF LWMEFK IKKWLHAFL +clone_030 GTVNLWM PRHVEAHY YYWMIVE MHRYWS HSMEDVEPFREEI PHYQCIGFNGEY EMFMWLTI LQASANSVFP PQCEMDLA DVWTCPVG IPLTLQ QSFVNTNKMVTQ IHADPCWVLS REFKYNHI +clone_031 TPCLEQSP RLCKWD VLNEASIM RVNFRQEV GMWFMQCIQCGPII TASNYPHRASLSWGNIFS TWGYQYM EKLWSTIF YLESWMFN QWTVCDM GLMGWEN TQFKAKAVQYHGV YMAAMQYCA WRFVTCG +clone_032 VHSIFDNCV RLKYQY LHGMKSRI SIHQLIL ELRVGDRGSHRA PYGVNNT HWVQIYILT "" "" "" "" "" "" "" +clone_033 KVHFCYID YLVKGWEP PNWCVYFM MVKLTWLI GFIQPEVGDMSCME PWQELCHWS RWIETFSHV GTRRAVEQ ACCNQK WHNVWLQHL IGGKSH YFDYKADFSYG PAITDWN FMQTACKC +clone_034 AGWKAWS ALTCAEQF TMIDWVK IWEDVPLQ NAPDLQFLPALT SGNNGGLFMCSDPPD TVGATAQT QMSDARDH LICRGGMF IFLHDTIC VEMTVKV WQWKERRRMM PELWFSD QNAALDWDE +clone_035 KNLIMKEEMK CLKQFKQ VNANYPLKA NMDPSH QKTWTNHYIW TDYEEEYLGPVAH STPWCIG CVWMNAFD MGGDDWMR ATYLYTPGT MNPVTPRW DWLSQCQGSTIAV WEEVCICNEIPRVSG ADGDYHE +clone_036 QDWPDKTRHP KVHWCVQ TWRIPLYGL YDYMWRQC EWSILWLYNTKWN IGQRNMVHPNSKGLSFWD NSKAMRH "" "" "" "" "" "" "" +clone_037 RINYPPF YIDTQSP NLGQIPN KQVPWWRA HITHNFHTRYWLCV YAHSFAIPRNRADGCG FKPENDQ GMACIKYM WELLCFY LICDVNNFF TMSQLGNY YTHTYVIKTFE IPLNVWQDCWASQWKFK MSHAKLGT +clone_038 RVNHQMPH LIYIKPQG PQNNDYK KFAGTMMG YYRFRKQWLQ NAHEKGYP TGASHHDP RLKYITVF WSCHVKQ PKPRTSYS RMPPSID YNHIREVYYHEN QIWFKTK LITHVCW +clone_039 RIMWCAFPFR GDYYANFM HKCMYVTN LHFHRNPS KINVQTTNVMGK FFQRP KVVSHQHG PEDNMSGM WMCHAIE PMSHGSCWD YYYACD PKTPYEMFVT YCVYGTYK QHFLMTILK +clone_040 MYMTCIL QQNVCSAD QFIPLIS NKLLWGP LHFQNCNREFF FHNCAGACYWDPWN EVIPHVHL "" "" "" "" "" "" "" +clone_041 RLECAYMCNM LRKIMM CKFVSVE RKWNTP KLQMDEIHPGAW AYKQSIPAKPWIQTDCR WYIDGKD LDQYFQN EAFTWI QSTEKLK KDQFATE IFQAQHTGWR STIVK EYDRHQSD +clone_042 SPYKKLNRI EWDRHH KEGWCINQC ACDKWRC DGQKSNHLPDI SFDDDWNNNISDIFDIFL SKMTLQLD VRHLSQYC RQFMRSK HDFCQKWIQ HISPYW ACHHEKWFKTKF GYMMGNSYKWYNLL TEMWCYGKV +clone_043 YKPAYFC ILEIHG PYWLVPGN NYFLSS YEHPGFDTIS YIIVKVWFMVKDMS AFQRNPYCY GLQSGLVKN TFMKEHP QTNHHRV TGTARYL FYYILHEIMEWL GSPFDERIEGTAYLDWED GNLHFFLI +clone_044 EGFALVLG FEIITN MVSWLVTD HSNFVS KGGCVFDVCHMLW GHSIKPEW TPNFAYGH "" "" "" "" "" "" "" +clone_045 LTKPSAPR THAQCSSN ASITEYNVV EQNDNM PRKAEVQWTEAMY KFWYMEGSLM HYGLDASH SNVESWGAP SVLDCK QFAQILM THGTGC DNPFYRLALNF WNHPDQEPYEVGFFT GNQPNHSG +clone_046 AGKTFIMN YKLFFWTT IIEHDKK SKVSQI WYDEGWLCNHNFN DLVGSYPETVYKWP HYVKQGG VFFEDAH IRMENH FRQHCRMSC QWFGCGR SQVERRSKYLE VVFPVDDLNVNN GYYWDKF +clone_047 QSKHLHRL EAYSIE FKTAGKF PAAAPS TMWLLNTFEYGRME QSVVNVNQS YDYPFNL NHGYGWSCE RLYYEHE KFRFRSEMN PVGWTG FIQKYKGIICVPLK MHTPTWDQKICPPQE HRARAYADY +clone_048 DKFVLETV CWFMWPP LHQLFPI WKHISS WYIVSKYYHIY RRQGLMTT SEWWYEFLQ "" "" "" "" "" "" "" +clone_049 SKCDQQGNQK KTACAKD GGGNMMTTQ WIYYLEY IALFQNECPG KCGWPLIMDVHCCAE RTSSRCRR WHTPPPKCKW MTAKQF WKRQWQL TSNVYWHM ASEFCTSLKG LCWWMPK GVEGDTVPM diff --git a/software/tests/data/characterization/sc_dropout_plan.json b/software/tests/data/characterization/sc_dropout_plan.json new file mode 100644 index 0000000..f4b6d69 --- /dev/null +++ b/software/tests/data/characterization/sc_dropout_plan.json @@ -0,0 +1,13 @@ +{ + "mode": "antibody_tcr_legacy_sc", + "receptor": "IG", + "chains": [ + "A", + "B" + ], + "fullChains": [ + "A", + "B" + ], + "hasFv": true +} diff --git a/software/tests/data/characterization/tcr_ab_input.tsv b/software/tests/data/characterization/tcr_ab_input.tsv new file mode 100644 index 0000000..a194c94 --- /dev/null +++ b/software/tests/data/characterization/tcr_ab_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 YFSICLCATN IDTVQP MIAQMGHS RPIYIHR DHEVHMESANAFIP DHPKYHPLYKTHHHMRQ GEEATCN TQHYPDG PSGKEVDG DDCARVPKV QRKCERQA KFRNWLEKKMSA HFPCQLICHNRF MKSQNKSWD +clone_001 AQWEPVN GIGFDNWF NSSHFQN HEKRMGEL GWITSTPERGRAR FGEVFE DMTVENPQ FHNRGHYSVD VAINPPI QKSNGAVMN KNLDHCR YACEQEKMMMV WANLIGFATQ QTYTFFSDF +clone_002 AFDATQW PRTVCWFD HPLTHFP YPGMHML RKYMHSNYHGMAA ESIPENQRDQTA ENRFTIDY NYHCWFRC GSELEFG NSTYYSYPS VFIFMCY QNGNLFDVVWM SKERNVTVVYYP AVQYQYSH +clone_003 EAGEISQGT ACWHRHCW AFCDCIMFM CKYLFHI YTHCSNPKMDQ VIFRYDDSNKPTSGP FPRILENV TPMILTFC TMGFDN LAQNVVC VMCAKC VFHSHQQAVR YEIQKMHTHLKWK RTTNLGVEW +clone_004 QMKQYSQ ANSLNYP RTGMDNFM ERKHLEVD PAQWSGCLQISW CILCHRCCVRH KWKEYEAY DVPVRYAKT GCSDIP NRPWENDNI YCPPPWTG WHTPSQRIFQCP WVGHNELQDL WHQCTRP +clone_005 WCTHVKGRS HLWAHHS TISCSFCH EMSWTMW LRFKAGKWNRWY FMPTLTPVQKKLFW CNMNLTS GRKAGAMP RLEPGY VDMYSEDT NGTSCTN WEGAAAMMTHTF RTNYATRLYQRA ATVRVGDQ +clone_006 PDFGWHKTG DNYKTMT PYTKEECT LGKVFNMW SEFTIYIETPP PFWQVQCYHY NLMRWAV FQTHMQK MPAPVLRW SHCMAHQ MGDCCPMV PGKYYMMQLEYY FYHHMTTWY LQPSHAETV +clone_007 PSPWQNCWT ALFFCP WSYIICNKV DAWTMCV HVLHTEAPEVN FLAMAEEC HILWYGSDY WIPWKITHLI YSLTELG TYKGWCRKY ESTSAG NAPAADYNGGWQC TACGDC QKCWWFCG +clone_008 VQKPKCE FVQTGI LHNYTNYAP CHFRVNH WLLTYQMSVTNEDV GMLSWRKA SDGYSFPW REIGQRDCC MMCHYRI WMKRWPCSM DCLRAPSI YIQCKHKQSK FSTITEKA HHISFFCSK +clone_009 HQWHHEEMN CSICTHTG YGCVHKWD TYDQNWH VTKILRKKYCGASY PVPGIW PERGMWA RLPVTFW MYYVVVIL ENEMSFHSP PWIENPA DIMHLTTMERLEMA YPCPKS GSWWAFFP +clone_010 EQHDMQLM VRAANVD ECFGLHKDL MIREEIEN KPADDFNCRCA ENLANHDPQMFEPYI WDKAENN KFWPMRRQ CRNCLRQ VCESNVN VPTPLPY RSDIWSAQNLM FIKMGAKYAPTQNCLNSL EFSCHERVA +clone_011 YNMWSFKY WGCNNKT FIIFAYI ILSDENV WDYYYKCVKYDFQ HYVTY ETKASLKH WSQWGVHEET MPLTQRF CWGTNSHAR NMVWFC EQITSPGVSKDCR QKLFFPEVHWAA FSMLATSGG +clone_012 EETLWQQLHD YWHKSAGG TSKSHHW LAPIFGET CNHKEPAPMEM PWDMDMMLMRKC YDQYYQTM YLGYIVQHGR PHKMFGFC TDTNHWQ VSQKCM KWIVDTPTKEH NDGGPEWVQRFAG QISDKNC +clone_013 IEHMKFS CDFFPCYW GSRFSLHRY GIQCVK HIKIQDGFKGW VHLVRLYLMT DMYPIFEV TWMYEGGIYD NGVDYN MMCGDRAA WMDGCQW GWICSHNIQKAIF FDAWLHWKKMNPM KKTYQMPCG +clone_014 CDLGIKD PDYHFW WMKMLICK CDKWEITK DGMSMKKPREQTRG WNAKDF HKICRKHE TMTRGPSEKW GRPYPMI KRESFYS AWMTCNFG FAEVNKGLACGNRK CESKC HNKCRGGI +clone_015 GMDRMGPMIR DVCIES IFVLTTWG GYILLCNV FAFFWCLVNQT NPNIRNLFCNSWGAHTS VGPRMDD WVLSGEL CTYFYEAK MDFNYHP YEEDCGF LSGQGLHQGCQMG CSEDHNYPQAY GRPEDWL +clone_016 WLNGQLWCL NIYDNQT NHFDHESSF IMFWVTR CPTRPLDHGTPR FTWWCGW SENDINF SYQCVNG PWDTQK NDFPSLG EPCSQIP VFCPKFLMWQTR TRWNGMFQDH CWGKFQHV +clone_017 YQMEHKEY THHACHQ GKWFQNHN KTMPIPI FRLTVHMIKIMRYM YPCKMRTDQLVSYN GWYWRLYE MKPTISI PVKSPH VEQIKALPQ ETPVMTM RMRHGYAVSPKLDE WKKNVCLA TNPHSGG +clone_018 TDLEGRAPR NANEPHW KRRCSALY RLILGC QQGRVFCYSW MDMVH WYNLDTI WAMCLDKM YCWEQR HFSHLTHQR WFSEWPPT TPEHKEKFIIKLTY TSTQYAW RIFHLQR +clone_019 RFLWPCMYF DPAENW RGWHGSAR INEAAHI CYAHKALEQF TRFWHLMGCFMPTMHF YGAPQHHGH HVRYMPCWKK VEESWRGC HHKMLIC QSGWHAKW ENEPRWLINME RSMDGTIRMNGMP EIVQGMQ +clone_020 SSYGYMW DAGRVNSG TWIYHYHMK NTYCARI AIIWERYAIS SCCQFEYVSEFFMWSH RYGDFKLWY VDGHHWIKA YAPIDRN VYSPRKA HISNLAL RFECDHSGATWP GPWTIYH WLTAKLNID +clone_021 QNRDCVR MPFGHQSY FFCGCFM TILIPFVF CSKRNDFSSVE SHQVWGE IDKRNCPQK QCFMTEMLMR YFINNTLR ADTSNDQH LIVGMDS ELYHGRCNPQIY DNRHTKYKRCGNKNR NGKWCQYA +clone_022 YCAPYHC KMPSSI WEWYMIAWV IGSFSD IQPMAHNTCF WGAEQNVCK CMTKIWV EMLEHQAYH QWNCDEQ CSWGICEFY YYFHFMC LERMPMQPNIGIS DKQDYAMQWMAERYIGLV PNSIIKS +clone_023 FEVAIFL KTWMTQ QYRGYIENY CQSERL VLQWSLCAGRNWI MKAKSQIPFFFEVRMWGP QLMVRLC RSAYDTIPK DWEVGW QDYYFPSEY YAEHGV FPVLLKEKTEAD FPTSV PWGPKHVA +clone_024 RVEITYRY TIAKLV WFCYPLY DAFGRFV NKPIVGIRPIVYD TIWWT EGFCIHEK SYCDRLR VFACNAPG VNKHNNFN QNGPKQI TVPDEMDIAWIMT WRQWWNYSYKP CKSGYLH +clone_025 WYPGGESI FMHYRAY PEKCTAE HFGCIC WDQIGSGGALQQQF TVACR EEVDSFGQ RTYYGMR FIEWYL HKCGKVG SHIGDGY MVEIYWKQATCYK MFYIYLKYFHAEW PILEETN +clone_026 KMMATGD SGEMKMHT AREKSHET VLGENET GFMSSGQLIA LFHDVMCEGTC IIPNMCQ WYNTHLPVH GQFMVRA GDVNLGH SATELW YSTRQAAYCVI IMTTIQHPWNCGEIWYMA SCHDTSMRF +clone_027 EKVIIPVW FLDQPNYC HKFTHYD DCCHEHHF CYWKDVKHMQGLFD QIAKSGFTSETEC YTMMYLEFA PSGRIPGIY LWNITA DPNNPHFWH SHYDKCQF EQQGNNLMPTRKW DVGTWKGA DYLWTSLP +clone_028 DNMPLMVDCK RTQCHR GPMRLCMGW WEDIARK HHHHQCKVRLL GCGKPVSL MKHCFMWE GNARGAPWEN GNYYTDWL MEVYDQVTE GFVNDGN IDVGQQLRNELMIG EKCLPWHVH MNEWTNHWT +clone_029 LFPNHGKA MYFEFL KVIMNGV EWNCNIV YCEHYWRRLSDAH CRPPMV MFCCRFY CYLRRFIL ETRMHCIN RCEEGLLIP VPHWLSI DMVDYHGGHANPL REDHPME GKIHQYPLP +clone_030 REYNSIYN ARDWVYKY ECVPNLNE DMCHLKYC CADEFEQCTLSC KQNSGFAFNG LAWLWPEM DHCCFARTYG FVALKE FCWIQCYY LRRESPDQ AASQTRLWNYKT RTGPIEFPAPPGEDEIE LPCYMMV +clone_031 MPNHFFT WQPMFFRN VNKTAHYTA QPDSPC AWYNFNRFYW KSETNEMGYIFPCLE IIVRVFTA CHKIHVF ADCHNAHR FPNLLYNA IYDNQE FFGHWYAKDCYG LKPNDFHCGHFC YWNWTMH +clone_032 EAFEVEE SHVTEFV MYWIFQCS ESKTKN MFYYQGCVEVSNR LCAIGEQQADWADVFFM KLPLVYMNF MEMLCSWTM SFFVAYG NVMAHMD RYVNVYS MLHDWSKDKTDAP HNPFKRDIETAY YYKFSMA +clone_033 QYRRDWVH FNNRSQ DRWEPIFC WMEKWH NPEDTLVGTVW VHTYCIAY LTGEDDH KSPLFCNK QQLKQDQ TIKEGHAWY KPVLAYV RLKIWDKDHYQD NAFACKQQQTCI VDEWYPKV +clone_034 VWHANLW YYFWNQH FQWEYKNIN VHHQRY KIWVNPCHCYTHP TSKDG VVEVWSTH HPVFMVRCF RPRHCTID MWFNKGVR HQSRIA HPIGDCRQMTDSM VDGQTE ESIVCLERH +clone_035 SMAQRWSH HYWMNG NWYLLYFWS AWNIRK NMCPPKDECATCGP CSSQIMP SNCGAQNHA QAYWIKC SQECTRKY CSIMINHPP WPEQWE EFCGSYTSNDKNQC ALNAMGRPQD HKVRGCRMF +clone_036 PDMSFIMA EIGIEYW WHCLVYF PPNVWSSN CYRASQAGSY EKYFGFNMNVMTDWS YNTRCVMRR VAIISTNH FTRWYVKM GDPIQGS FENFADWW RFGRMCNQHYYW FYLFLDR LPPTRTL +clone_037 VVPAYCTNP YSAYYEKC IHHSWYSK NWWNKV SESDSAMMNRH RGASCYNDGVDKQPV CQYVMLRW FNIMYAHDVM NQNAWV ALNKYSDI NVTTFERD YINLFYYARD TMIQLAAFNMSFMW HMGRVDH +clone_038 RDKQEGR YMNRIRVV NCWKQCG CDIQVPID HAFTAGLLQQ NDLQHDTRMLSLK KHSDESRQ PVIMALT FFEVAKM YASHTGSD SCMGDHYP MSPTQNKPTQ NAAEHM DCVECPGCT +clone_039 VTTAHTDCTH YHAMHI MYMNDMGM PRRCLY CHFIIDIWKATEE DMRFWKMLVLKFD HWGKIMHGG KNGGKKG LAPGITTG LYVGVRLC RCQWMQL NCEVMALFEW PWDNFGI VVCEDRSQ +clone_040 LILQWSGL ECLHCLA RYCAVIGN MGPGRDC IYGVFTIWIMWRF AQKKDYAFAQN KQPGPPWG PPSIVVGSAF LGHHHTF ICLNYEKG EYPRYC GRSRPMGICT ENTGYCE IMPDCNIER +clone_041 TVWAFGSGL GFDKPT KSLYTPSHN FCHASK IGQCDHYHETRNY SVNLDISLTWQNYWMQI KYCCCYR MQGNGFVL DLLMDCC KTEMFNAA YIENHNG TYEIMDYPTFA MDAYDGWNR AGWIRNER +clone_042 QSVTIWGIPL MWALND LVGYACDAK RFGPAV KETNCEVAKPIFK ENAFKAHRNPVRRPNKVH RQTNKLNIV FLPNPKFDM SLVPIYPP CIPLMLL QMKPQL TPRLLYHKSWDR GFYTKQG QPMYMFAT +clone_043 LKLKKSA CWCLYH RHPEKPTLG MIVRVDN QWHHHMKTNHEP HYTQQVHNCDQP RKGPHDF SRIPADVGFV KFWWVH EMLKQNSM MRPYYQ YNGEGSQDFCGP HFTAQCKLYHVMIQTPDS QDEYVTQWQ +clone_044 FEQIPLTL FQFMFES QEKLRLI GRMFFDV NEYEYAKYACMFNC AESSS FFTWSSHL RVITAWDTG PSTTENM YQFMEDMC LSRWHVEK GVGALQHAFAMI RNQVFRHGECW KPRRTAFWQ +clone_045 VKYDNYRDSE GCWMGWVF ACVEIHQHR AEHECEW PQTAHLVSDP GQTSHVIVYA DLNKFQP KKCTNCTE MIAEEFIL WYLKRMNID KMLMACVY IRRLLEMRCMHA NNNMK FKYFGEMN +clone_046 PTFRLDG MPKSCEP RFRICQKN MKQPMF VVSYAGPASTQYR HRQRHMMI PLIFQHY NDTYCQAI PTAKHDM QCLFPGH LVIYTVPE LTDGSVDARKLCC LAKAPQEFHKSEWG SESWYDW +clone_047 YIWVFNVQN KQHPNMFN DMGVKAQG PIDRIP RMCRYIPYIKQNKT RGWIY EWHNVLEY APGEMWCVLP RMAQEWEL IYLQLGLS SSPVFCMM SCVVWMMVAEKFD KGQKQRIHGAE YSAYSEL +clone_048 HSAYQLS PFVVHS YEDKLHL PVLLQN PGSIPMPFESQMR RAQRMW VCWDEYASM HFSYKKAD QFSCDED TQMDAHE QLYKGS QTNIHQDDGYL PKGIRF VEAQCANWN +clone_049 INSYFFKL LHPFGYN CDKNTHRVV DPFMQWI NPDWAEWQTFSKPR GIMMFNDSCSTTWFYLGG GQKIPENK LREDPWHKRT WKPDHLQ CPGHEPKAD LYTKPR MARHIEYGDCFW YEWEYLIR RVYWNKKNN diff --git a/software/tests/data/characterization/tcr_ab_plan.json b/software/tests/data/characterization/tcr_ab_plan.json new file mode 100644 index 0000000..3649462 --- /dev/null +++ b/software/tests/data/characterization/tcr_ab_plan.json @@ -0,0 +1,13 @@ +{ + "mode": "antibody_tcr_legacy_bulk", + "receptor": "TCRAB", + "chains": [ + "A", + "B" + ], + "fullChains": [ + "A", + "B" + ], + "hasFv": false +} diff --git a/software/tests/data/characterization/tcr_gd_input.tsv b/software/tests/data/characterization/tcr_gd_input.tsv new file mode 100644 index 0000000..a35af34 --- /dev/null +++ b/software/tests/data/characterization/tcr_gd_input.tsv @@ -0,0 +1,51 @@ +entity_key A_FR1 A_CDR1 A_FR2 A_CDR2 A_FR3 A_CDR3 A_FR4 B_FR1 B_CDR1 B_FR2 B_CDR2 B_FR3 B_CDR3 B_FR4 +clone_000 QNMAVAY ELKHYL WICFYFQ GRNKPM YTHNVPIIRDVW DWSITAW KAVNADV AFSMYPH NQKCSMMR MCLPRMKM VEGPYQQ IYHNYVIHQAHLSH TTHMMQAASNWEQPM SAGGFPVYC +clone_001 MLEEWEKAT QPEYDLNE RPHTQGPF PLKKCPIC GPPIEHAKSHEWF MCWKCELAGHMIH DIWAKFGP YVPQCIH VNPPDK AVPNKCQ SLAKTD QLRNDFDLANLPYR KCRKE WVYWHTP +clone_002 PVCAFSGRFN QHLGHN QRTAGSYV LGRPFK AAPKHNSQFRV IWSEAYPGAFWHCVICR CSPATIGH DSKNCVGAM VTCEYDMA QMINPAH NFNRRNS MWYNTRGRSTI CQAFS INRAGNMNM +clone_003 RVIAAVYVSP DHKHLD NNPFYMFD LHKFILIC IPAWRWWEIY TAVHTP HQFPHSVRW DFVVPLI WTNSKQMK CQVHHETWP FCQFCK EMMIDAHFMRLLGC PWVPFHE WWMEFFNH +clone_004 YWVNGANRI DYVQIG VLPDREIGN DLPRHSI MFDEYAIGTM DFHCKLYSPAVHEQS IHKQGCKSC GRQAAVCWHD KLPKMVN SRNNSTWE RIAYDNS MPMDCNCKFHKMN DTALTHN CPYDPFRVF +clone_005 IFEIHLTC DASKEWGC NCDPGHVT VSMQEQP CMMMVQGWLLAYSL GMAGCACQFC ARCFQIFT QMRSGKCTTD PGIHFM QQPFQAFF FEIVEWKI FQDQVEMPNK ETYNYKTKTFCYRRRW NQPNIAC +clone_006 SFPLWICT VQHDTM QNRLMYDD TENDTKY SPYHDNGVECT WDNCLGARDECHCQCG NAERCGVAS MTNWAMRMM VCHDDL IQMHKWR GQTHDG FLIDFKTSYW MVKNRWNMQEVENKKEQC QNDYGCL +clone_007 MEFGCEK KSCPHISD WWHHNIQ ESFLQMDA LEFREFTEGCFTK ITKMNPKDW FDSKPMR QDADQHI SSDRKQ PQIYFYWKY HNNRIG HDAKHAVKYKQ SEEADIK LYCHDHV +clone_008 EMVRFIV TYPYFRS WPRSSHKH RYTKQKHI GIAYWYAMTKDMY EGENNN GTRIVIF KHPACLA TSLCQCK TRGFPFK WHKNICAT MMMYFWLPSDINS YGTQFHIKLKLNSQNP DDYTATPG +clone_009 MQVKERAW SQKERGWQ AAHFHLW NFQGSVV MVWCQWSFCYPVD DVWANWYSYSFWWPRPA TEKLIFM DWWIHAFALK KIAAGPG MWFFLAW RWFTLLQ KVVLPVEVFFID TFKSHKWPQIE KRDDWHINR +clone_010 NYTYHICSA ALIRNPPA KMLHRETHM PSMSNAG PLPTMAEKHIDCVW NMANAAIYKAW YDTEKKQP RTDYKTDRR DGLHPWSI SETYCVRLW RMCYWG HNQDGTIMMQTV VGVGSY MTFCIQV +clone_011 CNGAQMMS EGKAALKR CNAFEASC PWVRTQN WNKNSMPGRS PSFITGK WRCDHLT LTDVPWMQHE HGTSTLS WVSRTCRVQ MNLPRMQ DCTACHFRWDRV DHVIHFV VCMHASY +clone_012 GWDKKSN LLFEQL SSCNHGRML ELFPDQQL DPCDQVGRPGW QQNKYEYY HIEEFWFL YPEEPRY KYQKFHI ENRDCMW DADMQDFC FCEIILAHDV MLKVPWERHKYIQIFA IRYRHCAI +clone_013 LKCLKFCLA LTVRQEQH HLWDTGNG EVLSITA APYRCTQINYG FEYYKSNN LGWFFIFMR YHMQHAW HRQAWKR KFRLPVR TEPKPNRN SNTFWNNMEWKF FFRLIYY NHHIFNAN +clone_014 WHKLAGM TGFPRVH VQVVLCKKY IRNEFNHW RHKLLTTTCIVDI ENQIWMDGTFCFQDTY WASQAGV NHNSSYW TCLCKI GAAYDITNY TTNVVH IKLVIMTCPHWIIH ICYGICQ NSPGEETS +clone_015 GKHGTGQ DETRGEYG IKVEKDC AFTEMLGD RYHQWGSVNY FYKSWAI SSCFQDEL CVICGLIKCL QYHGEM GMRSNCWQN VWAAML AHTDFKMNTS QSQVYHRPVVPVV VITMSSVS +clone_016 DETMHYNA IDVKPD YTAAPTL SRGRPVT CLGIWRTRVAR HTLSHD CTMRPYW ERWIEGWL VEDPGEVI PYWIQSN WADSRCRV QRETDHAMHCAER YSHRRFLWDEHLYF GADLANIYT +clone_017 RWRYHGWDQ AGSCWDH WIRQNWL KSEATRQV YDRDSPECAKMQ ETVVC NPYWHQKW PMCLTQYF GQLRVMV DTCAKDNIT FLKWCST ISHFEAWGYP CFKAWF VLDTVKCYC +clone_018 TQQCHAGTV QLFASV WDVKGRMP KYEFKR NMHHCCPYRQYDTF MSVRHVSCLATRVFDD EFDMEQP SGMFQKGNDF WPDQNTC FAEECQITR VSDFCDR VTGYYLETHET HHNKTDIIVIMK ECVMHEG +clone_019 CVRAECHYNK LIGCFYM KYHPDNRQ EQRPNSL CNNQFMYKFN PMVLY GFFEPEFK YQVGVGKQG YPDQTCRE EYESHFDTF WCDIHR AKHSNMPCKVR IHVRCAFEPSKT GMRWNERN +clone_020 TFMRIPLV CQYQSQ CMISWVI FIPSWCCW KQCDMQVKCC CGNTSNVMGH MPQYENY HAQIPKM MIFPRWD DILPYDAL RGQRLNGQ EKYNVGESDMQHQV QHQQLPSNIDLQHVAN RTTEESG +clone_021 RDFWHNKMN QGFTLT GLGLMET ELVMEFK MFRISCIAMSKME MIHDVA YLIKAEPMD YGNWSKMCRI ISSKHWE KKAVGWFYA HCIFLFKY YQAEELNNATDKSS RYCNRGR NVKSEEFHR +clone_022 FCAKNDAR LLYKRLE SLMLGAAQR CGRYRF PQSYFDLFLMCTK HKRHQLLPPQFTQSEAP APWVRDTD YHLIQQFP ITSYCWDC GREDLKIM NYPDPLYP PCCAWPYFHTLEWI CWPAMLKSKV KNCLVDGYF +clone_023 KSLMYPTKCI IALILS KLYWHTE FKFPMRL AKFKCTYICLSN HHFQFPPERTIIPAII RGEDGCS YIYQVVWMWQ QAQINF CMQAWFPPW SSNNLFKV CARWTQMPESTWLP CIGPRFDQKFYKIT GNTCQYK +clone_024 RYFQPDGM AWTRINNN QARSVNTP CWGEIG LQYSPDFKFKLLNS MCFGVNHSPRIPIM TRPWFTA NVWFPDGDMK TKWATRK IWWSETIV EMVEKMC RVNVMFRYQG DANDGEKIITWTQC TTSLFNHQ +clone_025 SVNALNWK WRPWWPEP EFPISLY NLDQMIS VRVHYITYEKFE DTKSLELDQE SRYLINYD ESRWQFNIW GPHFDDYG ATSECDAM VFYVIQ AAESGNGETPFQT GAGFHT FKNFEWER +clone_026 IKRFWSSAE DRQSEHMV SSFEGFDE ANVLYFDH LEYEMQTKREIG TICTKPIHYSPTWQ SFNAILI CQDRGHAHS GIWSKHNF SQWMFWSLY CNFYLT FFIAMQNSKRNHC NVLMI NMDYLFV +clone_027 RGQGRQQMYW SAAFELQA TPSGEITQ EQKFPNRT EGGNQCMKQTWED WLEFNDCVMYHMIHI RAAAPQHPQ EDQNRMG TWFVAVQ VYHRPRII PKHMPLF YWFFYNKFETW HFNFTNYTKYWT EIHCQAF +clone_028 CKEKIVW QMEYFIEN VFIICFV FNQWWDW DLLVNEWAGN TTNAHHHPLFVSQIAW IQKGEKG DPNEQLLIT GVGWAAYK EIILIKHGN SPIVICLR NPHQMIWMRFEIDH WKDWNLILSFWEFCDDLK CDCFDLWG +clone_029 AALYAAYMG VFMFHA LQPQMGD ACIRSRGQ EVVVNRDKRL YWDVFLMKLPCYFS KQGNADE WVGADNWQS LTQKLC ICFMIHNME ETRMTLP FGIWRMNKSVGPII MPTMDFCM FGLQYDMVC +clone_030 FCDNASN TIGGTG QRCYLAYW MNTPYS VFPEMSYSSDKIP PQENQAE PMIRSWECQ PHTQWFA FSFTLKPR AMLMSCECE QMSSLSYW PWQPSAVPVP SEKSGMAL CSTINASR +clone_031 QIVHIIRVTL HEPYACIK TMLSQGC RFCPKLL YCYRYNMGSDMNP HEQPI YGMDQNFS CYEVSAMFL QKMPSVN VVHIFPP PTVKCADP SVMYNVFHASTVWY VANTVIILHWTFTHEQ SDGYLKLT +clone_032 GQMDMYVLFK SEFGTGMT RLPWVYYP VCGTVM ICSPHYMSPTPT IFYNVDCWVHT RCECGDD SNLDPKYHSH LPCDNA PKGSHWQ RMQYCEFS MCWYPEKCQKC IGWEFHF WVLRWMSW +clone_033 IITKKFDGTL AWDIDY KHWHWINNN GHAKKG KKAVLEQKCE HNSTLDYPYIML QLQPSLPS TGMPSQEHL HMVFFYMR EWKHGGE YWDMCIE LAQPHMPPYK DDSAGKDLDDEMENF TFQHINPYD +clone_034 MEAFFWYNS DGWDIGFH TDQTFFFL VHSIFLY NCVKGDSFVF GHLGQV LSGMWKPM LCRMTYWFT NCRSVC AIPIHIYV SPHSMLQ AVWTMPDIILRA YPMHNNEN CQFHIQGY +clone_035 CPWQFQNK HRGHYML QANVLTA KVSHGK QEGFESYNEIWTK CVLLEHTEHFFFNW DSSEQYQLS IPYNFVMV NYYDMGC EVMYVNNQ STDYDYVG FWAEISVWYVEFK EQHDWHRGCSWICDRR PQTWWLSS +clone_036 TLYWHNNC MNCMKWES DTNNEVKAA KSPLKAFP TGGSAKCVLDMRY FQSHMPRFTC TVMIVIPYF HHHVTTV PCMNQHY ACSCCYY ELPWWPP FKHVQPAYMR RMMFMMGQTAHCWVEYD KGVHCGTQ +clone_037 MQQQICRMP FMVALN DAWGYWWL CKPWNTMF VMVFYVMDACNT KAYMESKLFMHP WWVTVTGGT DGRYKHSGL MVSHMCA WTHHYQTC AWNWLIAW HWKVPCERRNWC RCPMP LFECSRG +clone_038 PCLFSFHE LKAETA GHWMWFNQ WDMGMRNS QTANMSHKCCT CVGQKNFD LRFTLWY WYWEYHPCIF IGPGRECP VFNQFQW YTHNEVN EEPSCILDSSYATQ GGMLY VVLMHHGG +clone_039 YCKADCAAH AVLVCHT NMWAFVVC TRPMWK IGRLTMTWTN PCYYMC RAIHCGH LTIPYRLKWY MNWKRWE STPHMWEP FNMCTE EYVIYCCEAIST PIFPVQTWYPKWPIMDSI ELEWTYYIR +clone_040 HLSRNFGPF PIIQAHPT HWNWERFMH HNFYPAG QVKSQTNPKFFIVM IMCLWC PHGMQYLYD VVFVDQACFD MAIYLFH RKLLFPQSV TRESHTF MHVLFSGAVQLATN MDYMVYCWVDH HYIYTDVA +clone_041 KCVGALSC KMLSVSA PNISSFS NECHGFV ESFTAVWGCCPYN FEAFNKS MLLHTTI PKWSRHKFY GWYSDDKN LMWQWSE NQREIMK RISVDGFCSPW VSEYGQS EYWHRMLG +clone_042 PLHSQFWW TDETDG RAWCCHT DNHKTWA HNTTTMWLRLVS RSHGKCCRGQQH LGGYHVH IMTSMHRQV KDWGVGG FYTRELVPQ HPHTSPET IMEIEMVMMQS LHTWIHSENT VWKENSR +clone_043 YLPVTIK DWPENR FTPTMFMNE IFSNDE RTYCAQVWQFGTY QWSWEWNDQYVIYC PPNWTQSLR TPLWELGA LQAMKQI KAPRNILQM EIMGTKW ANRLLLMTSFSK HRQRAIKLTMLLQ LIRTGRL +clone_044 RSHACWPIA QTWHPCV WNMIQQAV PMVTMS KRGPRFEWHGSTFY LTNAFPLQMFQMFH YGLGVQVC EPFEVVCAD DNEVNFRH ATQRVCK RHTINST DIQTNLCMPA GDFYKVKVWYI MCALPRCR +clone_045 SDSTHVKRG EPCSWS ELVDPKHK RQWTDQ WVQMDHVASEI YGPKFAGVD PSIGWWYKT QYYGHRWFW RMTPGH LDVDVGLH KANNEYC EADAEGECSYRPF PCDSGGPWCQ NFVMARN +clone_046 SLWFLRVDMP FGNAKDCW VIDGGEGA HQPGWI KVTVVIDFIIL EFVRL VHYTFPD VNTGAQMSDT PMCGPEC QHTICDKLN IFTSVDP RDMPCAMGPW IAPIDYNAMKMIIQH ITWVYHC +clone_047 HVYYSGVYEP TRWCIHG DHMNDSGR PAPFGH MMVTSIHWTHP PTDTNVQACMTC MHIWNYDRM TDTCDHGELY ISNFYSKR DSIWPKIH ALEKSDVA RIPQHEACNSWDYY MICHNFRYNGFKD HSNSCYHYQ +clone_048 PTESLLH WCEGME EHGIQDET IAIMPTK TSEVCEAMSQ RWEIWFAVV IMVSYNLP PDMTFPFW FKGHHWH HSGMQHIH IWGEKL GVIPTMRGHL FMAIAQNMIPFFNL QELHDNIN +clone_049 KELPLRLVYV FGTWSK KNNGAAR ESQNRPG PACADDNWCT VGAPVWPNTKPD KMCFVQFGM DNHIPRK MWIEPGM PAPKMLH SIFGGW HKEQMCMDACLA ECYQCPY LCHCHLTF diff --git a/software/tests/data/characterization/tcr_gd_plan.json b/software/tests/data/characterization/tcr_gd_plan.json new file mode 100644 index 0000000..4657525 --- /dev/null +++ b/software/tests/data/characterization/tcr_gd_plan.json @@ -0,0 +1,13 @@ +{ + "mode": "antibody_tcr_legacy_bulk", + "receptor": "TCRGD", + "chains": [ + "A", + "B" + ], + "fullChains": [ + "A", + "B" + ], + "hasFv": false +} diff --git a/software/tests/integration/test_characterization_snapshot.py b/software/tests/integration/test_characterization_snapshot.py new file mode 100644 index 0000000..1704d15 --- /dev/null +++ b/software/tests/integration/test_characterization_snapshot.py @@ -0,0 +1,40 @@ +"""Whole-frame characterization snapshot — the vectorization safety net. + +Freezes the CURRENT quantized pipeline output over a broad fixed corpus. The +vectorized engine (later) must reproduce these byte-for-byte after the same +quantization. Regenerate intentionally with: + uv run python -m tests._corpus_gen --write-golden +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from io_layer import read_input_tsv, read_plan, write_output_tsv +from pipeline import run + +DATA = Path(__file__).resolve().parents[1] / "data" / "characterization" +GOLDEN = DATA / "golden" +CASES = sorted(p.name.removesuffix("_input.tsv") for p in DATA.glob("*_input.tsv")) + + +@pytest.mark.parametrize("case", CASES) +def test_quantized_output_matches_golden(case, tmp_path): + reads = read_input_tsv(DATA / f"{case}_input.tsv") + plan = read_plan(DATA / f"{case}_plan.json") + out = run(reads, plan) + + props = tmp_path / "p.tsv" + write_output_tsv(out["properties"], props, sort_keys=["entity_key"]) + assert props.read_bytes() == (GOLDEN / f"{case}.properties.tsv").read_bytes() + + if (GOLDEN / f"{case}.aa_fraction.tsv").exists(): + aa = tmp_path / "aa.tsv" + write_output_tsv(out["aa_fraction"], aa, sort_keys=["entity_key", "aminoAcid"]) + assert aa.read_bytes() == (GOLDEN / f"{case}.aa_fraction.tsv").read_bytes() + + got_stats = json.dumps(out["stats"], sort_keys=True, separators=(",", ":")) + assert got_stats == (GOLDEN / f"{case}.stats.json").read_text() From 0d6bb3062776c76fd58372cd148fc3309ab48483 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 09:22:38 -0700 Subject: [PATCH 03/21] Harden characterization snapshot test diagnostics (case/artifact-attributed diffs, mode-gated aa_fraction) --- .../test_characterization_snapshot.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/software/tests/integration/test_characterization_snapshot.py b/software/tests/integration/test_characterization_snapshot.py index 1704d15..00c4637 100644 --- a/software/tests/integration/test_characterization_snapshot.py +++ b/software/tests/integration/test_characterization_snapshot.py @@ -8,6 +8,7 @@ from __future__ import annotations +import difflib import json from pathlib import Path @@ -21,6 +22,24 @@ CASES = sorted(p.name.removesuffix("_input.tsv") for p in DATA.glob("*_input.tsv")) +def _assert_file_bytes_equal(got: Path, golden: Path, label: str) -> None: + """Byte-exact comparison; on mismatch, raise a case/artifact-attributed + AssertionError carrying a unified line diff instead of an opaque byte blob.""" + got_b, gold_b = got.read_bytes(), golden.read_bytes() + if got_b == gold_b: + return + diff = "\n".join( + difflib.unified_diff( + gold_b.decode().splitlines(), + got_b.decode().splitlines(), + fromfile=f"golden/{label}", + tofile=f"got/{label}", + lineterm="", + ) + ) + raise AssertionError(f"{label}: byte mismatch vs golden\n{diff}") + + @pytest.mark.parametrize("case", CASES) def test_quantized_output_matches_golden(case, tmp_path): reads = read_input_tsv(DATA / f"{case}_input.tsv") @@ -29,12 +48,15 @@ def test_quantized_output_matches_golden(case, tmp_path): props = tmp_path / "p.tsv" write_output_tsv(out["properties"], props, sort_keys=["entity_key"]) - assert props.read_bytes() == (GOLDEN / f"{case}.properties.tsv").read_bytes() + _assert_file_bytes_equal(props, GOLDEN / f"{case}.properties.tsv", f"{case}.properties.tsv") - if (GOLDEN / f"{case}.aa_fraction.tsv").exists(): + if plan["mode"] == "peptide": aa = tmp_path / "aa.tsv" write_output_tsv(out["aa_fraction"], aa, sort_keys=["entity_key", "aminoAcid"]) - assert aa.read_bytes() == (GOLDEN / f"{case}.aa_fraction.tsv").read_bytes() + aa_golden = GOLDEN / f"{case}.aa_fraction.tsv" + assert aa_golden.exists(), f"{case}.aa_fraction.tsv: golden missing at {aa_golden}" + _assert_file_bytes_equal(aa, aa_golden, f"{case}.aa_fraction.tsv") got_stats = json.dumps(out["stats"], sort_keys=True, separators=(",", ":")) - assert got_stats == (GOLDEN / f"{case}.stats.json").read_text() + golden_text = (GOLDEN / f"{case}.stats.json").read_text() + assert got_stats == golden_text, f"{case}.stats.json mismatch" From fbe730afceb15bb8677aa9a14221ea0bc7f56d40 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 09:25:29 -0700 Subject: [PATCH 04/21] Add Hypothesis invariant tests for property functions --- software/pyproject.toml | 1 + software/tests/unit/test_invariants.py | 61 ++++++++++++++++++++++++++ software/uv.lock | 23 ++++++++++ 3 files changed, 85 insertions(+) create mode 100644 software/tests/unit/test_invariants.py diff --git a/software/pyproject.toml b/software/pyproject.toml index d3793e7..1fc1484 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -12,6 +12,7 @@ dev = [ "pytest>=9.0.2", "pytest-cov>=6.0.0", "ruff>=0.7.0", + "hypothesis>=6.155.2", ] [tool.pytest.ini_options] diff --git a/software/tests/unit/test_invariants.py b/software/tests/unit/test_invariants.py new file mode 100644 index 0000000..08360b2 --- /dev/null +++ b/software/tests/unit/test_invariants.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from aa_tables import STANDARD_AAS +from pka_tables import IPC2_PEPTIDE, IPC2_PROTEIN +from properties import ( + INSTABILITY_MIN_LENGTH, + aa_fractions, + charge_at_ph, + effective_length, + fv_charge, + instability_index, + isoelectric_point, +) + +aa_seq = st.text(alphabet=STANDARD_AAS, min_size=1, max_size=60) + + +@given(aa_seq) +def test_aa_fractions_sum_to_one(seq): + fr = aa_fractions(seq) + assert fr is not None + assert sum(fr.values()) == pytest.approx(1.0, abs=1e-9) + + +@given(aa_seq) +def test_pi_in_range_or_none(seq): + pi = isoelectric_point(seq, IPC2_PEPTIDE, include_cys=True) + assert pi is None or (0.0 <= pi <= 14.0) + + +@given( + aa_seq, + st.floats(min_value=0.5, max_value=13.5), + st.floats(min_value=0.5, max_value=13.5), +) +def test_charge_monotonic_in_ph(seq, ph_a, ph_b): + lo, hi = sorted((ph_a, ph_b)) + c_lo = charge_at_ph(seq, lo, IPC2_PEPTIDE, include_cys=True) + c_hi = charge_at_ph(seq, hi, IPC2_PEPTIDE, include_cys=True) + assert c_lo >= c_hi - 1e-9 # charge is non-increasing in pH + + +@given(aa_seq, aa_seq) +def test_fv_charge_additive(vh, vl): + fv = fv_charge(vh, vl, 7.0, IPC2_PROTEIN) + a = charge_at_ph(vh, 7.0, IPC2_PROTEIN, include_cys=False) + b = charge_at_ph(vl, 7.0, IPC2_PROTEIN, include_cys=False) + assert fv == pytest.approx(a + b, abs=1e-9) + + +@given(aa_seq) +def test_instability_floor(seq): + ii = instability_index(seq) + if effective_length(seq) < INSTABILITY_MIN_LENGTH: + assert ii is None + else: + assert ii is not None diff --git a/software/uv.lock b/software/uv.lock index 433c56a..c14ddad 100644 --- a/software/uv.lock +++ b/software/uv.lock @@ -51,6 +51,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, ] +[[package]] +name = "hypothesis" +version = "6.155.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "sortedcontainers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f5/04/64032a1dccd2233615c8a3f701bbb563558575ed017496a24b6d81762c91/hypothesis-6.155.2.tar.gz", hash = "sha256:ae36880287c9c5defe9f199d3d2b67d9947a4da2a46e6c57373cbdf2345b20e1", size = 477765, upload-time = "2026-06-05T16:32:23.63Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/6e/e735f27ac1a530a4cd0a31cd970ec495a3a11830fdc5d281cc292593b330/hypothesis-6.155.2-py3-none-any.whl", hash = "sha256:c85ce6dcd630a90ce501f1d1dd1bc84b97f5649ca8a27e134c8cbf5aa480b1a5", size = 544213, upload-time = "2026-06-05T16:32:21.15Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -212,6 +224,7 @@ source = { virtual = "." } [package.dev-dependencies] dev = [ { name = "biopython" }, + { name = "hypothesis" }, { name = "numpy" }, { name = "polars" }, { name = "pyarrow" }, @@ -225,6 +238,7 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "biopython", specifier = ">=1.84" }, + { name = "hypothesis", specifier = ">=6.155.2" }, { name = "numpy", specifier = ">=2.0.0" }, { name = "polars", specifier = ">=1.39.0" }, { name = "pyarrow", specifier = ">=18.0.0" }, @@ -232,3 +246,12 @@ dev = [ { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.7.0" }, ] + +[[package]] +name = "sortedcontainers" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, +] From db49c3b79f42b94e5dc1c0148a80ec6411b76396 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 09:33:50 -0700 Subject: [PATCH 05/21] Add vectorized per-residue count-matrix substrate --- software/src/vectorized.py | 39 +++++++++++++++++++ .../tests/unit/test_vectorized_substrate.py | 30 ++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 software/src/vectorized.py create mode 100644 software/tests/unit/test_vectorized_substrate.py diff --git a/software/src/vectorized.py b/software/src/vectorized.py new file mode 100644 index 0000000..69a77dd --- /dev/null +++ b/software/src/vectorized.py @@ -0,0 +1,39 @@ +"""Vectorized compute substrate. A column of sequences -> per-residue count +matrix + length + validity, matching properties.py's scalar cleaning exactly. +Single-threaded by construction (pure numpy elementwise + per-seq counting).""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + +from aa_tables import STANDARD_AAS + +_AA_INDEX = {aa: i for i, aa in enumerate(STANDARD_AAS)} + + +@dataclass(frozen=True) +class Substrate: + counts: np.ndarray # (N, 20) int64, STANDARD_AAS order + length: np.ndarray # (N,) int64 effective length + valid: np.ndarray # (N,) bool + + +def build_counts(seqs: list[str | None]) -> Substrate: + n = len(seqs) + counts = np.zeros((n, 20), dtype=np.int64) + valid = np.zeros(n, dtype=bool) + for i, s in enumerate(seqs): + if s is None or s == "" or "*" in s: + continue + row = counts[i] + any_std = False + for c in s.upper(): + j = _AA_INDEX.get(c) + if j is not None: + row[j] += 1 + any_std = True + valid[i] = any_std + length = counts.sum(axis=1) + return Substrate(counts=counts, length=length, valid=valid) diff --git a/software/tests/unit/test_vectorized_substrate.py b/software/tests/unit/test_vectorized_substrate.py new file mode 100644 index 0000000..4c1d13c --- /dev/null +++ b/software/tests/unit/test_vectorized_substrate.py @@ -0,0 +1,30 @@ +from __future__ import annotations + +from hypothesis import given +from hypothesis import strategies as st + +from aa_tables import STANDARD_AAS +from properties import aa_counts, clean_sequence, effective_length, is_invalid_sequence +from vectorized import build_counts + +# Include non-standard residues, stop codons, lowercase, gaps, and empties. +raw = st.lists(st.text(alphabet=STANDARD_AAS + "*BZXJUabc-", max_size=40), min_size=1, max_size=30) + + +@given(raw) +def test_counts_match_scalar_oracle(seqs): + sub = build_counts(seqs) + assert sub.counts.shape == (len(seqs), 20) + for i, s in enumerate(seqs): + invalid = is_invalid_sequence(s) or not clean_sequence(s) + assert bool(sub.valid[i]) == (not invalid) + if not invalid: + expected = [aa_counts(s)[aa] for aa in STANDARD_AAS] + assert sub.counts[i].tolist() == expected + assert int(sub.length[i]) == effective_length(s) + + +def test_handles_none_and_empty(): + sub = build_counts([None, "", "*", "BZXJ", "A"]) + assert sub.valid.tolist() == [False, False, False, False, True] + assert int(sub.length[4]) == 1 From 3469a9fc61cd9ef4d2325fb801bacfb56c013989 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 10:17:21 -0700 Subject: [PATCH 06/21] Vectorize linear properties (MW, GRAVY, aromaticity, aliphatic, extinction, aa fractions) --- software/src/vectorized.py | 121 +++++++++++++++++ software/tests/unit/test_vectorized_linear.py | 126 ++++++++++++++++++ 2 files changed, 247 insertions(+) create mode 100644 software/tests/unit/test_vectorized_linear.py diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 69a77dd..e64fe5f 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -7,11 +7,63 @@ from dataclasses import dataclass import numpy as np +from Bio.Data.IUPACData import protein_weights +from Bio.SeqUtils.ProtParam import ProtParamData from aa_tables import STANDARD_AAS _AA_INDEX = {aa: i for i, aa in enumerate(STANDARD_AAS)} +# --------------------------------------------------------------------------- # +# Constant vectors in STANDARD_AAS order, sourced verbatim from BioPython so +# the vectorized math uses the SAME numbers the scalar oracle (properties.py) +# resolves through ProteinAnalysis. Hand-retyping any of these would silently +# break per-property parity. +# --------------------------------------------------------------------------- # + +# Kyte-Doolittle hydropathy (Bio.SeqUtils.ProtParam.ProtParamData.kd). +_KD = np.array([ProtParamData.kd[aa] for aa in STANDARD_AAS], dtype=np.float64) + +# Average per-residue masses (Bio.Data.IUPACData.protein_weights). +_MASS = np.array([protein_weights[aa] for aa in STANDARD_AAS], dtype=np.float64) + +# Average water mass subtracted per peptide bond. Bio/SeqUtils/__init__.py +# molecular_weight() uses water = 18.0153 for the non-monoisotopic protein path, +# and ProteinAnalysis.molecular_weight() calls it with monoisotopic=False. +_WATER = 18.0153 + +# Aromatic indicator (F, W, Y) — ProteinAnalysis.aromaticity() = sum of the +# F/W/Y mole fractions. +_AROMATIC = np.array([1.0 if aa in "FWY" else 0.0 for aa in STANDARD_AAS], dtype=np.float64) + +# Reduced extinction contributions: Trp 5500, Tyr 1490 (per +# ProteinAnalysis.molar_extinction_coefficient(): W*5500 + Y*1490). +_EXT_RED = np.array( + [5500.0 if aa == "W" else 1490.0 if aa == "Y" else 0.0 for aa in STANDARD_AAS], + dtype=np.float64, +) + +_C_INDEX = STANDARD_AAS.index("C") +_A_INDEX = _AA_INDEX["A"] +_V_INDEX = _AA_INDEX["V"] +_I_INDEX = _AA_INDEX["I"] +_L_INDEX = _AA_INDEX["L"] + + +def _mask(arr: np.ndarray, valid: np.ndarray) -> np.ndarray: + """Return a float64 copy of `arr` with NaN wherever `valid` is False.""" + out = np.asarray(arr, dtype=np.float64).copy() + out[~valid] = np.nan + return out + + +def _safe_div(num: np.ndarray, length: np.ndarray, valid: np.ndarray) -> np.ndarray: + """`num / length` as float64, NaN where invalid or length == 0.""" + out = np.full(len(length), np.nan, dtype=np.float64) + ok = valid & (length > 0) + out[ok] = np.asarray(num, dtype=np.float64)[ok] / length[ok] + return out + @dataclass(frozen=True) class Substrate: @@ -37,3 +89,72 @@ def build_counts(seqs: list[str | None]) -> Substrate: valid[i] = any_std length = counts.sum(axis=1) return Substrate(counts=counts, length=length, valid=valid) + + +# --------------------------------------------------------------------------- # +# Linear properties — pure array ops over the count matrix. Each returns a +# float64 array (or pair of arrays) with NaN for invalid rows, matching the +# scalar properties.py oracle within 1e-6. +# --------------------------------------------------------------------------- # + + +def gravy(sub: Substrate) -> np.ndarray: + """Kyte-Doolittle GRAVY: (sum of per-residue hydropathy) / length. + + Mirrors ProteinAnalysis.gravy(), which returns total_gravy / length. + """ + return _safe_div(sub.counts @ _KD, sub.length, sub.valid) + + +def molecular_weight(sub: Substrate) -> np.ndarray: + """Average mass (Da): sum(residue masses) - (length - 1) * water. + + Mirrors Bio.SeqUtils.molecular_weight(seq, "protein"); water = 18.0153. + """ + mw = sub.counts @ _MASS - (sub.length - 1) * _WATER + return _mask(mw, sub.valid & (sub.length > 0)) + + +def aromaticity(sub: Substrate) -> np.ndarray: + """Aromatic mole fraction (F + W + Y) / length.""" + return _safe_div(sub.counts @ _AROMATIC, sub.length, sub.valid) + + +def aliphatic_index(sub: Substrate) -> np.ndarray: + """Ikai aliphatic index: 100 * (X_A + 2.9*X_V + 3.9*(X_I + X_L)). + + X_aa is the mole fraction; the shared `/length` is folded into _safe_div. + """ + c = sub.counts + num = 100.0 * (c[:, _A_INDEX] + 2.9 * c[:, _V_INDEX] + 3.9 * (c[:, _I_INDEX] + c[:, _L_INDEX])) + return _safe_div(num, sub.length, sub.valid) + + +def extinction(sub: Substrate) -> tuple[np.ndarray, np.ndarray]: + """Pace extinction coefficients at 280 nm, returned as (oxidized, reduced). + + reduced = nW*5500 + nY*1490 + oxidized = reduced + (nC // 2) * 125 (cystine Cys-Cys bonds) + + Matches the scalar extinction_coefficients() column order (oxidized first); + BioPython's molar_extinction_coefficient() returns (reduced, oxidized). + """ + red = sub.counts @ _EXT_RED + ox = red + (sub.counts[:, _C_INDEX] // 2) * 125.0 + return _mask(ox, sub.valid), _mask(red, sub.valid) + + +def aa_fractions(sub: Substrate) -> np.ndarray: + """Per-AA mole fractions, (N, 20) float64 in STANDARD_AAS order. + + Mirrors ProteinAnalysis.amino_acids_percent / 100 = count / length. + Invalid rows (and length == 0) are all-NaN. + """ + frac = np.divide( + sub.counts, + sub.length[:, None], + out=np.full(sub.counts.shape, np.nan, dtype=np.float64), + where=(sub.length[:, None] > 0), + ) + frac[~sub.valid] = np.nan + return frac.astype(np.float64) diff --git a/software/tests/unit/test_vectorized_linear.py b/software/tests/unit/test_vectorized_linear.py new file mode 100644 index 0000000..18932e1 --- /dev/null +++ b/software/tests/unit/test_vectorized_linear.py @@ -0,0 +1,126 @@ +"""Parity tests: vectorized linear properties vs the scalar properties.py oracle. + +For every VALID generated sequence each vectorized property must equal the +scalar value within abs=1e-6. Where the scalar returns None (invalid: empty, +stop codon, non-standard-only), the vectorized row must be NaN (and (NaN, NaN) +for extinction). Output arrays must be float64 so the downstream pipeline's +Float64 quantization rounding still applies. +""" + +from __future__ import annotations + +import math + +import numpy as np +import pytest +from hypothesis import given +from hypothesis import strategies as st + +import properties as scalar +from aa_tables import STANDARD_AAS +from vectorized import ( + aa_fractions, + aliphatic_index, + aromaticity, + build_counts, + extinction, + gravy, + molecular_weight, +) + +_TOL = 1e-6 + +# Mixed list: valid standard-AA seqs of varied length AND known invalids +# (empty, stop codon, non-standard-only, mixed-with-stop). +seqs_strategy = st.lists( + st.one_of( + st.text(alphabet=STANDARD_AAS, min_size=1, max_size=50), + st.sampled_from(["", "*", "BZXJ", "ACDEF*GH", "X", "---", "acdik"]), + ), + min_size=1, + max_size=25, +) + + +def _assert_scalar_parity(vec, seqs, scalar_fn): + """vec[i] == scalar_fn(seqs[i]) within tol; NaN where scalar returns None.""" + for i, s in enumerate(seqs): + exp = scalar_fn(s) + if exp is None: + assert math.isnan(vec[i]), f"seq={s!r}: expected NaN, got {vec[i]}" + else: + assert not math.isnan(vec[i]), f"seq={s!r}: expected {exp}, got NaN" + assert abs(vec[i] - exp) <= _TOL, f"seq={s!r}: vec={vec[i]} scalar={exp} diff={abs(vec[i] - exp)}" + + +@given(seqs_strategy) +def test_gravy_parity(seqs): + vec = gravy(build_counts(seqs)) + assert vec.dtype == np.float64 + _assert_scalar_parity(vec, seqs, scalar.gravy) + + +@given(seqs_strategy) +def test_molecular_weight_parity(seqs): + vec = molecular_weight(build_counts(seqs)) + assert vec.dtype == np.float64 + _assert_scalar_parity(vec, seqs, scalar.molecular_weight) + + +@given(seqs_strategy) +def test_aromaticity_parity(seqs): + vec = aromaticity(build_counts(seqs)) + assert vec.dtype == np.float64 + _assert_scalar_parity(vec, seqs, scalar.aromaticity) + + +@given(seqs_strategy) +def test_aliphatic_index_parity(seqs): + vec = aliphatic_index(build_counts(seqs)) + assert vec.dtype == np.float64 + _assert_scalar_parity(vec, seqs, scalar.aliphatic_index) + + +@given(seqs_strategy) +def test_extinction_parity(seqs): + ox, red = extinction(build_counts(seqs)) + assert ox.dtype == np.float64 + assert red.dtype == np.float64 + for i, s in enumerate(seqs): + exp_ox, exp_red = scalar.extinction_coefficients(s) + if exp_ox is None: + assert math.isnan(ox[i]), f"seq={s!r}: expected NaN ox, got {ox[i]}" + assert math.isnan(red[i]), f"seq={s!r}: expected NaN red, got {red[i]}" + else: + assert abs(ox[i] - exp_ox) <= _TOL, f"seq={s!r}: ox vec={ox[i]} scalar={exp_ox}" + assert abs(red[i] - exp_red) <= _TOL, f"seq={s!r}: red vec={red[i]} scalar={exp_red}" + + +@given(seqs_strategy) +def test_aa_fractions_parity(seqs): + frac = aa_fractions(build_counts(seqs)) + assert frac.dtype == np.float64 + assert frac.shape == (len(seqs), len(STANDARD_AAS)) + for i, s in enumerate(seqs): + exp = scalar.aa_fractions(s) + if exp is None: + assert np.all(np.isnan(frac[i])), f"seq={s!r}: expected all-NaN row" + else: + for j, aa in enumerate(STANDARD_AAS): + assert abs(frac[i, j] - exp[aa]) <= _TOL, f"seq={s!r} aa={aa}: vec={frac[i, j]} scalar={exp[aa]}" + + +def test_invalid_rows_are_nan(): + """Explicit invalid-row coverage, decoupled from hypothesis sampling.""" + seqs = ["", "*", "BZXJ", "ACDEF*GH", "ACDEFGHIKLMNPQRSTVWY"] + sub = build_counts(seqs) + for fn in (gravy, molecular_weight, aromaticity, aliphatic_index): + vec = fn(sub) + assert np.all(np.isnan(vec[:4])) # invalids + assert not math.isnan(vec[4]) # the valid seq + ox, red = extinction(sub) + assert np.all(np.isnan(ox[:4])) and np.all(np.isnan(red[:4])) + assert not math.isnan(ox[4]) and not math.isnan(red[4]) + frac = aa_fractions(sub) + assert np.all(np.isnan(frac[:4])) + assert pytest.approx(frac[4].sum(), abs=_TOL) == 1.0 From 6cd03c9324af45db6e21787013f101bf368fab3d Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 10:23:51 -0700 Subject: [PATCH 07/21] Vectorize charge and delta-charge (Henderson-Hasselbalch) --- software/src/vectorized.py | 67 +++++++++++++++++++ software/tests/unit/test_vectorized_charge.py | 63 +++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 software/tests/unit/test_vectorized_charge.py diff --git a/software/src/vectorized.py b/software/src/vectorized.py index e64fe5f..2c9770d 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -11,6 +11,7 @@ from Bio.SeqUtils.ProtParam import ProtParamData from aa_tables import STANDARD_AAS +from pka_tables import PKaSet _AA_INDEX = {aa: i for i, aa in enumerate(STANDARD_AAS)} @@ -158,3 +159,69 @@ def aa_fractions(sub: Substrate) -> np.ndarray: ) frac[~sub.valid] = np.nan return frac.astype(np.float64) + + +# --------------------------------------------------------------------------- # +# Charge — vectorized Henderson-Hasselbalch, byte-for-byte with BioPython's +# Bio.SeqUtils.IsoelectricPoint.charge_at_pH: +# +# positive_charge = sum over pos_pKs of content[aa] / (10**(pH - pK) + 1.0) +# negative_charge = sum over neg_pKs of content[aa] / (10**(pK - pH) + 1.0) +# charge = positive_charge - negative_charge +# +# content[Nterm] = content[Cterm] = 1.0 (one terminus pair per sequence); +# content[K/R/H/D/E/Y/C] = residue count. The scalar oracle injects IPC 2.0 +# pKa overrides (properties._ipc2_isoelectric_point): pos_pKs = {Nterm, K, R, H}, +# neg_pKs = {Cterm, D, E, Y} and C only when include_cys and "C" in side_chain. +# --------------------------------------------------------------------------- # + + +def _charge_raw(sub: Substrate, ph: float, pka_set: PKaSet, include_cys: bool) -> np.ndarray: + """Vectorized BioPython charge_at_pH over ALL rows — finite for every row, + invalid rows are NOT masked here. The public wrappers apply NaN via _mask. + + Kept finite-for-all-rows on purpose: the pI bisection (Task 8) reuses this + over the same count matrix and needs a clean charge value per row. + """ + c = sub.counts + # One N-terminus per sequence (count = 1), basic side chains K/R/H. + pos = 1.0 / (10 ** (ph - pka_set.n_terminus) + 1.0) + for aa in ("K", "R", "H"): + pk = pka_set.side_chain[aa] + pos = pos + c[:, _AA_INDEX[aa]] * (1.0 / (10 ** (ph - pk) + 1.0)) + + # One C-terminus per sequence (count = 1), acidic side chains D/E/Y (+C). + neg = 1.0 / (10 ** (pka_set.c_terminus - ph) + 1.0) + neg_aas = ["D", "E", "Y"] + if include_cys and "C" in pka_set.side_chain: + neg_aas.append("C") + for aa in neg_aas: + pk = pka_set.side_chain[aa] + neg = neg + c[:, _AA_INDEX[aa]] * (1.0 / (10 ** (pk - ph) + 1.0)) + + return pos - neg + + +def charge_at_ph(sub: Substrate, ph: float, pka_set: PKaSet, include_cys: bool = True) -> np.ndarray: + """Net charge at a given pH, float64, NaN for invalid rows. + + Mirrors properties.charge_at_ph (BioPython IsoelectricPoint.charge_at_pH + with IPC 2.0 pKa overrides). + """ + return _mask(_charge_raw(sub, ph, pka_set, include_cys), sub.valid) + + +def charge_shift( + sub: Substrate, + pka_set: PKaSet, + include_cys: bool = True, + ph_from: float = 7.4, + ph_to: float = 6.0, +) -> np.ndarray: + """ΔCharge = charge(ph_from) - charge(ph_to), float64, NaN for invalid rows. + + Mirrors properties.charge_shift (defaults ph_from=7.4, ph_to=6.0). + """ + hi = _charge_raw(sub, ph_from, pka_set, include_cys) + lo = _charge_raw(sub, ph_to, pka_set, include_cys) + return _mask(hi - lo, sub.valid) diff --git a/software/tests/unit/test_vectorized_charge.py b/software/tests/unit/test_vectorized_charge.py new file mode 100644 index 0000000..1a4d6e8 --- /dev/null +++ b/software/tests/unit/test_vectorized_charge.py @@ -0,0 +1,63 @@ +"""Parity tests: vectorized charge / ΔCharge vs the scalar properties.py oracle. + +Charge follows BioPython's IsoelectricPoint.charge_at_pH (Henderson-Hasselbalch) +with IPC 2.0 pKa overrides. For every VALID generated sequence each vectorized +value must equal the scalar value within abs=1e-6, across BOTH pKa sets +(IPC2_PEPTIDE, IPC2_PROTEIN) and BOTH include_cys settings, at several pH +points. Where the scalar returns None (invalid: empty, stop codon, +non-standard-only), the vectorized row must be NaN. Output arrays are float64. +""" + +from __future__ import annotations + +import math + +import numpy as np +from hypothesis import given +from hypothesis import strategies as st + +import properties as scalar +from aa_tables import STANDARD_AAS +from pka_tables import IPC2_PEPTIDE, IPC2_PROTEIN +from vectorized import build_counts, charge_at_ph, charge_shift + +# Mixed valid (standard-AA-only text) + invalid (empty, stop codon, +# non-standard-only) sequences, so every row exercises one of the branches. +seqs = st.lists( + st.one_of( + st.text(alphabet=STANDARD_AAS, min_size=1, max_size=50), + st.sampled_from(["", "*", "BZXJ"]), + ), + min_size=1, + max_size=25, +) + +PKA = [IPC2_PEPTIDE, IPC2_PROTEIN] + + +@given(seqs, st.sampled_from(PKA), st.booleans(), st.sampled_from([6.0, 7.0, 7.4])) +def test_charge_parity(seqs, pka, inc, ph): + sub = build_counts(seqs) + vec = charge_at_ph(sub, ph, pka, include_cys=inc) + assert vec.dtype == np.float64 + assert vec.shape == (len(seqs),) + for i, s in enumerate(seqs): + exp = scalar.charge_at_ph(s, ph, pka, include_cys=inc) + if exp is None: + assert math.isnan(vec[i]) + else: + assert abs(vec[i] - exp) <= 1e-6 + + +@given(seqs, st.sampled_from(PKA), st.booleans()) +def test_charge_shift_parity(seqs, pka, inc): + sub = build_counts(seqs) + vec = charge_shift(sub, pka, include_cys=inc) + assert vec.dtype == np.float64 + assert vec.shape == (len(seqs),) + for i, s in enumerate(seqs): + exp = scalar.charge_shift(s, pka, include_cys=inc) + if exp is None: + assert math.isnan(vec[i]) + else: + assert abs(vec[i] - exp) <= 1e-6 From 3fc494207afdbadfa51c3e3053dd07635140a675 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 10:29:59 -0700 Subject: [PATCH 08/21] Vectorize isoelectric point via lockstep bisection --- software/src/vectorized.py | 56 ++++++++++ software/tests/unit/test_vectorized_pi.py | 120 ++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 software/tests/unit/test_vectorized_pi.py diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 2c9770d..81db659 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -4,6 +4,7 @@ from __future__ import annotations +import math from dataclasses import dataclass import numpy as np @@ -225,3 +226,58 @@ def charge_shift( hi = _charge_raw(sub, ph_from, pka_set, include_cys) lo = _charge_raw(sub, ph_to, pka_set, include_cys) return _mask(hi - lo, sub.valid) + + +def isoelectric_point( + sub: Substrate, + pka_set: PKaSet, + include_cys: bool = True, + lo: float = 0.0, + hi: float = 14.0, + tol: float = 1e-3, +) -> np.ndarray: + """Vectorized pI: lockstep fixed-iteration bisection of `_charge_raw` over + all rows, bit-identical to the scalar `properties.isoelectric_point`. + + Mirrors `properties._bisect_charge_zero(charge_fn, lo, hi, tol)` exactly: + + * SAME bracket [lo, hi] and tol -> the bracket width starts at (hi - lo) and + halves every iteration regardless of data, so the loop runs the SAME + data-independent `ceil(log2((hi - lo) / tol))` iterations (== 14 for + [0, 14], tol 1e-3) for every row; + * SAME branch test `(f_mid > 0) == (f_lo > 0)` to move `lo` up to `mid`, + else move `hi` down to `mid`; + * `_charge_raw` is bit-identical to the scalar charge (charge parity = 0.0). + + The only scalar behaviour not reproduced is the `if f_mid == 0.0: return mid` + early-out — a midpoint landing on EXACT zero net charge — which is + astronomically rare and, when it happens, the lockstep loop still converges + to the same value within `tol`. + + Rows with no zero crossing in [lo, hi] (both endpoints strictly same-sign) + or invalid rows -> NaN, matching the scalar `None`. Returns float64. + """ + f = lambda ph: _charge_raw(sub, ph, pka_set, include_cys) # noqa: E731 + + f_lo = f(lo) + f_hi = f(hi) + # No zero-crossing when both endpoints share a strict sign (matches scalar). + crossing = ~(((f_lo > 0) & (f_hi > 0)) | ((f_lo < 0) & (f_hi < 0))) + + n = sub.counts.shape[0] + lo_a = np.full(n, lo, dtype=np.float64) + hi_a = np.full(n, hi, dtype=np.float64) + f_lo_a = np.asarray(f_lo, dtype=np.float64).copy() + + iters = math.ceil(math.log2((hi - lo) / tol)) + for _ in range(iters): + mid = 0.5 * (lo_a + hi_a) + f_mid = f(mid) + go_lo = (f_mid > 0) == (f_lo_a > 0) # same branch test as scalar + lo_a = np.where(go_lo, mid, lo_a) + f_lo_a = np.where(go_lo, f_mid, f_lo_a) + hi_a = np.where(go_lo, hi_a, mid) + + pi = 0.5 * (lo_a + hi_a) + pi[~(crossing & sub.valid)] = np.nan + return pi diff --git a/software/tests/unit/test_vectorized_pi.py b/software/tests/unit/test_vectorized_pi.py new file mode 100644 index 0000000..71a043e --- /dev/null +++ b/software/tests/unit/test_vectorized_pi.py @@ -0,0 +1,120 @@ +"""Parity + determinism tests: vectorized isoelectric point vs the scalar oracle. + +The vectorized pI (vectorized.isoelectric_point) is a lockstep, fixed-iteration +bisection over the row-wise charge function `_charge_raw`, mirroring the scalar +`properties._bisect_charge_zero` exactly: + +* SAME bracket [0, 14], SAME tol 1e-3 -> SAME data-independent iteration count + (ceil(log2(14/0.001)) == 14 halvings for every sequence); +* SAME branch test `(f_mid > 0) == (f_lo > 0)`; +* `_charge_raw` is bit-identical to the scalar charge (T7: 0.0 residual). + +Therefore the vectorized pI is bit-identical to the scalar pI for every valid +sequence, modulo the scalar's astronomically-rare `if f_mid == 0.0: return mid` +early-out (a midpoint landing on EXACT zero charge), which the vectorized loop +ignores and instead converges to the same value within tol. + +Two tests: + +* PARITY (Hypothesis): vectorized within 1.5e-3 of scalar over generated + sequences (valid + invalid), both pKa sets, both include_cys; NaN where scalar + None; float64 dtype. (Expect ~0.0 residual; 1.5e-3 is headroom.) +* STRADDLE (deterministic, seeded): over the committed peptide corpus PLUS >=2000 + seeded random standard-AA sequences, under BOTH (IPC2_PEPTIDE, include_cys=True) + and (IPC2_PROTEIN, include_cys=False): round(vectorized, 3) == round(scalar, 3) + for EVERY cell where the scalar is not None. The straddle count must be exactly 0. +""" + +from __future__ import annotations + +import csv +import math +import random +from pathlib import Path + +import numpy as np +from hypothesis import given +from hypothesis import strategies as st + +import properties as scalar +from aa_tables import STANDARD_AAS +from pka_tables import IPC2_PEPTIDE, IPC2_PROTEIN +from vectorized import build_counts, isoelectric_point + +# Mixed valid (standard-AA-only text) + invalid (empty, stop codon, +# non-standard-only) sequences, so every row exercises one of the branches. +seqs = st.lists( + st.one_of( + st.text(alphabet=STANDARD_AAS, min_size=1, max_size=50), + st.sampled_from(["", "*", "BZXJ"]), + ), + min_size=1, + max_size=25, +) + +# pipeline configs: peptides use IPC2_PEPTIDE/include_cys=True; +# full chains use IPC2_PROTEIN/include_cys=False. +PKA = [IPC2_PEPTIDE, IPC2_PROTEIN] +CONFIGS = [(IPC2_PEPTIDE, True), (IPC2_PROTEIN, False)] + +_CORPUS = Path(__file__).resolve().parents[1] / "data" / "characterization" / "peptide_input.tsv" + + +@given(seqs, st.sampled_from(PKA), st.booleans()) +def test_pi_parity(seqs, pka, inc): + sub = build_counts(seqs) + vec = isoelectric_point(sub, pka, include_cys=inc) + assert vec.dtype == np.float64 + assert vec.shape == (len(seqs),) + for i, s in enumerate(seqs): + exp = scalar.isoelectric_point(s, pka, include_cys=inc) + if exp is None: + assert math.isnan(vec[i]) + else: + assert abs(vec[i] - exp) <= 1.5e-3 + + +def _read_corpus_sequences() -> list[str]: + with _CORPUS.open(newline="") as fh: + reader = csv.DictReader(fh, delimiter="\t") + return [row.get("sequence", "") or "" for row in reader] + + +def _seeded_random_sequences(rng: random.Random, n: int) -> list[str]: + out: list[str] = [] + for _ in range(n): + length = rng.randint(1, 60) + out.append("".join(rng.choice(STANDARD_AAS) for _ in range(length))) + return out + + +def test_pi_no_straddles(): + """The determinism gate: round(vectorized, 3) == round(scalar, 3) for every + cell where the scalar is not None, across the committed corpus + >=2000 + seeded random sequences, under both pipeline pKa configs. Zero straddles. + """ + rng = random.Random(0) + sample = _read_corpus_sequences() + _seeded_random_sequences(rng, 2500) + assert len(sample) >= 2000 + + sub = build_counts(sample) + + straddles: list[tuple[str, str, float, float]] = [] + compared = 0 + for pka, inc in CONFIGS: + vec = isoelectric_point(sub, pka, include_cys=inc) + assert vec.dtype == np.float64 + for i, s in enumerate(sample): + exp = scalar.isoelectric_point(s, pka, include_cys=inc) + if exp is None: + # scalar None must correspond to vectorized NaN. + assert math.isnan(vec[i]) + continue + compared += 1 + rv = round(float(vec[i]), 3) + re = round(float(exp), 3) + if rv != re: + straddles.append((pka.name, s, float(vec[i]), float(exp))) + + assert compared >= 2000 + assert straddles == [], f"{len(straddles)} straddle(s) at 3 dp; first: {straddles[0]}" From 5aef86a676650bf1239166cc15965c0863e3dda7 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 10:37:09 -0700 Subject: [PATCH 09/21] Vectorize instability index via dipeptide weight lookup --- software/src/vectorized.py | 55 +++++++++ .../tests/unit/test_vectorized_instability.py | 112 ++++++++++++++++++ 2 files changed, 167 insertions(+) create mode 100644 software/tests/unit/test_vectorized_instability.py diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 81db659..63cc51b 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -45,6 +45,19 @@ dtype=np.float64, ) +# Dipeptide instability weights (Guruprasad et al. 1990) as a 20x20 matrix in +# STANDARD_AAS x STANDARD_AAS order: _DIWV[i, j] == DIWV[STANDARD_AAS[i]][...[j]]. +# ProteinAnalysis.instability_index() resolves the same ProtParamData.DIWV nested +# dict, so building the matrix from it (rather than retyping) keeps parity exact. +_DIWV = np.array( + [[ProtParamData.DIWV[a][b] for b in STANDARD_AAS] for a in STANDARD_AAS], + dtype=np.float64, +) + +# Spec R9 floor for the instability index — kept in sync with +# properties.INSTABILITY_MIN_LENGTH (cleaned-sequence length below this -> NA). +_INSTABILITY_MIN_LENGTH = 10 + _C_INDEX = STANDARD_AAS.index("C") _A_INDEX = _AA_INDEX["A"] _V_INDEX = _AA_INDEX["V"] @@ -281,3 +294,45 @@ def isoelectric_point( pi = 0.5 * (lo_a + hi_a) pi[~(crossing & sub.valid)] = np.nan return pi + + +# --------------------------------------------------------------------------- # +# Instability index — the only ORDER-dependent property. The count matrix is +# insufficient: it needs the consecutive-residue PAIRS of the cleaned sequence. +# We clean each sequence to an index array (matching properties._prepare / +# clean_sequence) and gather the dipeptide weights with numpy — NO BioPython +# ProteinAnalysis object is constructed per row. +# +# Per the Guruprasad et al. 1990 method as implemented by +# Bio.SeqUtils.ProtParam.ProteinAnalysis.instability_index over the CLEANED seq: +# score = sum(DIWV[seq[i]][seq[i+1]] for i in range(len - 1)) +# index = (10.0 / len(seq)) * score +# The scalar oracle (properties.SequenceContext.instability_index) additionally +# returns None below the spec R9 floor (cleaned length < INSTABILITY_MIN_LENGTH). +# --------------------------------------------------------------------------- # + + +def instability_index(seqs: list[str | None]) -> np.ndarray: + """Guruprasad instability index per sequence, float64, NaN where the scalar + oracle returns None. + + A row is NaN when the sequence is invalid (None / empty / contains a stop + codon `*` / nothing standard remains after cleaning) OR when the cleaned + (standard-AA-only) length is below `_INSTABILITY_MIN_LENGTH` (= 10). Order is + preserved: each row's value is a pure function of that row's sequence, with + no dict/set iteration leaking into the output. + """ + n = len(seqs) + out = np.full(n, np.nan, dtype=np.float64) + for i, s in enumerate(seqs): + if s is None or s == "" or "*" in s: + continue + idx = [j for c in s.upper() if (j := _AA_INDEX.get(c)) is not None] + length = len(idx) + if length < _INSTABILITY_MIN_LENGTH: + # Includes the "nothing standard remains" case (length == 0). + continue + a = np.asarray(idx, dtype=np.intp) + score = _DIWV[a[:-1], a[1:]].sum() + out[i] = (10.0 / length) * score + return out diff --git a/software/tests/unit/test_vectorized_instability.py b/software/tests/unit/test_vectorized_instability.py new file mode 100644 index 0000000..5b2ab6e --- /dev/null +++ b/software/tests/unit/test_vectorized_instability.py @@ -0,0 +1,112 @@ +"""Parity tests: vectorized instability index vs the scalar properties.py oracle. + +The Guruprasad instability index is the only ORDER-dependent property — it sums +the BioPython dipeptide instability weights (ProtParamData.DIWV) over consecutive +residue pairs of the CLEANED sequence, then scales by 10/length: + + score = sum(DIWV[seq[i]][seq[i+1]] for i in range(len - 1)) + index = (10.0 / len(seq)) * score + +(See Bio.SeqUtils.ProtParam.ProteinAnalysis.instability_index.) + +For every VALID generated sequence the vectorized value must equal the scalar +value within abs=1e-6. Where the scalar returns None — invalid (None / empty / +stop codon / non-standard-only) OR effective length < INSTABILITY_MIN_LENGTH +(= 10) — the vectorized row must be NaN. Output arrays are float64, and results +are order-stable (a sequence's value never depends on its position in the list). +""" + +from __future__ import annotations + +import math + +import numpy as np +from hypothesis import given, settings +from hypothesis import strategies as st + +import properties as scalar +from aa_tables import STANDARD_AAS +from vectorized import instability_index + +# Pools that exercise every branch of the cleaning + floor logic. +_STD = st.text(alphabet="".join(STANDARD_AAS), min_size=0, max_size=30) +_LOWER = st.text(alphabet="".join(STANDARD_AAS).lower(), min_size=1, max_size=10) +_NONSTD = st.text(alphabet="BZXUJ-", min_size=1, max_size=8) # non-standard only +_INVALID = st.sampled_from([None, "", "*", "ACD*EFG", "BZXUJ", "----", "bcdefghij"]) + +_SEQ = st.one_of(_STD, _LOWER, _NONSTD, _INVALID) + + +@given(seqs=st.lists(_SEQ, min_size=1, max_size=40)) +@settings(max_examples=300) +def test_matches_scalar_oracle(seqs: list[str | None]) -> None: + vec = instability_index(seqs) + assert vec.dtype == np.float64 + assert vec.shape == (len(seqs),) + + for i, s in enumerate(seqs): + expected = scalar.instability_index(s) + if expected is None: + assert math.isnan(vec[i]), f"row {i} {s!r}: expected NaN, got {vec[i]}" + else: + assert not math.isnan(vec[i]), f"row {i} {s!r}: expected {expected}, got NaN" + assert abs(vec[i] - expected) <= 1e-6, ( + f"row {i} {s!r}: vec={vec[i]} scalar={expected} residual={abs(vec[i] - expected)}" + ) + + +def test_floor_below_min_length_is_nan() -> None: + """effective length < 10 (after cleaning) -> NaN; >= 10 -> finite.""" + # 9 standard residues (with junk that cleans away) -> below floor. + below = "AC-DE*" # has stop codon -> invalid anyway + nine = "ACDEFGHIK" # 9 std residues, exactly below floor + ten = "ACDEFGHIKL" # 10 std residues, exactly at floor + eleven_dirty = "ACDEFGHIKLM-XBZ" # 11 std residues + junk -> at/above floor + + vec = instability_index([nine, ten, eleven_dirty, below]) + assert math.isnan(vec[0]), "9 residues must be below the floor -> NaN" + assert not math.isnan(vec[1]), "10 residues must be at the floor -> finite" + assert not math.isnan(vec[2]), "11 cleaned residues must be above the floor" + assert math.isnan(vec[3]), "stop codon -> invalid -> NaN" + + # Cross-check the finite rows against the oracle. + assert abs(vec[1] - scalar.instability_index(ten)) <= 1e-6 + assert abs(vec[2] - scalar.instability_index(eleven_dirty)) <= 1e-6 + + +def test_invalid_rows_are_nan() -> None: + seqs = [None, "", "*", "ACD*EF", "BZXUJ", "----", "X"] + vec = instability_index(seqs) + assert vec.dtype == np.float64 + assert np.all(np.isnan(vec)), f"all invalid rows must be NaN, got {vec}" + + +def test_known_value() -> None: + """Spot-check a fixed 12-mer against a directly recomputed DIWV sum.""" + seq = "ACDEFGHIKLMN" + expected = scalar.instability_index(seq) + vec = instability_index([seq]) + assert expected is not None + assert abs(vec[0] - expected) <= 1e-6 + + +def test_order_stability() -> None: + """A sequence's result must not depend on its position in the input list.""" + seqs = [ + "ACDEFGHIKLMNPQRSTVWY", # 20-mer, valid + None, + "ACDEFGHIK", # 9-mer, below floor -> NaN + "WYWYWYWYWYWY", # 12-mer, valid + "*", + "MKLVAACDEFGHIK", # 14-mer, valid + "bcdefghijk", # lowercase 10-mer -> 10 std residues, valid + ] + forward = instability_index(seqs) + reversed_in = list(reversed(seqs)) + reversed_out = instability_index(reversed_in) + + # Re-reverse the reversed output to align positions with `forward`. + realigned = reversed_out[::-1] + np.testing.assert_array_equal(np.isnan(forward), np.isnan(realigned)) # NaN pattern stable + finite = ~np.isnan(forward) + np.testing.assert_allclose(forward[finite], realigned[finite], atol=0.0, rtol=0.0) From 8d5cf95fd0e70a542b8bd3ff1ca9031e1cf055ee Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 10:59:02 -0700 Subject: [PATCH 10/21] Swap pipeline to vectorized engine; remove per-row compute loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace row-by-row BioPython compute with vectorized array math (build_counts substrate + per-property numpy/bisection). Add signed-zero canonicalization to the quantization boundary (determinism hardening; numpy reduction order can flip the sign of a sub-ULP zero). Regenerate the characterization golden under the canonicalized contract — only signed-zero cells changed (-0.0 -> 0.0), verified. --- software/src/main.py | 25 +- software/src/pipeline.py | 396 ++++++++---------- software/src/vectorized.py | 88 +++- .../golden/antibody_bulk_full.properties.tsv | 2 +- .../golden/sc_dropout.properties.tsv | 2 +- .../golden/tcr_gd.properties.tsv | 2 +- .../tests/integration/test_perf_benchmark.py | 89 ++++ software/tests/unit/test_vectorized_fv_pi.py | 110 +++++ 8 files changed, 474 insertions(+), 240 deletions(-) create mode 100644 software/tests/integration/test_perf_benchmark.py create mode 100644 software/tests/unit/test_vectorized_fv_pi.py diff --git a/software/src/main.py b/software/src/main.py index 422f7bf..32420e2 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -9,14 +9,23 @@ from __future__ import annotations -import argparse -import json -import logging -import sys -from pathlib import Path - -from io_layer import read_input_tsv, read_plan, write_output_tsv -from pipeline import run +import os + +# Pin the polars thread pool to 1 BEFORE polars (or anything importing it, +# including `pipeline`) is loaded — POLARS_MAX_THREADS is read once at import. +# The block reserves cpu(1), and single-threaded execution keeps output +# byte-stable (no thread-count-dependent reduction order). setdefault so an +# explicit override from the environment still wins. +os.environ.setdefault("POLARS_MAX_THREADS", "1") + +import argparse # noqa: E402 +import json # noqa: E402 +import logging # noqa: E402 +import sys # noqa: E402 +from pathlib import Path # noqa: E402 + +from io_layer import read_input_tsv, read_plan, write_output_tsv # noqa: E402 +from pipeline import run # noqa: E402 def _configure_logging() -> None: diff --git a/software/src/pipeline.py b/software/src/pipeline.py index 3aede3f..3109e99 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -18,14 +18,14 @@ import logging from typing import Any +import numpy as np import polars as pl +import vectorized as vec from aa_tables import STANDARD_AAS from pka_tables import IPC2_PEPTIDE, IPC2_PROTEIN from properties import ( INSTABILITY_MIN_LENGTH, - SequenceContext, - _bisect_charge_zero, effective_length, ) @@ -94,7 +94,21 @@ def _quantize_for_cid(df: pl.DataFrame) -> pl.DataFrame: for c in df.columns: dp = _decimals_for(c) if dp is not None and df.schema[c] == pl.Float64: - exprs.append(pl.col(c).round(dp)) + # Round, then canonicalize signed zero to `+0.0`. A property whose + # true value is ~0 (e.g. GRAVY of a charge-balanced chain) lands on a + # sub-ULP residual whose SIGN depends on summation order — the scalar + # BioPython path sums in residue order, the vectorized `counts @ KD` + # in AA-index order, so the same numeric zero rounds to `-0.0` on one + # and `+0.0` on the other. Both are numerically equal, but the TSV + # writer emits different bytes (`-0.0` vs `0.0`). `-0.0 == 0.0` is + # True in polars, so the `when` maps BOTH signed zeros to `+0.0` + # (note: `col + 0.0` does NOT canonicalize in polars — it preserves + # the negative-zero bit). This makes the content-addressable id + # insensitive to FP-residual-sign drift — the same determinism + # guarantee the rounding already gives the other digits. round(null) + # is null and `null == 0.0` is null, so NA cells stay null/empty. + rounded = pl.col(c).round(dp) + exprs.append(pl.when(rounded == 0.0).then(pl.lit(0.0)).otherwise(rounded).alias(c)) return df.with_columns(exprs) if exprs else df @@ -143,111 +157,82 @@ def run(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any]: ] -_NA_PEPTIDE_ROW: dict[str, float | None] = dict.fromkeys(PEPTIDE_PROPERTY_COLUMNS) +def _na_to_null(values: np.ndarray) -> pl.Series: + """Wrap a numpy float64 array as a Float64 polars Series with NaN -> null. - -def _compute_peptide_row(seq: str) -> dict[str, float | None]: - """All 9 scalar properties for a single peptide. Cys is included as - ionizable (free thiol assumption) — the IPC 2.0 peptide pKa set is used. - - Uses one `SequenceContext` per sequence so `_prepare`, `ProteinAnalysis`, - and `IsoelectricPoint(IPC2_PEPTIDE, include_cys=True)` are constructed - exactly once and shared across all 10 property reads. - """ - ctx = SequenceContext.from_seq(seq) - if ctx is None: - return dict(_NA_PEPTIDE_ROW) - eox, ered = ctx.extinction_coefficients() - return { - "charge_peptide": ctx.charge_at_ph(PH, IPC2_PEPTIDE, include_cys=True), - "chargeShift_peptide": ctx.charge_shift(IPC2_PEPTIDE, include_cys=True), - "gravy_peptide": ctx.gravy(), - "mw_peptide": ctx.molecular_weight(), - "pi_peptide": ctx.isoelectric_point(IPC2_PEPTIDE, include_cys=True), - "eox_peptide": eox, - "ered_peptide": ered, - "instability_peptide": ctx.instability_index(), - "aliphatic_peptide": ctx.aliphatic_index(), - "aromaticity_peptide": ctx.aromaticity(), - } - - -def _compute_peptide_row_from_ctx(ctx: SequenceContext) -> dict[str, float | None]: - """Variant that takes a pre-built context — used when `run_peptide` already - constructed one to share with the AA-fraction pass. + THE #1 vectorization trap: the vectorized functions emit `np.nan` for NA + rows, but the golden's NA cells are EMPTY. A NaN survives to TSV as the + literal "NaN", not an empty cell. `fill_nan(None)` converts NaN -> null so + the IO layer writes an empty cell, matching the scalar `None` -> empty path. """ - eox, ered = ctx.extinction_coefficients() - return { - "charge_peptide": ctx.charge_at_ph(PH, IPC2_PEPTIDE, include_cys=True), - "chargeShift_peptide": ctx.charge_shift(IPC2_PEPTIDE, include_cys=True), - "gravy_peptide": ctx.gravy(), - "mw_peptide": ctx.molecular_weight(), - "pi_peptide": ctx.isoelectric_point(IPC2_PEPTIDE, include_cys=True), - "eox_peptide": eox, - "ered_peptide": ered, - "instability_peptide": ctx.instability_index(), - "aliphatic_peptide": ctx.aliphatic_index(), - "aromaticity_peptide": ctx.aromaticity(), - } + return pl.Series(values=values, dtype=pl.Float64).fill_nan(None) def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: - """Compute peptide-mode outputs. + """Compute peptide-mode outputs on the vectorized engine. + + Builds the per-residue count substrate once over the `sequence` column, then + derives all 10 peptide properties + the AA-fraction matrix with array ops. + charge / chargeShift / pi use the IPC 2.0 peptide pKa set with Cys included + as ionizable (free thiol); the rest are the linear / instability funcs. - Builds one `SequenceContext` per sequence and reuses it for both the - properties row and the AA-fraction rows — so each sequence is `_prepare`d - once and BioPython objects are constructed once across all 11 reads. - Accumulates columnar arrays directly into a dict-of-lists (one allocation - per column, vs. one dict per row) and constructs the DataFrame from those. + NaN -> null is applied to every emitted float column (and the AA-fraction + `value`) so NA rows render as empty cells, byte-matching the scalar path. """ keys = reads["entity_key"].to_list() seqs = reads["sequence"].to_list() n = len(seqs) log.info("Computing peptide properties + AA fractions (%d sequences)", n) - prop_cols: dict[str, list[Any]] = {"entity_key": [], **{c: [] for c in PEPTIDE_PROPERTY_COLUMNS}} - aa_entity: list[str] = [] - aa_amino: list[str] = [] - aa_value: list[float | None] = [] - for k, s in zip(keys, seqs): - prop_cols["entity_key"].append(k) - ctx = SequenceContext.from_seq(s) - if ctx is None: - for c in PEPTIDE_PROPERTY_COLUMNS: - prop_cols[c].append(None) - # Emit one row per std AA with NA value, so the 2-axis PColumn - # keeps a uniform shape across entities. - for aa in STANDARD_AAS: - aa_entity.append(k) - aa_amino.append(aa) - aa_value.append(None) - else: - row = _compute_peptide_row_from_ctx(ctx) - for c in PEPTIDE_PROPERTY_COLUMNS: - prop_cols[c].append(row[c]) - fractions = ctx.aa_fractions() - for aa in STANDARD_AAS: - aa_entity.append(k) - aa_amino.append(aa) - aa_value.append(fractions[aa]) + sub = vec.build_counts(seqs) + + eox, ered = vec.extinction(sub) + prop_arrays: dict[str, np.ndarray] = { + "charge_peptide": vec.charge_at_ph(sub, PH, IPC2_PEPTIDE, include_cys=True), + "chargeShift_peptide": vec.charge_shift(sub, IPC2_PEPTIDE, include_cys=True), + "gravy_peptide": vec.gravy(sub), + "mw_peptide": vec.molecular_weight(sub), + "pi_peptide": vec.isoelectric_point(sub, IPC2_PEPTIDE, include_cys=True), + "eox_peptide": eox, + "ered_peptide": ered, + "instability_peptide": vec.instability_index(seqs), + "aliphatic_peptide": vec.aliphatic_index(sub), + "aromaticity_peptide": vec.aromaticity(sub), + } properties = pl.DataFrame( - prop_cols, - schema={"entity_key": pl.Utf8, **{c: pl.Float64 for c in PEPTIDE_PROPERTY_COLUMNS}}, + { + "entity_key": pl.Series(values=keys, dtype=pl.Utf8), + **{c: _na_to_null(prop_arrays[c]) for c in PEPTIDE_PROPERTY_COLUMNS}, + } ) + + # AA-fraction long frame from the (N, 20) mole-fraction matrix: 20 rows per + # entity (one per STANDARD_AAS), value NaN -> null for invalid entities so + # the 2-axis PColumn keeps a uniform shape across entities. + fractions = vec.aa_fractions(sub) # (N, 20), NaN for invalid rows + aa_entity = [k for k in keys for _ in STANDARD_AAS] + aa_amino = list(STANDARD_AAS) * n + aa_value = fractions.reshape(-1) # row-major: entity 0's 20 AAs, then entity 1's, ... aa_fraction = pl.DataFrame( - {"entity_key": aa_entity, "aminoAcid": aa_amino, "value": aa_value}, - schema={"entity_key": pl.Utf8, "aminoAcid": pl.Utf8, "value": pl.Float64}, + { + "entity_key": pl.Series(values=aa_entity, dtype=pl.Utf8), + "aminoAcid": pl.Series(values=aa_amino, dtype=pl.Utf8), + "value": _na_to_null(aa_value), + } ) # Quantize at the output boundary like the property columns. aaFraction # displays as .3f; round to 5 dp (one digit below display, headroom for the - # upcoming vectorization's FP drift). `value` is a closed-form ratio today, - # so rounding only trims repeating-decimal tails — no display change. + # vectorization's FP drift). round(null) == null, so NA cells stay empty. aa_fraction = aa_fraction.with_columns(pl.col("value").round(5)) # R9 — flag whether any *real* peptide falls below the Instability Index # floor. `if s` filters None / "" so the banner does not fire on empty - # cells (which are no peptide, not a short peptide); `0 < effective_length` - # filters sequences that clean to empty (e.g. all-non-standard residues). + # cells (no peptide, not a short peptide); `0 < effective_length` filters + # sequences that clean to empty (all-non-standard residues). NOTE: this is + # the scalar `effective_length` count, NOT `sub.length`: a stop-codon cell + # like "ACDE*FGHI" is substrate-INVALID (length 0) but still has 8 standard + # residues, which the scalar oracle counts toward the floor. Using + # effective_length keeps this stat bit-identical to the scalar path. has_below_floor = any(0 < effective_length(s) < INSTABILITY_MIN_LENGTH for s in seqs if s) stats = { "medianCdr3Length": {}, @@ -282,82 +267,6 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: REQUIRED_FEATURES = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") -_NA_CDR3_ROW: dict[str, float | None] = dict.fromkeys(CDR3_PROPS) -_NA_FULL_CHAIN_ROW: dict[str, float | None] = dict.fromkeys(FULL_CHAIN_PROPS) -_NA_FV_ROW: dict[str, float | None] = dict.fromkeys(FV_PROPS) - - -def _compute_cdr3_row(cdr3: str) -> dict[str, float | None]: - """CDR3 charge, ΔCharge, and GRAVY. CDR3 uses the IPC 2.0 peptide pKa set - with Cys included as ionizable (per spec — CDR3 Cys treated as free thiol). - """ - ctx = SequenceContext.from_seq(cdr3) - if ctx is None: - return dict(_NA_CDR3_ROW) - return { - "charge": ctx.charge_at_ph(PH, IPC2_PEPTIDE, include_cys=True), - "chargeShift": ctx.charge_shift(IPC2_PEPTIDE, include_cys=True), - "gravy": ctx.gravy(), - } - - -def _compute_full_chain_row(chain_seq: str) -> dict[str, float | None]: - """Full-chain (VH / VL etc.) — protein pKa set, Cys excluded from - ionisation (assumed disulfide-bonded). One context, one ProteinAnalysis, - one IsoelectricPoint shared across all 9 reads. - """ - return _compute_full_chain_row_from_ctx(SequenceContext.from_seq(chain_seq)) - - -def _compute_full_chain_row_from_ctx(ctx: SequenceContext | None) -> dict[str, float | None]: - if ctx is None: - return dict(_NA_FULL_CHAIN_ROW) - eox, ered = ctx.extinction_coefficients() - return { - "charge": ctx.charge_at_ph(PH, IPC2_PROTEIN, include_cys=False), - "pi": ctx.isoelectric_point(IPC2_PROTEIN, include_cys=False), - "gravy": ctx.gravy(), - "mw": ctx.molecular_weight(), - "eox": eox, - "ered": ered, - "instability": ctx.instability_index(), - "aliphatic": ctx.aliphatic_index(), - "aromaticity": ctx.aromaticity(), - } - - -def _compute_fv_row(vh: str, vl: str) -> dict[str, float | None]: - """Fv columns — IPC 2.0 protein set, Cys-excluded. pI uses the per-chain - sum of charge functions (NOT a concatenated string), per spec. Fv - ΔCharge = ΔCharge(VH) + ΔCharge(VL). - - Builds one context per chain so the chain-level full-chain pass and the - Fv pass share their `IsoelectricPoint(IPC2_PROTEIN, include_cys=False)` — - the same IP serves both `charge_at_ph(7.0)` and the bisection here. - """ - return _compute_fv_row_from_ctx(SequenceContext.from_seq(vh), SequenceContext.from_seq(vl)) - - -def _compute_fv_row_from_ctx( - vh_ctx: SequenceContext | None, - vl_ctx: SequenceContext | None, -) -> dict[str, float | None]: - if vh_ctx is None or vl_ctx is None: - return dict(_NA_FV_ROW) - ox_vh, red_vh = vh_ctx.extinction_coefficients() - ox_vl, red_vl = vl_ctx.extinction_coefficients() - fn_vh = vh_ctx.isoelectric(IPC2_PROTEIN, include_cys=False).charge_at_pH - fn_vl = vl_ctx.isoelectric(IPC2_PROTEIN, include_cys=False).charge_at_pH - return { - "charge": fn_vh(PH) + fn_vl(PH), - "chargeShift": (fn_vh(7.4) - fn_vh(6.0)) + (fn_vl(7.4) - fn_vl(6.0)), - "pi": _bisect_charge_zero(lambda ph: fn_vh(ph) + fn_vl(ph)), - "eox": ox_vh + ox_vl, - "ered": red_vh + red_vl, - "mw": vh_ctx.molecular_weight() + vl_ctx.molecular_weight(), - } - - def _reconstruct_chain(row: dict[str, str], chain: str) -> str | None: """Concatenate FR1+CDR1+FR2+CDR2+FR3+CDR3+FR4. Returns None if any region is missing (empty string in input). @@ -375,10 +284,10 @@ def _reconstruct_chain(row: dict[str, str], chain: str) -> str | None: def _planned_output_columns(plan: dict[str, Any]) -> list[str]: """Output column order — matches process.tpl.tengo's xsv import expectations. - This and `_compute_row_for` are sibling functions: the column list and - per-row population both walk (chains × CDR3_PROPS), (fullChains × FULL_CHAIN_PROPS), - and conditionally FV_PROPS. Property name tuples (CDR3_PROPS, FULL_CHAIN_PROPS, - FV_PROPS) are the single source of truth — both functions consume them. + Walks (chains × CDR3_PROPS), (fullChains × FULL_CHAIN_PROPS), and + conditionally FV_PROPS. The property name tuples (CDR3_PROPS, + FULL_CHAIN_PROPS, FV_PROPS) are the single source of truth — `run_antibody_tcr` + populates exactly these columns in this order. """ cols: list[str] = [] for ch in plan.get("chains", []): @@ -390,45 +299,6 @@ def _planned_output_columns(plan: dict[str, Any]) -> list[str]: return cols -def _compute_row_for(record: dict[str, Any], plan: dict[str, Any]) -> dict[str, Any]: - """Build the output row for one input record. The set of populated - columns matches `_planned_output_columns(plan)` exactly — both are - driven by the same plan keys and shared property tuples. - - For each chain, the reconstructed full-chain context is held and reused - by the Fv pass below, so VH/VL `IsoelectricPoint(IPC2_PROTEIN, False)` is - constructed once per clone instead of twice. - """ - out: dict[str, Any] = {"entity_key": record["entity_key"]} - - # CDR3 per chain — empty cell ⇒ NA for this clone, not for the column. - for ch in plan.get("chains", []): - cdr3_props = _compute_cdr3_row(record.get(f"{ch}_CDR3") or "") - for p in CDR3_PROPS: - out[f"{p}_{ch}_CDR3"] = cdr3_props[p] - - # Full chain — reconstruct then compute. NA per-clone if any of the - # seven regions is empty for that clone. Cache contexts for Fv reuse. - chain_ctx: dict[str, SequenceContext | None] = {} - for ch in plan.get("fullChains", []): - reconstructed = _reconstruct_chain(record, ch) - ctx = SequenceContext.from_seq(reconstructed) if reconstructed is not None else None - chain_ctx[ch] = ctx - full_props = _compute_full_chain_row_from_ctx(ctx) - for p in FULL_CHAIN_PROPS: - out[f"{p}_{ch}_VDJRegion"] = full_props[p] - - # Fv — only when both VH and VL fully reconstructed for this clone. - # Reuses the per-chain contexts from the full-chain pass above so the - # IPC2_PROTEIN/include_cys=False IsoelectricPoint is shared. - if plan.get("hasFv"): - fv = _compute_fv_row_from_ctx(chain_ctx.get("A"), chain_ctx.get("B")) - for p in FV_PROPS: - out[f"{p}_Fv"] = fv[p] - - return out - - def _median_cdr3_length_by_chain(reads: pl.DataFrame, chains: list[str]) -> dict[str, float]: """Median effective length of CDR3 sequences per chain. @@ -451,7 +321,52 @@ def _median_cdr3_length_by_chain(reads: pl.DataFrame, chains: list[str]) -> dict return out +def _column_or_empty(reads: pl.DataFrame, col: str) -> list[str | None]: + """Return `col` as a python list, or a list of empty strings (one per row) + when the column is absent from the reads. Mirrors the old per-row + `record.get(col, "")` / `record.get(col) or ""` defence against a plan that + names a chain/region the input TSV does not carry. + """ + if col not in reads.columns: + return [""] * reads.height + return reads[col].to_list() + + +def _reconstruct_chain_column(reads: pl.DataFrame, chain: str) -> list[str | None]: + """Reconstruct the full chain for every clone: FR1+CDR1+...+FR4, None if any + region is missing (empty cell OR absent column). + + Pulls each region column once, then delegates the per-clone rule to + `_reconstruct_chain` so the reconstruction logic has a single source of + truth. A region column absent from the reads is materialised as empty + strings, so the missing-region -> None rule fires the same way. + """ + cols = {feat: _column_or_empty(reads, f"{chain}_{feat}") for feat in REQUIRED_FEATURES} + out: list[str | None] = [] + for i in range(reads.height): + row = {f"{chain}_{feat}": cols[feat][i] for feat in REQUIRED_FEATURES} + out.append(_reconstruct_chain(row, chain)) + return out + + def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any]: + """Compute antibody/TCR-mode outputs on the vectorized engine. + + * CDR3 (per `plan["chains"]`): build_counts on the `{chain}_CDR3` column -> + charge / chargeShift / gravy with the IPC 2.0 peptide pKa set, Cys + included (CDR3 Cys treated as free thiol). NaN where the CDR3 cell is + empty / invalid. + * Full chain (per `plan["fullChains"]`): reconstruct FR1..FR4 -> build_counts + -> charge / pi (IPC 2.0 protein, Cys excluded) + gravy / mw / eox / ered / + instability / aliphatic / aromaticity. NaN where ANY region is missing. + * Fv (when `plan["hasFv"]`): per-chain SUMS over VH=A, VL=B — charge, + chargeShift, eox, ered, mw additive; pi = bisection of the SUMMED charge + function. NaN where EITHER chain is not fully reconstructed. + + Every emitted float column has NaN -> null applied so NA rows render as + empty cells, byte-matching the scalar path. Column order is + `_planned_output_columns(plan)`. + """ chains = plan.get("chains", []) full_chains = plan.get("fullChains", []) n = reads.height @@ -461,15 +376,70 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any log.info("Reconstructing full chains %s and computing full-chain properties", list(full_chains)) if plan.get("hasFv"): log.info("Computing Fv properties (paired VH+VL)") + + series: dict[str, pl.Series] = {"entity_key": pl.Series(values=reads["entity_key"].to_list(), dtype=pl.Utf8)} + + # CDR3 per chain — IPC2_PEPTIDE, Cys included. Empty cell -> NA for this + # clone (build_counts marks it invalid -> the funcs emit NaN -> null). + for ch in chains: + sub = vec.build_counts(_column_or_empty(reads, f"{ch}_CDR3")) + cdr3_arrays = { + "charge": vec.charge_at_ph(sub, PH, IPC2_PEPTIDE, include_cys=True), + "chargeShift": vec.charge_shift(sub, IPC2_PEPTIDE, include_cys=True), + "gravy": vec.gravy(sub), + } + for p in CDR3_PROPS: + series[f"{p}_{ch}_CDR3"] = _na_to_null(cdr3_arrays[p]) + + # Full chain — reconstruct then compute. IPC2_PROTEIN, Cys excluded. NA per + # clone where any region is missing (reconstruction None -> invalid row). + # Cache the per-chain substrates for Fv reuse below. + chain_subs: dict[str, vec.Substrate] = {} + chain_seqs: dict[str, list[str | None]] = {} + for ch in full_chains: + seqs = _reconstruct_chain_column(reads, ch) + sub = vec.build_counts(seqs) + chain_subs[ch] = sub + chain_seqs[ch] = seqs + eox, ered = vec.extinction(sub) + full_arrays = { + "charge": vec.charge_at_ph(sub, PH, IPC2_PROTEIN, include_cys=False), + "pi": vec.isoelectric_point(sub, IPC2_PROTEIN, include_cys=False), + "gravy": vec.gravy(sub), + "mw": vec.molecular_weight(sub), + "eox": eox, + "ered": ered, + "instability": vec.instability_index(seqs), + "aliphatic": vec.aliphatic_index(sub), + "aromaticity": vec.aromaticity(sub), + } + for p in FULL_CHAIN_PROPS: + series[f"{p}_{ch}_VDJRegion"] = _na_to_null(full_arrays[p]) + + # Fv — per-chain sums over VH=A, VL=B. charge/chargeShift/eox/ered/mw are + # additive (NaN propagates if either chain invalid); pi bisects the SUMMED + # charge function. Reuses the cached full-chain substrates so the chains are + # reconstructed/counted once. Mirrors scalar `_compute_fv_row_from_ctx`. + if plan.get("hasFv"): + sub_vh = chain_subs["A"] + sub_vl = chain_subs["B"] + eox_vh, ered_vh = vec.extinction(sub_vh) + eox_vl, ered_vl = vec.extinction(sub_vl) + fv_arrays = { + "charge": vec.charge_at_ph(sub_vh, PH, IPC2_PROTEIN, include_cys=False) + + vec.charge_at_ph(sub_vl, PH, IPC2_PROTEIN, include_cys=False), + "chargeShift": vec.charge_shift(sub_vh, IPC2_PROTEIN, include_cys=False) + + vec.charge_shift(sub_vl, IPC2_PROTEIN, include_cys=False), + "pi": vec.fv_isoelectric_point(sub_vh, sub_vl, IPC2_PROTEIN, include_cys=False), + "eox": eox_vh + eox_vl, + "ered": ered_vh + ered_vl, + "mw": vec.molecular_weight(sub_vh) + vec.molecular_weight(sub_vl), + } + for p in FV_PROPS: + series[f"{p}_Fv"] = _na_to_null(fv_arrays[p]) + out_cols = _planned_output_columns(plan) - columns: dict[str, list[Any]] = {"entity_key": [], **{c: [] for c in out_cols}} - for record in reads.iter_rows(named=True): - row = _compute_row_for(record, plan) - columns["entity_key"].append(row["entity_key"]) - for c in out_cols: - columns[c].append(row[c]) - schema = {"entity_key": pl.Utf8, **{c: pl.Float64 for c in out_cols}} - properties = pl.DataFrame(columns, schema=schema) + properties = pl.DataFrame({"entity_key": series["entity_key"], **{c: series[c] for c in out_cols}}) aa_fraction = pl.DataFrame(schema={"entity_key": pl.Utf8, "aminoAcid": pl.Utf8, "value": pl.Float64}) stats = {"medianCdr3Length": _median_cdr3_length_by_chain(reads, chains)} return { diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 63cc51b..f3602b9 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -241,26 +241,29 @@ def charge_shift( return _mask(hi - lo, sub.valid) -def isoelectric_point( - sub: Substrate, - pka_set: PKaSet, - include_cys: bool = True, +def _bisect_charge_zero_vec( + charge_fn, + valid: np.ndarray, lo: float = 0.0, hi: float = 14.0, tol: float = 1e-3, ) -> np.ndarray: - """Vectorized pI: lockstep fixed-iteration bisection of `_charge_raw` over - all rows, bit-identical to the scalar `properties.isoelectric_point`. + """Lockstep fixed-iteration bisection of a row-wise charge function for the + pH where net charge crosses zero, bit-identical to the scalar + `properties._bisect_charge_zero(charge_fn, lo, hi, tol)`. + + `charge_fn(ph)` must return a finite-for-all-rows float64 array of net charge + at the scalar `ph` (e.g. `_charge_raw` for one substrate, or a per-chain SUM + of `_charge_raw` for the Fv path). `valid` is the per-row validity mask. - Mirrors `properties._bisect_charge_zero(charge_fn, lo, hi, tol)` exactly: + Mirrors the scalar bisection exactly: * SAME bracket [lo, hi] and tol -> the bracket width starts at (hi - lo) and halves every iteration regardless of data, so the loop runs the SAME data-independent `ceil(log2((hi - lo) / tol))` iterations (== 14 for [0, 14], tol 1e-3) for every row; * SAME branch test `(f_mid > 0) == (f_lo > 0)` to move `lo` up to `mid`, - else move `hi` down to `mid`; - * `_charge_raw` is bit-identical to the scalar charge (charge parity = 0.0). + else move `hi` down to `mid`. The only scalar behaviour not reproduced is the `if f_mid == 0.0: return mid` early-out — a midpoint landing on EXACT zero net charge — which is @@ -270,14 +273,12 @@ def isoelectric_point( Rows with no zero crossing in [lo, hi] (both endpoints strictly same-sign) or invalid rows -> NaN, matching the scalar `None`. Returns float64. """ - f = lambda ph: _charge_raw(sub, ph, pka_set, include_cys) # noqa: E731 - - f_lo = f(lo) - f_hi = f(hi) + f_lo = charge_fn(lo) + f_hi = charge_fn(hi) # No zero-crossing when both endpoints share a strict sign (matches scalar). crossing = ~(((f_lo > 0) & (f_hi > 0)) | ((f_lo < 0) & (f_hi < 0))) - n = sub.counts.shape[0] + n = len(valid) lo_a = np.full(n, lo, dtype=np.float64) hi_a = np.full(n, hi, dtype=np.float64) f_lo_a = np.asarray(f_lo, dtype=np.float64).copy() @@ -285,17 +286,72 @@ def isoelectric_point( iters = math.ceil(math.log2((hi - lo) / tol)) for _ in range(iters): mid = 0.5 * (lo_a + hi_a) - f_mid = f(mid) + f_mid = charge_fn(mid) go_lo = (f_mid > 0) == (f_lo_a > 0) # same branch test as scalar lo_a = np.where(go_lo, mid, lo_a) f_lo_a = np.where(go_lo, f_mid, f_lo_a) hi_a = np.where(go_lo, hi_a, mid) pi = 0.5 * (lo_a + hi_a) - pi[~(crossing & sub.valid)] = np.nan + pi[~(crossing & valid)] = np.nan return pi +def isoelectric_point( + sub: Substrate, + pka_set: PKaSet, + include_cys: bool = True, + lo: float = 0.0, + hi: float = 14.0, + tol: float = 1e-3, +) -> np.ndarray: + """Vectorized pI: lockstep fixed-iteration bisection of `_charge_raw` over + all rows, bit-identical to the scalar `properties.isoelectric_point`. + + Delegates the bisection to `_bisect_charge_zero_vec`; `_charge_raw` is + bit-identical to the scalar charge (charge parity = 0.0), so the pI is + bit-identical to the scalar pI for every valid sequence. + """ + return _bisect_charge_zero_vec( + lambda ph: _charge_raw(sub, ph, pka_set, include_cys), + sub.valid, + lo=lo, + hi=hi, + tol=tol, + ) + + +def fv_isoelectric_point( + sub_vh: Substrate, + sub_vl: Substrate, + pka_set: PKaSet, + include_cys: bool = False, + lo: float = 0.0, + hi: float = 14.0, + tol: float = 1e-3, +) -> np.ndarray: + """Vectorized Fv pI: pH where charge(VH, pH) + charge(VL, pH) = 0. + + Mirrors the scalar `properties.fv_isoelectric_point` / + `_compute_fv_row_from_ctx`'s pi: bisect the per-chain charge SUM, not the pI + of a concatenated VH+VL string. The bisected function is + `f(ph) = _charge_raw(vh, ph) + _charge_raw(vl, ph)` — both finite for all + rows — and the row is valid only where BOTH chains are valid (matches the + scalar's "None if either chain invalid"). + + `sub_vh` and `sub_vl` must be aligned row-for-row (same N, same clone order). + Returns float64, NaN where either chain is invalid or no zero crossing. + """ + valid = sub_vh.valid & sub_vl.valid + return _bisect_charge_zero_vec( + lambda ph: _charge_raw(sub_vh, ph, pka_set, include_cys) + _charge_raw(sub_vl, ph, pka_set, include_cys), + valid, + lo=lo, + hi=hi, + tol=tol, + ) + + # --------------------------------------------------------------------------- # # Instability index — the only ORDER-dependent property. The count matrix is # insufficient: it needs the consecutive-residue PAIRS of the cleaned sequence. diff --git a/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv b/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv index 8c39868..6af6262 100644 --- a/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv +++ b/software/tests/data/characterization/golden/antibody_bulk_full.properties.tsv @@ -34,7 +34,7 @@ clone_031 1.04 -1.011 -1.2125 -0.883 -0.875 -0.4375 -3.807 5.021 -0.30847 7068.0 clone_032 2.824 -0.376 -0.7125 0.771 -0.576 -0.92941 4.193 9.413 -0.37424 7528.7168 17085.0 16960.0 35.8336 50.4545 0.15152 2.257 8.813 -0.79286 8284.4152 22710.0 22460.0 47.5986 47.2857 0.12857 6.449 -3.45 9.117 39795.0 39420.0 15813.132 clone_033 1.104 -0.871 -0.075 -0.107 -0.236 -1.96667 2.272 9.254 -0.32131 7374.5451 12865.0 12490.0 43.1705 76.5574 0.11475 -1.688 6.053 -0.57818 6895.8474 36230.0 35980.0 83.2856 63.8182 0.18182 0.584 -3.952 7.689 49095.0 48470.0 14270.3925 clone_034 0.109 -0.88 -2.1 0.814 -0.411 0.48 -1.693 6.041 -1.06066 7365.9152 27960.0 27960.0 50.9525 38.3607 0.18033 1.169 8.606 -0.05574 6975.1083 10470.0 9970.0 46.5984 63.9344 0.11475 -0.524 -3.244 6.673 38430.0 37930.0 14341.0235 -clone_035 1.763 -0.546 -1.61111 -0.101 -0.211 -0.0 -3.812 5.021 -0.38793 6865.7803 21220.0 20970.0 37.6138 65.5172 0.13793 -3.597 5.783 -0.89167 7301.0603 17085.0 16960.0 73.9917 56.8333 0.13333 -7.409 -4.052 5.52 38305.0 37930.0 14166.8406 +clone_035 1.763 -0.546 -1.61111 -0.101 -0.211 0.0 -3.812 5.021 -0.38793 6865.7803 21220.0 20970.0 37.6138 65.5172 0.13793 -3.597 5.783 -0.89167 7301.0603 17085.0 16960.0 73.9917 56.8333 0.13333 -7.409 -4.052 5.52 38305.0 37930.0 14166.8406 clone_036 0.538 -2.131 -2.98333 0.82 -0.423 -0.78462 -2.713 5.708 -0.48214 6945.8559 9065.0 8940.0 36.1589 60.8929 0.17857 2.217 9.407 -0.65 7259.1533 26720.0 26470.0 90.7823 39.3548 0.14516 -0.496 -3.319 6.689 35785.0 35410.0 14205.0092 clone_037 -0.883 -0.875 -0.8 0.891 -0.266 -0.36111 -3.604 5.791 -1.03175 7635.3661 24980.0 24980.0 22.3937 61.9048 0.12698 3.211 9.796 -0.27 8205.5071 11710.0 11460.0 67.7971 61.2857 0.11429 -0.393 -4.164 6.781 36690.0 36440.0 15840.8732 clone_038 -1.26 -2.846 -0.95294 0.117 -0.875 -0.06111 -0.614 6.573 -0.28333 8016.1788 23045.0 22920.0 63.7758 73.7879 0.18182 -3.78 5.19 -0.44366 8510.5249 30605.0 30480.0 21.8014 62.9577 0.15493 -4.393 -4.282 5.923 53650.0 53400.0 16526.7037 diff --git a/software/tests/data/characterization/golden/sc_dropout.properties.tsv b/software/tests/data/characterization/golden/sc_dropout.properties.tsv index 573cd49..7b10c38 100644 --- a/software/tests/data/characterization/golden/sc_dropout.properties.tsv +++ b/software/tests/data/characterization/golden/sc_dropout.properties.tsv @@ -1,7 +1,7 @@ entity_key charge_A_CDR3 chargeShift_A_CDR3 gravy_A_CDR3 charge_B_CDR3 chargeShift_B_CDR3 gravy_B_CDR3 charge_A_VDJRegion pi_A_VDJRegion gravy_A_VDJRegion mw_A_VDJRegion eox_A_VDJRegion ered_A_VDJRegion instability_A_VDJRegion aliphatic_A_VDJRegion aromaticity_A_VDJRegion charge_B_VDJRegion pi_B_VDJRegion gravy_B_VDJRegion mw_B_VDJRegion eox_B_VDJRegion ered_B_VDJRegion instability_B_VDJRegion aliphatic_B_VDJRegion aromaticity_B_VDJRegion charge_Fv chargeShift_Fv pi_Fv eox_Fv ered_Fv mw_Fv clone_000 -0.164 -0.359 -1.66 -5.839 4.476 -0.86154 6292.0732 12740.0 12490.0 54.9596 50.5769 0.09615 clone_001 1.831 -0.359 -1.0375 0.323 -1.497 -0.56364 -2.723 5.714 -1.13 6715.2272 11710.0 11460.0 50.615 34.1667 0.08333 -4.711 5.237 -1.00794 7433.9677 32095.0 31970.0 34.1905 41.746 0.1746 -7.434 -3.836 5.467 43805.0 43430.0 14149.1949 -clone_002 1.899 -0.211 -0.12 2.915 -1.281 -0.0 0.205 7.5 -0.64906 6609.4951 36105.0 35980.0 12.0208 53.3962 0.20755 5.255 9.57 -0.5873 7455.6765 8730.0 8480.0 28.4968 78.8889 0.06349 5.46 -3.364 9.138 44835.0 44460.0 14065.1716 +clone_002 1.899 -0.211 -0.12 2.915 -1.281 0.0 0.205 7.5 -0.64906 6609.4951 36105.0 35980.0 12.0208 53.3962 0.20755 5.255 9.57 -0.5873 7455.6765 8730.0 8480.0 28.4968 78.8889 0.06349 5.46 -3.364 9.138 44835.0 44460.0 14065.1716 clone_003 0.836 -0.368 -2.76 3.032 -1.027 -0.57778 -1.759 5.863 -0.52881 7007.7221 26930.0 26930.0 27.3136 62.7119 0.18644 3.291 9.355 -0.80154 7785.7825 13075.0 12950.0 37.2277 62.9231 0.12308 1.532 -3.666 8.475 40005.0 39880.0 14793.5046 clone_004 -0.116 -0.245 0.75455 -0.849 6.027 -0.07869 6922.9126 22585.0 22460.0 23.718 75.082 0.14754 clone_005 -2.11 -0.292 -1.625 -0.897 -0.947 0.11111 -2.773 5.379 -0.17571 8401.5326 31190.0 30940.0 55.2071 61.2857 0.2 -8.747 4.4 -1.10139 8144.6844 22585.0 22460.0 36.7972 39.3056 0.11111 -11.52 -3.135 4.702 53775.0 53400.0 16546.217 diff --git a/software/tests/data/characterization/golden/tcr_gd.properties.tsv b/software/tests/data/characterization/golden/tcr_gd.properties.tsv index ea84166..940590f 100644 --- a/software/tests/data/characterization/golden/tcr_gd.properties.tsv +++ b/software/tests/data/characterization/golden/tcr_gd.properties.tsv @@ -31,7 +31,7 @@ clone_028 0.545 -2.115 -0.15 -2.227 -0.556 -0.26667 -3.755 5.344 -0.11613 7539.5 clone_029 -0.182 -0.401 0.60714 -1.104 -0.228 0.65 0.18 7.52 -0.10656 7051.1177 11585.0 11460.0 55.7836 73.6066 0.14754 -1.814 5.586 0.24516 7311.7042 18240.0 17990.0 44.0774 72.2581 0.12903 clone_030 -2.095 -0.271 -2.47143 -0.162 -0.38 -0.2375 -3.832 4.575 -0.70179 6444.0944 17085.0 16960.0 51.4696 38.3929 0.14286 0.188 7.619 -0.13793 6540.5065 18115.0 17990.0 64.5621 52.2414 0.12069 clone_031 -0.883 -0.875 -1.46 -0.667 -1.51 0.36875 0.245 7.661 -0.27931 6855.9442 7700.0 7450.0 31.0431 72.2414 0.12069 -1.728 5.977 0.29275 7904.102 17085.0 16960.0 52.9101 87.3913 0.14493 -clone_032 -0.896 -0.88 0.46364 -0.883 -0.875 0.3 -3.777 4.996 -0.0 7181.2529 18700.0 18450.0 63.5018 53.2258 0.16129 0.268 7.557 -0.77895 7140.1348 37720.0 37470.0 48.3561 34.2105 0.21053 +clone_032 -0.896 -0.88 0.46364 -0.883 -0.875 0.3 -3.777 4.996 0.0 7181.2529 18700.0 18450.0 63.5018 53.2258 0.16129 0.268 7.557 -0.77895 7140.1348 37720.0 37470.0 48.3561 34.2105 0.21053 clone_033 -0.899 -0.888 -0.15833 -6.154 -0.455 -1.52 1.245 8.407 -0.72951 7165.1299 20970.0 20970.0 39.3016 80.0 0.11475 -9.683 4.581 -0.94615 7838.6481 16960.0 16960.0 59.5294 37.5385 0.15385 clone_034 0.114 -0.845 0.08333 -0.89 -0.892 -2.275 -3.755 5.187 0.23929 6602.4637 19480.0 19480.0 8.3911 64.2857 0.25 0.283 8.1 0.06034 6916.0456 17210.0 16960.0 62.5621 80.6897 0.13793 clone_035 -1.668 -1.548 0.3 0.327 -1.545 -1.7125 -2.693 5.818 -0.78906 7734.5055 21095.0 20970.0 44.4391 53.2812 0.17188 -5.771 4.619 -0.5 8446.3281 43555.0 43430.0 45.3118 54.2647 0.23529 diff --git a/software/tests/integration/test_perf_benchmark.py b/software/tests/integration/test_perf_benchmark.py new file mode 100644 index 0000000..5a1d03f --- /dev/null +++ b/software/tests/integration/test_perf_benchmark.py @@ -0,0 +1,89 @@ +"""Performance benchmark for the vectorized pipeline (marked slow). + +Generates a large full-paired antibody dataset (all 7 regions on both chains, +Fv requested) and times `run()` end-to-end. The vectorized engine replaces the +per-row BioPython loop; the documented serial baseline is ~6 s / 50k clones on +the dev Mac. The assertion uses a GENEROUS absolute bound so it does not flake +on slow CI — the real speedup goes in the PR / task report, not the gate. + +Run explicitly: + uv run pytest tests/integration/test_perf_benchmark.py -m slow -v +""" + +from __future__ import annotations + +import random +import time + +import polars as pl +import pytest + +from pipeline import run + +# IMGT regions per chain, in reconstruction order. +_REGIONS = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") + +# Realistic-ish per-region length bands (match the corpus generator's bands so +# the timed workload resembles real input shape). +_REGION_LEN = { + "FR1": (7, 10), + "CDR1": (6, 8), + "FR2": (7, 9), + "CDR2": (6, 8), + "FR3": (10, 14), + "CDR3": (5, 18), + "FR4": (7, 9), +} + +_STANDARD_AAS = "ACDEFGHIKLMNPQRSTVWY" + +_N_CLONES = 50_000 +# Generous bound: vectorized path runs well under a second on the dev Mac; +# 3.0 s leaves wide headroom for slow CI without masking a real regression +# (the serial baseline is ~6 s). +_TIME_BUDGET_S = 3.0 + +_PLAN = { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, +} + + +def _rand_seq(rng: random.Random, lo: int, hi: int) -> str: + return "".join(rng.choice(_STANDARD_AAS) for _ in range(rng.randint(lo, hi))) + + +def _make_reads(n: int, seed: int = 0) -> pl.DataFrame: + rng = random.Random(seed) + cols: dict[str, list[str]] = {"entity_key": [f"clone_{i:06d}" for i in range(n)]} + for ch in ("A", "B"): + for feat in _REGIONS: + lo, hi = _REGION_LEN[feat] + cols[f"{ch}_{feat}"] = [_rand_seq(rng, lo, hi) for _ in range(n)] + schema = {c: pl.Utf8 for c in cols} + return pl.DataFrame(cols, schema=schema) + + +@pytest.mark.slow +def test_full_paired_antibody_50k_throughput(capsys): + reads = _make_reads(_N_CLONES) + + start = time.perf_counter() + out = run(reads, _PLAN) + elapsed = time.perf_counter() - start + + # Sanity: every clone produced a row with the full planned column set. + props = out["properties"] + assert props.height == _N_CLONES + us_per_clone = elapsed / _N_CLONES * 1e6 + with capsys.disabled(): + print( + f"\n[perf] vectorized run(): {_N_CLONES} full-paired antibody clones " + f"in {elapsed:.3f}s ({us_per_clone:.2f} us/clone); " + f"serial baseline ~6s/50k -> ~{6.0 / elapsed:.1f}x" + ) + + assert elapsed < _TIME_BUDGET_S, f"50k clones took {elapsed:.3f}s, over the {_TIME_BUDGET_S}s budget" diff --git a/software/tests/unit/test_vectorized_fv_pi.py b/software/tests/unit/test_vectorized_fv_pi.py new file mode 100644 index 0000000..f6a9e21 --- /dev/null +++ b/software/tests/unit/test_vectorized_fv_pi.py @@ -0,0 +1,110 @@ +"""Parity + determinism tests: vectorized Fv isoelectric point vs the scalar oracle. + +The vectorized Fv pI (vectorized.fv_isoelectric_point) bisects the per-chain +charge SUM, mirroring the scalar `properties.fv_isoelectric_point` (and the +Fv-row path in `pipeline._compute_fv_row_from_ctx`) exactly: + +* SAME bracket [0, 14], SAME tol 1e-3 -> SAME data-independent iteration count; +* SAME branch test `(f_mid > 0) == (f_lo > 0)`; +* bisected function is `_charge_raw(vh, ph) + _charge_raw(vl, ph)`, each + bit-identical to the scalar per-chain charge. + +The Fv path always uses the protein pKa set with Cys excluded (IPC2_PROTEIN, +include_cys=False), matching the full-chain rule. + +Two tests: + +* PARITY (Hypothesis): vectorized within 1.5e-3 of scalar over generated VH/VL + pairs (valid + invalid); NaN where scalar None (either chain invalid); float64. +* STRADDLE (deterministic, seeded): over >=1000 seeded random VH/VL pairs, + round(vectorized, 3) == round(scalar, 3) for every pair where the scalar is + not None. Zero straddles. +""" + +from __future__ import annotations + +import math +import random + +import numpy as np +from hypothesis import given +from hypothesis import strategies as st + +import properties as scalar +from aa_tables import STANDARD_AAS +from pka_tables import IPC2_PROTEIN +from vectorized import build_counts, fv_isoelectric_point + +# Mixed valid + invalid sequences so every row exercises one of the branches. +_seq = st.one_of( + st.text(alphabet=STANDARD_AAS, min_size=1, max_size=50), + st.sampled_from(["", "*", "BZXJ"]), +) +pairs = st.lists(st.tuples(_seq, _seq), min_size=1, max_size=25) + + +@given(pairs, st.booleans()) +def test_fv_pi_parity(pairs, inc): + vhs = [vh for vh, _ in pairs] + vls = [vl for _, vl in pairs] + sub_vh = build_counts(vhs) + sub_vl = build_counts(vls) + vec = fv_isoelectric_point(sub_vh, sub_vl, IPC2_PROTEIN, include_cys=inc) + assert vec.dtype == np.float64 + assert vec.shape == (len(pairs),) + for i, (vh, vl) in enumerate(pairs): + # Scalar fv_isoelectric_point hard-codes include_cys=False; mirror its + # cleaning here and bisect the SUM with the requested include_cys so the + # parity holds for both settings. + vh_clean = scalar._prepare(vh) + vl_clean = scalar._prepare(vl) + if vh_clean is None or vl_clean is None: + assert math.isnan(vec[i]) + continue + ip_vh = scalar._ipc2_isoelectric_point(vh_clean, IPC2_PROTEIN, include_cys=inc) + ip_vl = scalar._ipc2_isoelectric_point(vl_clean, IPC2_PROTEIN, include_cys=inc) + exp = scalar._bisect_charge_zero(lambda ph: ip_vh.charge_at_pH(ph) + ip_vl.charge_at_pH(ph)) + if exp is None: + assert math.isnan(vec[i]) + else: + assert abs(vec[i] - exp) <= 1.5e-3 + + +def _seeded_random_sequences(rng: random.Random, n: int) -> list[str]: + out: list[str] = [] + for _ in range(n): + length = rng.randint(20, 130) # realistic VH/VL length band + out.append("".join(rng.choice(STANDARD_AAS) for _ in range(length))) + return out + + +def test_fv_pi_no_straddles(): + """Determinism gate: round(vectorized, 3) == round(scalar, 3) for every pair + where the scalar is not None, across >=1000 seeded random VH/VL pairs under + the Fv config (IPC2_PROTEIN, include_cys=False). Zero straddles. + + Uses the public scalar `properties.fv_isoelectric_point` (the exact oracle + the pipeline replaces) as the reference. + """ + rng = random.Random(0) + vhs = _seeded_random_sequences(rng, 1500) + vls = _seeded_random_sequences(rng, 1500) + + sub_vh = build_counts(vhs) + sub_vl = build_counts(vls) + vec = fv_isoelectric_point(sub_vh, sub_vl, IPC2_PROTEIN, include_cys=False) + assert vec.dtype == np.float64 + + straddles: list[tuple[str, str, float, float]] = [] + compared = 0 + for i, (vh, vl) in enumerate(zip(vhs, vls)): + exp = scalar.fv_isoelectric_point(vh, vl, IPC2_PROTEIN) + if exp is None: + assert math.isnan(vec[i]) + continue + compared += 1 + if round(float(vec[i]), 3) != round(float(exp), 3): + straddles.append((vh, vl, float(vec[i]), float(exp))) + + assert compared >= 1000 + assert straddles == [], f"{len(straddles)} straddle(s) at 3 dp; first: {straddles[0]}" From 4d59c02ca54c01426cfd9e59b682884f73437ec6 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 11:05:55 -0700 Subject: [PATCH 11/21] Add changeset for sequence-properties vectorization --- .changeset/vectorize-sequence-properties.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/vectorize-sequence-properties.md diff --git a/.changeset/vectorize-sequence-properties.md b/.changeset/vectorize-sequence-properties.md new file mode 100644 index 0000000..fec440d --- /dev/null +++ b/.changeset/vectorize-sequence-properties.md @@ -0,0 +1,5 @@ +--- +'@platforma-open/milaboratories.sequence-properties.software': patch +--- + +Vectorize the property computation — single-threaded numpy/polars array math replaces per-row BioPython. ~5x faster at 50k clones; output unchanged within the quantized-equal contract (signed-zero canonicalized). From 7dd77cb27e9a35f6eb30904097196a2368466c3b Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 11:25:21 -0700 Subject: [PATCH 12/21] Vectorize build_counts + full-chain reconstruction (perf) --- software/src/pipeline.py | 40 ++--- software/src/vectorized.py | 158 +++++++++++++++--- .../tests/integration/test_perf_benchmark.py | 9 +- 3 files changed, 155 insertions(+), 52 deletions(-) diff --git a/software/src/pipeline.py b/software/src/pipeline.py index 3109e99..3054733 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -267,20 +267,6 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: REQUIRED_FEATURES = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") -def _reconstruct_chain(row: dict[str, str], chain: str) -> str | None: - """Concatenate FR1+CDR1+FR2+CDR2+FR3+CDR3+FR4. Returns None if any - region is missing (empty string in input). - """ - parts = [] - for feat in REQUIRED_FEATURES: - col = f"{chain}_{feat}" - v = row.get(col, "") - if not v: - return None - parts.append(v) - return "".join(parts) - - def _planned_output_columns(plan: dict[str, Any]) -> list[str]: """Output column order — matches process.tpl.tengo's xsv import expectations. @@ -336,17 +322,23 @@ def _reconstruct_chain_column(reads: pl.DataFrame, chain: str) -> list[str | Non """Reconstruct the full chain for every clone: FR1+CDR1+...+FR4, None if any region is missing (empty cell OR absent column). - Pulls each region column once, then delegates the per-clone rule to - `_reconstruct_chain` so the reconstruction logic has a single source of - truth. A region column absent from the reads is materialised as empty - strings, so the missing-region -> None rule fires the same way. + Vectorized with polars: each region is normalised so that both null and the + empty string map to null (the `if not v` "missing region" rule), then + `concat_str(ignore_nulls=False)` joins the seven regions — yielding null + whenever ANY region is null/empty (any-missing -> None) without a per-row + Python loop. A region column absent from the reads is treated as all-empty, + so its rows reconstruct to None, same as the per-row path. """ - cols = {feat: _column_or_empty(reads, f"{chain}_{feat}") for feat in REQUIRED_FEATURES} - out: list[str | None] = [] - for i in range(reads.height): - row = {f"{chain}_{feat}": cols[feat][i] for feat in REQUIRED_FEATURES} - out.append(_reconstruct_chain(row, chain)) - return out + parts = [] + for feat in REQUIRED_FEATURES: + col = f"{chain}_{feat}" + if col not in reads.columns: + # Absent column -> every row missing this region -> chain is None. + return [None] * reads.height + # Empty string counts as missing (== None for the any-missing rule). + parts.append(pl.when(pl.col(col) == "").then(None).otherwise(pl.col(col))) + reconstructed = reads.select(pl.concat_str(parts, ignore_nulls=False).alias("_chain"))["_chain"] + return reconstructed.to_list() def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any]: diff --git a/software/src/vectorized.py b/software/src/vectorized.py index f3602b9..1712c33 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -16,6 +16,18 @@ _AA_INDEX = {aa: i for i, aa in enumerate(STANDARD_AAS)} +# byte -> AA index lookup (256 entries, int8). Non-standard bytes stay at -1. +# Both upper- and lower-case map to the same index so the table reproduces the +# scalar `.upper()` + standard-AA filter in a single numpy gather. latin-1 +# encoding with errors="replace" keeps exactly one byte per Python char (so +# per-char offsets stay aligned) and maps any non-latin-1 char to byte 0x3F +# ('?'), which the table leaves at -1 — i.e. dropped, like any non-standard +# residue. See build_counts for how the table is applied. +_BYTE_TO_AA = np.full(256, -1, dtype=np.int8) +for _i, _aa in enumerate(STANDARD_AAS): + _BYTE_TO_AA[ord(_aa)] = _i + _BYTE_TO_AA[ord(_aa.lower())] = _i + # --------------------------------------------------------------------------- # # Constant vectors in STANDARD_AAS order, sourced verbatim from BioPython so # the vectorized math uses the SAME numbers the scalar oracle (properties.py) @@ -87,21 +99,104 @@ class Substrate: valid: np.ndarray # (N,) bool -def build_counts(seqs: list[str | None]) -> Substrate: +@dataclass(frozen=True) +class _Cleaned: + """Vectorized cleaning intermediate, shared by build_counts and + instability_index. Encodes every candidate sequence (not None/empty/no `*`) + as one flat AA-index buffer with row boundaries, so per-char Python loops + are replaced by a single numpy gather over the joined bytes. + + * `cand_rows` — (C,) original row index of each candidate (into the full N). + * `aa_flat` — (T,) int8 AA index per *standard* residue, concatenated in + candidate then residue order (non-standard chars dropped). + * `row_of_res` — (T,) candidate-local row id (0..C-1) for each entry of + aa_flat — i.e. which candidate that residue belongs to. + * `lengths` — (C,) standard-residue count per candidate (== len of its + slice of aa_flat). + """ + + n: int + cand_rows: np.ndarray + aa_flat: np.ndarray + row_of_res: np.ndarray + lengths: np.ndarray + + +def _clean_vectorized(seqs: list[str | None]) -> _Cleaned: + """Clean a column of sequences in one numpy pass. + + Candidate = not None, not "", and no stop codon `*` (matches the scalar + `is_invalid_sequence` short-circuit). Each candidate is latin-1 encoded + (one byte per char), the bytes are concatenated into a single buffer, + `np.frombuffer`'d once, and mapped through `_BYTE_TO_AA`; standard residues + keep their AA index, non-standard chars (mapped to -1) are dropped. A + per-char candidate-local row id is built with `np.repeat`, then masked to + the kept residues — so `aa_flat[k]` belongs to candidate `row_of_res[k]`. + """ n = len(seqs) - counts = np.zeros((n, 20), dtype=np.int64) - valid = np.zeros(n, dtype=bool) + cand_rows: list[int] = [] + buf = bytearray() + char_lengths: list[int] = [] # per-candidate char count (== byte count) for i, s in enumerate(seqs): if s is None or s == "" or "*" in s: continue - row = counts[i] - any_std = False - for c in s.upper(): - j = _AA_INDEX.get(c) - if j is not None: - row[j] += 1 - any_std = True - valid[i] = any_std + b = s.encode("latin-1", "replace") + cand_rows.append(i) + char_lengths.append(len(b)) + buf += b + + cand_rows_arr = np.asarray(cand_rows, dtype=np.intp) + if not buf: + empty_i = np.empty(0, dtype=np.intp) + empty_aa = np.empty(0, dtype=np.int8) + return _Cleaned( + n=n, + cand_rows=cand_rows_arr, + aa_flat=empty_aa, + row_of_res=empty_i, + lengths=np.zeros(len(cand_rows_arr), dtype=np.int64), + ) + + char_len_arr = np.asarray(char_lengths, dtype=np.intp) + # AA index per character of the joined buffer; non-standard chars -> -1. + aa_per_char = _BYTE_TO_AA[np.frombuffer(bytes(buf), dtype=np.uint8)] + # Candidate-local row id per character (0..C-1), then drop non-standard. + row_per_char = np.repeat(np.arange(len(char_len_arr), dtype=np.intp), char_len_arr) + keep = aa_per_char >= 0 + aa_flat = aa_per_char[keep] + row_of_res = row_per_char[keep] + # Standard-residue count per candidate (folds the kept mask back per row). + lengths = np.bincount(row_of_res, minlength=len(char_len_arr)).astype(np.int64) + return _Cleaned( + n=n, + cand_rows=cand_rows_arr, + aa_flat=aa_flat, + row_of_res=row_of_res, + lengths=lengths, + ) + + +def build_counts(seqs: list[str | None]) -> Substrate: + """Per-residue (N, 20) count matrix + effective length + validity, matching + properties.py's scalar cleaning exactly. + + Fully vectorized: candidates are cleaned to one flat AA-index buffer + (`_clean_vectorized`), counts are scattered with a single `np.add.at` over + the (candidate-local-row, AA-index) pairs, then mapped back to the full N + rows. A candidate is valid iff at least one standard residue remained + (lengths > 0) — reproducing the scalar `clean_sequence(...) or None` rule. + """ + cl = _clean_vectorized(seqs) + n = cl.n + counts = np.zeros((n, 20), dtype=np.int64) + valid = np.zeros(n, dtype=bool) + n_cand = len(cl.cand_rows) + if n_cand: + cand_counts = np.zeros((n_cand, 20), dtype=np.int64) + if cl.aa_flat.size: + np.add.at(cand_counts, (cl.row_of_res, cl.aa_flat.astype(np.intp)), 1) + counts[cl.cand_rows] = cand_counts + valid[cl.cand_rows] = cl.lengths > 0 length = counts.sum(axis=1) return Substrate(counts=counts, length=length, valid=valid) @@ -378,17 +473,32 @@ def instability_index(seqs: list[str | None]) -> np.ndarray: preserved: each row's value is a pure function of that row's sequence, with no dict/set iteration leaking into the output. """ - n = len(seqs) - out = np.full(n, np.nan, dtype=np.float64) - for i, s in enumerate(seqs): - if s is None or s == "" or "*" in s: - continue - idx = [j for c in s.upper() if (j := _AA_INDEX.get(c)) is not None] - length = len(idx) - if length < _INSTABILITY_MIN_LENGTH: - # Includes the "nothing standard remains" case (length == 0). - continue - a = np.asarray(idx, dtype=np.intp) - score = _DIWV[a[:-1], a[1:]].sum() - out[i] = (10.0 / length) * score + cl = _clean_vectorized(seqs) + out = np.full(cl.n, np.nan, dtype=np.float64) + n_cand = len(cl.cand_rows) + if n_cand == 0: + return out + + # Consecutive cleaned residues k, k+1 form a dipeptide only when they belong + # to the SAME candidate (row boundaries in the joined buffer must not leak a + # pair across two sequences). Mask on row_of_res, gather DIWV weights, then + # sum per candidate with np.add.at — order-preserving, no per-row Python. + aa = cl.aa_flat.astype(np.intp) + rows = cl.row_of_res + if aa.size >= 2: + same = rows[:-1] == rows[1:] + left = aa[:-1][same] + right = aa[1:][same] + pair_row = rows[:-1][same] + weights = _DIWV[left, right] + score = np.zeros(n_cand, dtype=np.float64) + np.add.at(score, pair_row, weights) + else: + score = np.zeros(n_cand, dtype=np.float64) + + lengths = cl.lengths + keep = lengths >= _INSTABILITY_MIN_LENGTH # excludes length 0 ("nothing standard") + if keep.any(): + rows_keep = cl.cand_rows[keep] + out[rows_keep] = (10.0 / lengths[keep]) * score[keep] return out diff --git a/software/tests/integration/test_perf_benchmark.py b/software/tests/integration/test_perf_benchmark.py index 5a1d03f..74921bb 100644 --- a/software/tests/integration/test_perf_benchmark.py +++ b/software/tests/integration/test_perf_benchmark.py @@ -38,10 +38,11 @@ _STANDARD_AAS = "ACDEFGHIKLMNPQRSTVWY" _N_CLONES = 50_000 -# Generous bound: vectorized path runs well under a second on the dev Mac; -# 3.0 s leaves wide headroom for slow CI without masking a real regression -# (the serial baseline is ~6 s). -_TIME_BUDGET_S = 3.0 +# Generous bound: after vectorizing build_counts + instability + full-chain +# reconstruction the path runs ~0.4 s / 50k on the dev Mac (~14x over the ~6 s +# serial baseline). 1.5 s keeps ~3x headroom for slow CI without masking a real +# regression — a return toward the per-row Python loop would blow well past it. +_TIME_BUDGET_S = 1.5 _PLAN = { "mode": "antibody_tcr_legacy_bulk", From a971f7d6175b6a4b912436381501babbe4a71715 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 11:56:23 -0700 Subject: [PATCH 13/21] Adopt pyproject-as-source-of-truth deps model with generated requirements.txt + CI sync (per titeseq) --- .github/workflows/python-tests.yaml | 37 +++++++++++ software/package.json | 1 + software/pyproject.toml | 15 +++-- software/scripts/deps-export.sh | 10 +++ software/src/requirements.txt | 6 +- software/uv.lock | 95 +++++++++++++---------------- 6 files changed, 105 insertions(+), 59 deletions(-) create mode 100755 software/scripts/deps-export.sh diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml index 3cff09b..b74e8c1 100644 --- a/.github/workflows/python-tests.yaml +++ b/.github/workflows/python-tests.yaml @@ -23,6 +23,7 @@ jobs: - uses: astral-sh/setup-uv@v5 with: + version: "0.11.16" enable-cache: true cache-dependency-glob: software/uv.lock @@ -37,3 +38,39 @@ jobs: - name: Run pytest run: uv run pytest + + requirements-sync: + # src/requirements.txt is generated from pyproject.toml (the single source of + # truth) and is what pl-pkg builds from. This job fails if the committed file + # has drifted from pyproject — e.g. someone changed a dependency but forgot to + # regenerate. Fix locally with `pnpm deps:export` (see software/package.json). + if: github.repository != 'milaboratory/platforma-sequence-properties' + runs-on: ubuntu-latest + defaults: + run: + working-directory: software + steps: + - uses: actions/checkout@v4 + + - uses: astral-sh/setup-uv@v5 + with: + version: "0.11.16" + enable-cache: true + cache-dependency-glob: software/uv.lock + + - name: Regenerate requirements.txt from pyproject.toml + # Same script `pnpm deps:export` runs locally — single source of truth. + run: bash scripts/deps-export.sh + + - name: Verify requirements.txt is in sync with pyproject.toml + run: | + if ! git diff --exit-code -- src/requirements.txt; then + echo "" + echo "::error::src/requirements.txt is out of sync with pyproject.toml" + echo "Dependencies are defined in software/pyproject.toml; src/requirements.txt is generated." + echo "Resolve locally with:" + echo " cd software && pnpm deps:export" + echo "then commit the updated src/requirements.txt." + exit 1 + fi + echo "src/requirements.txt is in sync with pyproject.toml." diff --git a/software/package.json b/software/package.json index 74203df..01b3ae6 100644 --- a/software/package.json +++ b/software/package.json @@ -8,6 +8,7 @@ "changeset": "changeset", "version-packages": "changeset version", "build": "pl-pkg build", + "deps:export": "bash scripts/deps-export.sh", "prepublishOnly": "pl-pkg prepublish" }, "block-software": { diff --git a/software/pyproject.toml b/software/pyproject.toml index 1fc1484..831c4d6 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -2,13 +2,20 @@ name = "sequence-properties-calc-script" version = "0.0.0" requires-python = ">=3.12.0, <3.13" +# Runtime dependencies — single source of truth. These exact pins are exported to +# src/requirements.txt (top-level only) via `pnpm deps:export`, which is what pl-pkg +# builds from and what the scientific-slim runenv installs offline. Keep versions +# aligned with the wheels shipped by the runenv. +dependencies = [ + "polars-lts-cpu==1.33.1", + "numpy==2.2.6", + "pyarrow==21.0.0", + "biopython==1.87", +] [dependency-groups] +# Dev-only tooling (not shipped). Runtime libs live in [project.dependencies] above. dev = [ - "polars>=1.39.0", - "numpy>=2.0.0", - "pyarrow>=18.0.0", - "biopython>=1.84", "pytest>=9.0.2", "pytest-cov>=6.0.0", "ruff>=0.7.0", diff --git a/software/scripts/deps-export.sh b/software/scripts/deps-export.sh new file mode 100755 index 0000000..35eef44 --- /dev/null +++ b/software/scripts/deps-export.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash +# Regenerate src/requirements.txt from pyproject.toml (the single source of truth). +# One definition, shared by `pnpm deps:export` and the requirements-sync CI job. +# +# --no-deps: top-level pins only. The runenv supplies transitive deps for the +# offline (--no-index) install, so requirements.txt must not pin a full closure. +set -euo pipefail +cd "$(dirname "$0")/.." +uv pip compile pyproject.toml --no-deps --no-annotate \ + --custom-compile-command "pnpm deps:export" -o src/requirements.txt diff --git a/software/src/requirements.txt b/software/src/requirements.txt index f3fe977..0a58767 100644 --- a/software/src/requirements.txt +++ b/software/src/requirements.txt @@ -1,4 +1,6 @@ -polars-lts-cpu==1.33.1 +# This file was autogenerated by uv via the following command: +# pnpm deps:export +biopython==1.87 numpy==2.2.6 +polars-lts-cpu==1.33.1 pyarrow==21.0.0 -biopython==1.87 diff --git a/software/uv.lock b/software/uv.lock index c14ddad..ff08eab 100644 --- a/software/uv.lock +++ b/software/uv.lock @@ -74,21 +74,20 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.4" +version = "2.2.6" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, - { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, - { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, - { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, - { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, - { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, - { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, - { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, - { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, ] [[package]] @@ -110,46 +109,32 @@ wheels = [ ] [[package]] -name = "polars" -version = "1.40.1" +name = "polars-lts-cpu" +version = "1.33.1" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "polars-runtime-32" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b3/8c/bc9bc948058348ed43117cecc3007cd608f395915dae8a00974579a5dab1/polars-1.40.1.tar.gz", hash = "sha256:ab2694134b137596b5a59bfd7b4c54ebbc9b59f9403127f18e32d363777552e8", size = 733574, upload-time = "2026-04-22T19:15:55.507Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ea/91/74fc60d94488685a92ac9d49d7ec55f3e91fe9b77942a6235a5fa7f249c3/polars-1.40.1-py3-none-any.whl", hash = "sha256:c0f861219d1319cdea45c4ce4d30355a47176b8f98dcedf95ea8269f131b8abd", size = 828723, upload-time = "2026-04-22T19:14:25.452Z" }, -] - -[[package]] -name = "polars-runtime-32" -version = "1.40.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/54/ba/26d40f039be9f552b5fd7365a621bdfc0f8e912ef77094ae4693491b0bae/polars_runtime_32-1.40.1.tar.gz", hash = "sha256:37f3065615d1bf90d03b5326222df4c5c1f8a5d33e50470aa588e3465e6eb814", size = 2935843, upload-time = "2026-04-22T19:15:57.26Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f4/93/a0c4200a5e0af2eee31ea79330cb1f5f4c58f604cb3de352f654e2010c81/polars_lts_cpu-1.33.1.tar.gz", hash = "sha256:0a5426d95ec9eec937a56d3e7cf7911a4b5486c42f4dbbcc9512aa706039322c", size = 4822741, upload-time = "2025-09-09T08:37:51.491Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7d/46/22c8af5eed68ac2eeb556e0fa3ca8a7b798e984ceff4450888f3b5ac61fd/polars_runtime_32-1.40.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:b748ef652270cc49e9e69f99a035e0eb4d5f856d42bcd6ac4d9d80a40142aa1e", size = 52098755, upload-time = "2026-04-22T19:14:28.555Z" }, - { url = "https://files.pythonhosted.org/packages/c6/3e/48599a38009ca60ff82a6f38c8a621ce3c0286aa7397c7d79e741bd9060e/polars_runtime_32-1.40.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:d249b3743e05986060cec0a7aaa542d020df6c6b876e556023a310efd581f9be", size = 46367542, upload-time = "2026-04-22T19:14:32.433Z" }, - { url = "https://files.pythonhosted.org/packages/43/e9/384bc069367a1a36ee31c13782c178dbd039b2b873b772d4a0fc23a2373d/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5987b30e7aa1059d069498496e8dda35afd592b0ac3d46ed87e3ff8df1ad652c", size = 50252104, upload-time = "2026-04-22T19:14:35.945Z" }, - { url = "https://files.pythonhosted.org/packages/15/ef/7d57ceb0651af74194e97ed6583e148d352f03d696090221b8059cdfc90b/polars_runtime_32-1.40.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d7f42a8b3f16fc66002cc0f6516f7dd7653396886ae0ed362ab95c0b3408b59", size = 56250788, upload-time = "2026-04-22T19:14:39.743Z" }, - { url = "https://files.pythonhosted.org/packages/10/0f/e4b3ffc748827a14a474ec9c42e45c066050e440fec57e914091d9adda75/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e5f7becc237a7ec9d9a10878dc8e54b73bbf4e2d94a2991c37d7a0b38590d8f9", size = 50432590, upload-time = "2026-04-22T19:14:43.388Z" }, - { url = "https://files.pythonhosted.org/packages/d9/0b/b8d95fbed869fa4caabe9c400e4210374913b376e925e96fdcfa9be6416b/polars_runtime_32-1.40.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:992d14cf191dde043d36fbdbc98a65e43fbc7e9a5024cecd45f838ac4988c1ee", size = 54155564, upload-time = "2026-04-22T19:14:47.239Z" }, - { url = "https://files.pythonhosted.org/packages/06/d9/d091d8fb5cbed5e9536adfed955c4c89987a4cc3b8e73ae4532402b91c74/polars_runtime_32-1.40.1-cp310-abi3-win_amd64.whl", hash = "sha256:f78bb2abd00101cbb23cc0cb068f7e36e081057a15d2ec2dde3dda280709f030", size = 51829755, upload-time = "2026-04-22T19:14:50.85Z" }, - { url = "https://files.pythonhosted.org/packages/65/ad/b33c3022a394f3eb55c3310597cec615412a8a33880055eee191d154a628/polars_runtime_32-1.40.1-cp310-abi3-win_arm64.whl", hash = "sha256:b5cbfaf6b085b420b4bfcbe24e8f665076d1cccfdb80c0484c02a023ce205537", size = 45822104, upload-time = "2026-04-22T19:14:54.192Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9b/75916636b33724afabe820b0993f60dc243793421d6f680d5fcb531fe170/polars_lts_cpu-1.33.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:5db75d1b424bd8aa34c9a670a901592f1931cc94d9fb32bdd428dbaad8c33761", size = 38908638, upload-time = "2025-09-09T08:37:02.258Z" }, + { url = "https://files.pythonhosted.org/packages/81/e2/dc77b81650ba0c631c06f05d8e81faacee87730600fceca372273facf77b/polars_lts_cpu-1.33.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:37cf3a56cf447c69cfb3f9cd0e714d5b0c754705d7b497b9ab86cbf56e36b3e7", size = 35638895, upload-time = "2025-09-09T08:37:07.575Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/4dcff801d71dfa02ec682d6b32fd0ce5339de48797f663698d5f8348ffe7/polars_lts_cpu-1.33.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:656b530a672fe8fbd4c212b2a8481099e5cef63e84970975619ea7c25faeb833", size = 39585825, upload-time = "2025-09-09T08:37:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/0474c14dce2c0507bea40069daafb848980ba7c351ad991908e51ac895fb/polars_lts_cpu-1.33.1-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:64574c784380b37167b3db3a7cfdb9839cd308e89b8818859d2ffb34a9c896b2", size = 36685020, upload-time = "2025-09-09T08:37:15.597Z" }, + { url = "https://files.pythonhosted.org/packages/d4/0a/5ebba9b145388ffbbd09fa84ac3cd7d336b922e34256b1417abf0a1c2fb9/polars_lts_cpu-1.33.1-cp39-abi3-win_amd64.whl", hash = "sha256:6b849e0e1485acb8ac39bf13356d280ea7c924c2b41cd548ea6e4d102d70be77", size = 39191650, upload-time = "2025-09-09T08:37:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ad/bf3db68d30ac798ca31c80624709a0c03aa890e2e20e5ca987d7e55fcfc2/polars_lts_cpu-1.33.1-cp39-abi3-win_arm64.whl", hash = "sha256:c99ab56b059cee6bcabe9fb89e97f5813be1012a2251bf77f76e15c2d1cba934", size = 35445244, upload-time = "2025-09-09T08:37:22.97Z" }, ] [[package]] name = "pyarrow" -version = "24.0.0" +version = "21.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/c2/ea068b8f00905c06329a3dfcd40d0fcc2b7d0f2e355bdb25b65e0a0e4cd4/pyarrow-21.0.0.tar.gz", hash = "sha256:5051f2dccf0e283ff56335760cbc8622cf52264d67e359d5569541ac11b6d5bc", size = 1133487, upload-time = "2025-07-18T00:57:31.761Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, - { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, - { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, - { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, - { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/ca/d4/d4f817b21aacc30195cf6a46ba041dd1be827efa4a623cc8bf39a1c2a0c0/pyarrow-21.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3a302f0e0963db37e0a24a70c56cf91a4faa0bca51c23812279ca2e23481fccd", size = 31160305, upload-time = "2025-07-18T00:55:35.373Z" }, + { url = "https://files.pythonhosted.org/packages/a2/9c/dcd38ce6e4b4d9a19e1d36914cb8e2b1da4e6003dd075474c4cfcdfe0601/pyarrow-21.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:b6b27cf01e243871390474a211a7922bfbe3bda21e39bc9160daf0da3fe48876", size = 32684264, upload-time = "2025-07-18T00:55:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/2a2d9f8d7a59b639523454bec12dba35ae3d0a07d8ab529dc0809f74b23c/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e72a8ec6b868e258a2cd2672d91f2860ad532d590ce94cdf7d5e7ec674ccf03d", size = 41108099, upload-time = "2025-07-18T00:55:42.889Z" }, + { url = "https://files.pythonhosted.org/packages/ad/90/2660332eeb31303c13b653ea566a9918484b6e4d6b9d2d46879a33ab0622/pyarrow-21.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b7ae0bbdc8c6674259b25bef5d2a1d6af5d39d7200c819cf99e07f7dfef1c51e", size = 42829529, upload-time = "2025-07-18T00:55:47.069Z" }, + { url = "https://files.pythonhosted.org/packages/33/27/1a93a25c92717f6aa0fca06eb4700860577d016cd3ae51aad0e0488ac899/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:58c30a1729f82d201627c173d91bd431db88ea74dcaa3885855bc6203e433b82", size = 43367883, upload-time = "2025-07-18T00:55:53.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/4d09d919f35d599bc05c6950095e358c3e15148ead26292dfca1fb659b0c/pyarrow-21.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:072116f65604b822a7f22945a7a6e581cfa28e3454fdcc6939d4ff6090126623", size = 45133802, upload-time = "2025-07-18T00:55:57.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/30/f3795b6e192c3ab881325ffe172e526499eb3780e306a15103a2764916a2/pyarrow-21.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:cf56ec8b0a5c8c9d7021d6fd754e688104f9ebebf1bf4449613c9531f5346a18", size = 26203175, upload-time = "2025-07-18T00:56:01.364Z" }, ] [[package]] @@ -220,28 +205,32 @@ wheels = [ name = "sequence-properties-calc-script" version = "0.0.0" source = { virtual = "." } +dependencies = [ + { name = "biopython" }, + { name = "numpy" }, + { name = "polars-lts-cpu" }, + { name = "pyarrow" }, +] [package.dev-dependencies] dev = [ - { name = "biopython" }, { name = "hypothesis" }, - { name = "numpy" }, - { name = "polars" }, - { name = "pyarrow" }, { name = "pytest" }, { name = "pytest-cov" }, { name = "ruff" }, ] [package.metadata] +requires-dist = [ + { name = "biopython", specifier = "==1.87" }, + { name = "numpy", specifier = "==2.2.6" }, + { name = "polars-lts-cpu", specifier = "==1.33.1" }, + { name = "pyarrow", specifier = "==21.0.0" }, +] [package.metadata.requires-dev] dev = [ - { name = "biopython", specifier = ">=1.84" }, { name = "hypothesis", specifier = ">=6.155.2" }, - { name = "numpy", specifier = ">=2.0.0" }, - { name = "polars", specifier = ">=1.39.0" }, - { name = "pyarrow", specifier = ">=18.0.0" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-cov", specifier = ">=6.0.0" }, { name = "ruff", specifier = ">=0.7.0" }, From bfe4e57c7bba8860e7c749d795c4839349546dc2 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 12:07:47 -0700 Subject: [PATCH 14/21] Address code-review findings: public-oracle Fv test, unified signed-zero canonicalization, sync-guard + signed-zero tests, comment/log-token fixes --- software/src/pipeline.py | 60 +++++++++++-------- software/src/vectorized.py | 6 +- software/tests/integration/test_cli.py | 2 +- software/tests/unit/test_pipeline.py | 7 ++- software/tests/unit/test_quantization.py | 12 ++++ software/tests/unit/test_vectorized_fv_pi.py | 23 +++---- .../tests/unit/test_vectorized_substrate.py | 16 ++++- 7 files changed, 77 insertions(+), 49 deletions(-) diff --git a/software/src/pipeline.py b/software/src/pipeline.py index 3054733..fcd4511 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -89,26 +89,33 @@ def _decimals_for(col: str) -> int | None: return None +def _round_and_canonicalize_zero(col: str, dp: int) -> pl.Expr: + """Round `col` to `dp` decimals, then canonicalize signed zero to `+0.0`. + + This is the single definition of "how an emitted float is made CID-stable", + shared by the property frame (`_quantize_for_cid`) and the aa_fraction + `value` column. A property whose true value is ~0 (e.g. GRAVY of a + charge-balanced chain) lands on a sub-ULP residual whose SIGN depends on + summation order — the scalar BioPython path sums in residue order, the + vectorized `counts @ KD` in AA-index order, so the same numeric zero rounds + to `-0.0` on one and `+0.0` on the other. Both are numerically equal, but the + TSV writer emits different bytes (`-0.0` vs `0.0`). `-0.0 == 0.0` is True in + polars, so the `when` maps BOTH signed zeros to `+0.0` (note: `col + 0.0` + does NOT canonicalize in polars — it preserves the negative-zero bit). This + makes the content-addressable id insensitive to FP-residual-sign drift — the + same determinism guarantee the rounding already gives the other digits. + round(null) is null and `null == 0.0` is null, so NA cells stay null/empty. + """ + rounded = pl.col(col).round(dp) + return pl.when(rounded == 0.0).then(pl.lit(0.0)).otherwise(rounded).alias(col) + + def _quantize_for_cid(df: pl.DataFrame) -> pl.DataFrame: - exprs = [] - for c in df.columns: - dp = _decimals_for(c) - if dp is not None and df.schema[c] == pl.Float64: - # Round, then canonicalize signed zero to `+0.0`. A property whose - # true value is ~0 (e.g. GRAVY of a charge-balanced chain) lands on a - # sub-ULP residual whose SIGN depends on summation order — the scalar - # BioPython path sums in residue order, the vectorized `counts @ KD` - # in AA-index order, so the same numeric zero rounds to `-0.0` on one - # and `+0.0` on the other. Both are numerically equal, but the TSV - # writer emits different bytes (`-0.0` vs `0.0`). `-0.0 == 0.0` is - # True in polars, so the `when` maps BOTH signed zeros to `+0.0` - # (note: `col + 0.0` does NOT canonicalize in polars — it preserves - # the negative-zero bit). This makes the content-addressable id - # insensitive to FP-residual-sign drift — the same determinism - # guarantee the rounding already gives the other digits. round(null) - # is null and `null == 0.0` is null, so NA cells stay null/empty. - rounded = pl.col(c).round(dp) - exprs.append(pl.when(rounded == 0.0).then(pl.lit(0.0)).otherwise(rounded).alias(c)) + exprs = [ + _round_and_canonicalize_zero(c, dp) + for c in df.columns + if (dp := _decimals_for(c)) is not None and df.schema[c] == pl.Float64 + ] return df.with_columns(exprs) if exprs else df @@ -220,10 +227,13 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: "value": _na_to_null(aa_value), } ) - # Quantize at the output boundary like the property columns. aaFraction - # displays as .3f; round to 5 dp (one digit below display, headroom for the - # vectorization's FP drift). round(null) == null, so NA cells stay empty. - aa_fraction = aa_fraction.with_columns(pl.col("value").round(5)) + # Quantize at the output boundary like the property columns, through the + # shared CID-stability helper. aaFraction displays as .3f; round to 5 dp (one + # digit below display, headroom for the vectorization's FP drift). Fractions + # are >=0 so the signed-zero canonicalization is a no-op here, but routing + # through the same helper keeps "how an emitted float is made CID-stable" in + # one place. + aa_fraction = aa_fraction.with_columns(_round_and_canonicalize_zero("value", 5)) # R9 — flag whether any *real* peptide falls below the Instability Index # floor. `if s` filters None / "" so the banner does not fire on empty @@ -411,7 +421,9 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any # Fv — per-chain sums over VH=A, VL=B. charge/chargeShift/eox/ered/mw are # additive (NaN propagates if either chain invalid); pi bisects the SUMMED # charge function. Reuses the cached full-chain substrates so the chains are - # reconstructed/counted once. Mirrors scalar `_compute_fv_row_from_ctx`. + # reconstructed/counted once. Mirrors the scalar Fv anchors: + # `properties.fv_isoelectric_point` (pi) and the additive `properties.fv_charge` + # / `properties.fv_molecular_weight` / `properties.fv_extinction_coefficients`. if plan.get("hasFv"): sub_vh = chain_subs["A"] sub_vl = chain_subs["B"] diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 1712c33..02a2654 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -427,9 +427,9 @@ def fv_isoelectric_point( ) -> np.ndarray: """Vectorized Fv pI: pH where charge(VH, pH) + charge(VL, pH) = 0. - Mirrors the scalar `properties.fv_isoelectric_point` / - `_compute_fv_row_from_ctx`'s pi: bisect the per-chain charge SUM, not the pI - of a concatenated VH+VL string. The bisected function is + Mirrors the scalar `properties.fv_isoelectric_point`: bisect the per-chain + charge SUM, not the pI of a concatenated VH+VL string. The bisected function + is `f(ph) = _charge_raw(vh, ph) + _charge_raw(vl, ph)` — both finite for all rows — and the row is valid only where BOTH chains are valid (matches the scalar's "None if either chain invalid"). diff --git a/software/tests/integration/test_cli.py b/software/tests/integration/test_cli.py index 1114d6d..dfabc1b 100644 --- a/software/tests/integration/test_cli.py +++ b/software/tests/integration/test_cli.py @@ -386,4 +386,4 @@ def test_cli_writes_progress_to_stderr(tmp_path: Path, capsys): assert rc == 0 captured = capsys.readouterr() assert "peptide" in captured.err.lower(), captured.err - assert "scalar" in captured.err.lower() or "properties" in captured.err.lower(), captured.err + assert "properties" in captured.err.lower(), captured.err diff --git a/software/tests/unit/test_pipeline.py b/software/tests/unit/test_pipeline.py index 61ff3f5..1b9b9dc 100644 --- a/software/tests/unit/test_pipeline.py +++ b/software/tests/unit/test_pipeline.py @@ -545,13 +545,14 @@ def test_run_logs_peptide_mode_dispatch(self, caplog: pytest.LogCaptureFixture): run(reads, {"mode": "peptide"}) assert any("peptide" in r.message.lower() for r in caplog.records), caplog.text - def test_peptide_path_logs_scalar_and_aa_milestones(self, caplog: pytest.LogCaptureFixture): + def test_peptide_path_logs_properties_and_aa_milestones(self, caplog: pytest.LogCaptureFixture): reads = pl.DataFrame({"entity_key": ["p1"], "sequence": ["ACDEFGHIKL"]}) with caplog.at_level(logging.INFO, logger="pipeline"): run(reads, {"mode": "peptide"}) text = caplog.text.lower() - # One milestone for scalar properties, one for AA fractions. - assert "scalar" in text or "properties" in text, caplog.text + # The peptide path logs one milestone covering both properties and AA + # fractions ("Computing peptide properties + AA fractions"). + assert "properties" in text, caplog.text assert "aa" in text or "amino" in text or "fraction" in text, caplog.text def test_run_logs_antibody_mode_dispatch(self, caplog: pytest.LogCaptureFixture): diff --git a/software/tests/unit/test_quantization.py b/software/tests/unit/test_quantization.py index aded2fc..22aec33 100644 --- a/software/tests/unit/test_quantization.py +++ b/software/tests/unit/test_quantization.py @@ -18,6 +18,8 @@ from __future__ import annotations +import math + import polars as pl import pytest @@ -113,6 +115,16 @@ def test_constants_track_documented_values(self): assert CID_QUANTIZE_DECIMALS == 3 assert CID_QUANTIZE_PREFIXES == ("charge_", "chargeShift_", "pi_") + # Signed-zero canonicalization: a `-0.0` input (FP-residual-sign drift on a + # ~0 property) must emit as `+0.0`, so the TSV writer produces identical bytes + # regardless of summation order. `-0.0 == 0.0` numerically, so the guard is + # on the SIGN bit, not equality. + def test_negative_zero_canonicalized_to_positive_zero(self): + out = _quantize_for_cid(pl.DataFrame({"entity_key": ["x"], "gravy_x": [-0.0]})) + v = out["gravy_x"][0] + assert v == 0.0 + assert math.copysign(1.0, v) == 1.0, f"expected +0.0, got {v!r} (negative-zero bit set)" + class TestPipelineQuantizationApplied: """Quantization fires at the pipeline boundary, not just in the helper.""" diff --git a/software/tests/unit/test_vectorized_fv_pi.py b/software/tests/unit/test_vectorized_fv_pi.py index f6a9e21..2e2cbb6 100644 --- a/software/tests/unit/test_vectorized_fv_pi.py +++ b/software/tests/unit/test_vectorized_fv_pi.py @@ -1,8 +1,7 @@ """Parity + determinism tests: vectorized Fv isoelectric point vs the scalar oracle. The vectorized Fv pI (vectorized.fv_isoelectric_point) bisects the per-chain -charge SUM, mirroring the scalar `properties.fv_isoelectric_point` (and the -Fv-row path in `pipeline._compute_fv_row_from_ctx`) exactly: +charge SUM, mirroring the scalar `properties.fv_isoelectric_point` exactly: * SAME bracket [0, 14], SAME tol 1e-3 -> SAME data-independent iteration count; * SAME branch test `(f_mid > 0) == (f_lo > 0)`; @@ -43,27 +42,19 @@ pairs = st.lists(st.tuples(_seq, _seq), min_size=1, max_size=25) -@given(pairs, st.booleans()) -def test_fv_pi_parity(pairs, inc): +@given(pairs) +def test_fv_pi_parity(pairs): vhs = [vh for vh, _ in pairs] vls = [vl for _, vl in pairs] sub_vh = build_counts(vhs) sub_vl = build_counts(vls) - vec = fv_isoelectric_point(sub_vh, sub_vl, IPC2_PROTEIN, include_cys=inc) + # The pipeline only ever uses Fv pI with include_cys=False (the full-chain + # rule). Compare against the PUBLIC scalar oracle, which hard-codes that rule. + vec = fv_isoelectric_point(sub_vh, sub_vl, IPC2_PROTEIN, include_cys=False) assert vec.dtype == np.float64 assert vec.shape == (len(pairs),) for i, (vh, vl) in enumerate(pairs): - # Scalar fv_isoelectric_point hard-codes include_cys=False; mirror its - # cleaning here and bisect the SUM with the requested include_cys so the - # parity holds for both settings. - vh_clean = scalar._prepare(vh) - vl_clean = scalar._prepare(vl) - if vh_clean is None or vl_clean is None: - assert math.isnan(vec[i]) - continue - ip_vh = scalar._ipc2_isoelectric_point(vh_clean, IPC2_PROTEIN, include_cys=inc) - ip_vl = scalar._ipc2_isoelectric_point(vl_clean, IPC2_PROTEIN, include_cys=inc) - exp = scalar._bisect_charge_zero(lambda ph: ip_vh.charge_at_pH(ph) + ip_vl.charge_at_pH(ph)) + exp = scalar.fv_isoelectric_point(vh, vl, IPC2_PROTEIN) if exp is None: assert math.isnan(vec[i]) else: diff --git a/software/tests/unit/test_vectorized_substrate.py b/software/tests/unit/test_vectorized_substrate.py index 4c1d13c..973c0c0 100644 --- a/software/tests/unit/test_vectorized_substrate.py +++ b/software/tests/unit/test_vectorized_substrate.py @@ -3,9 +3,9 @@ from hypothesis import given from hypothesis import strategies as st -from aa_tables import STANDARD_AAS +from aa_tables import STANDARD_AA_SET, STANDARD_AAS from properties import aa_counts, clean_sequence, effective_length, is_invalid_sequence -from vectorized import build_counts +from vectorized import _BYTE_TO_AA, build_counts # Include non-standard residues, stop codons, lowercase, gaps, and empties. raw = st.lists(st.text(alphabet=STANDARD_AAS + "*BZXJUabc-", max_size=40), min_size=1, max_size=30) @@ -28,3 +28,15 @@ def test_handles_none_and_empty(): sub = build_counts([None, "", "*", "BZXJ", "A"]) assert sub.valid.tolist() == [False, False, False, False, True] assert int(sub.length[4]) == 1 + + +def test_byte_table_kept_set_matches_oracle(): + """Structural sync guard: the vectorized byte->AA table keeps a byte iff the + oracle's cleaning rule keeps that character. A future change to STANDARD_AAS + or the byte table that desyncs them from `properties.clean_sequence`'s + `c.upper() in STANDARD_AA_SET` rule fails here. Covers upper AND lower case. + """ + for b in range(256): + kept = _BYTE_TO_AA[b] >= 0 + oracle_kept = chr(b).upper() in STANDARD_AA_SET + assert kept == oracle_kept, f"byte {b} ({chr(b)!r}): table kept={kept}, oracle kept={oracle_kept}" From 8a4fd34f14a5a28b19c7560528d2b88bc1d5ac72 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 12:21:43 -0700 Subject: [PATCH 15/21] Silence spurious matmul SIMD warnings (errstate); replace inline noqa E402 with ruff per-file-ignore --- software/pyproject.toml | 7 +++++++ software/src/main.py | 16 ++++++++-------- software/src/vectorized.py | 23 +++++++++++++++++++---- software/tests/_corpus_gen.py | 6 +++--- 4 files changed, 37 insertions(+), 15 deletions(-) diff --git a/software/pyproject.toml b/software/pyproject.toml index 831c4d6..2bc32aa 100644 --- a/software/pyproject.toml +++ b/software/pyproject.toml @@ -59,6 +59,13 @@ ignore = [] fixable = ["ALL"] unfixable = [] +[tool.ruff.lint.per-file-ignores] +# E402: these modules intentionally run setup before imports — main.py pins +# POLARS_MAX_THREADS before polars is imported; _corpus_gen.py adds src to +# sys.path before importing the package (it runs standalone via `python -m`). +"src/main.py" = ["E402"] +"tests/_corpus_gen.py" = ["E402"] + [tool.ruff.format] quote-style = "double" indent-style = "space" diff --git a/software/src/main.py b/software/src/main.py index 32420e2..bf4ad94 100644 --- a/software/src/main.py +++ b/software/src/main.py @@ -18,14 +18,14 @@ # explicit override from the environment still wins. os.environ.setdefault("POLARS_MAX_THREADS", "1") -import argparse # noqa: E402 -import json # noqa: E402 -import logging # noqa: E402 -import sys # noqa: E402 -from pathlib import Path # noqa: E402 - -from io_layer import read_input_tsv, read_plan, write_output_tsv # noqa: E402 -from pipeline import run # noqa: E402 +import argparse +import json +import logging +import sys +from pathlib import Path + +from io_layer import read_input_tsv, read_plan, write_output_tsv +from pipeline import run def _configure_logging() -> None: diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 02a2654..15fc5d0 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -92,6 +92,21 @@ def _safe_div(num: np.ndarray, length: np.ndarray, valid: np.ndarray) -> np.ndar return out +def _matvec(counts: np.ndarray, weights: np.ndarray) -> np.ndarray: + """counts (N,20) · weights (20,) -> (N,). + + Wrapped in np.errstate because numpy's matmul SIMD kernel raises spurious + overflow / invalid / divide-by-zero RuntimeWarnings on this matvec shape + even though the result is exact: the flags are read from FP status on + masked tail SIMD lanes, not from the data. The output is bit-identical to + the unwrapped `counts @ weights` (verified, maxdiff 0.0) and parity-tested + to 1e-16; multiply-sum / einsum silence the warning too but change the + summation order (and the emitted bytes), so they are not used here. + """ + with np.errstate(over="ignore", invalid="ignore", divide="ignore"): + return counts @ weights + + @dataclass(frozen=True) class Substrate: counts: np.ndarray # (N, 20) int64, STANDARD_AAS order @@ -213,7 +228,7 @@ def gravy(sub: Substrate) -> np.ndarray: Mirrors ProteinAnalysis.gravy(), which returns total_gravy / length. """ - return _safe_div(sub.counts @ _KD, sub.length, sub.valid) + return _safe_div(_matvec(sub.counts, _KD), sub.length, sub.valid) def molecular_weight(sub: Substrate) -> np.ndarray: @@ -221,13 +236,13 @@ def molecular_weight(sub: Substrate) -> np.ndarray: Mirrors Bio.SeqUtils.molecular_weight(seq, "protein"); water = 18.0153. """ - mw = sub.counts @ _MASS - (sub.length - 1) * _WATER + mw = _matvec(sub.counts, _MASS) - (sub.length - 1) * _WATER return _mask(mw, sub.valid & (sub.length > 0)) def aromaticity(sub: Substrate) -> np.ndarray: """Aromatic mole fraction (F + W + Y) / length.""" - return _safe_div(sub.counts @ _AROMATIC, sub.length, sub.valid) + return _safe_div(_matvec(sub.counts, _AROMATIC), sub.length, sub.valid) def aliphatic_index(sub: Substrate) -> np.ndarray: @@ -249,7 +264,7 @@ def extinction(sub: Substrate) -> tuple[np.ndarray, np.ndarray]: Matches the scalar extinction_coefficients() column order (oxidized first); BioPython's molar_extinction_coefficient() returns (reduced, oxidized). """ - red = sub.counts @ _EXT_RED + red = _matvec(sub.counts, _EXT_RED) ox = red + (sub.counts[:, _C_INDEX] // 2) * 125.0 return _mask(ox, sub.valid), _mask(red, sub.valid) diff --git a/software/tests/_corpus_gen.py b/software/tests/_corpus_gen.py index d6871f0..ff65e7d 100644 --- a/software/tests/_corpus_gen.py +++ b/software/tests/_corpus_gen.py @@ -32,10 +32,10 @@ if str(_SRC) not in sys.path: sys.path.insert(0, str(_SRC)) -import polars as pl # noqa: E402 +import polars as pl -from io_layer import read_input_tsv, read_plan, write_output_tsv # noqa: E402 -from pipeline import run # noqa: E402 +from io_layer import read_input_tsv, read_plan, write_output_tsv +from pipeline import run DATA = Path(__file__).resolve().parent / "data" / "characterization" GOLDEN = DATA / "golden" From 25e18b9a446d55584604c550583cfc30b47719e1 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 13:24:51 -0700 Subject: [PATCH 16/21] Reduce vectorized engine peak memory (int32 indices, share clean, drop dead retention) --- software/src/pipeline.py | 31 ++++++++--- software/src/vectorized.py | 111 +++++++++++++++++++++++++++---------- 2 files changed, 104 insertions(+), 38 deletions(-) diff --git a/software/src/pipeline.py b/software/src/pipeline.py index fcd4511..df23ae4 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -186,7 +186,7 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: NaN -> null is applied to every emitted float column (and the AA-fraction `value`) so NA rows render as empty cells, byte-matching the scalar path. """ - keys = reads["entity_key"].to_list() + key_series = reads["entity_key"].cast(pl.Utf8) seqs = reads["sequence"].to_list() n = len(seqs) @@ -208,15 +208,17 @@ def run_peptide(reads: pl.DataFrame) -> dict[str, Any]: } properties = pl.DataFrame( { - "entity_key": pl.Series(values=keys, dtype=pl.Utf8), + "entity_key": key_series, **{c: _na_to_null(prop_arrays[c]) for c in PEPTIDE_PROPERTY_COLUMNS}, } ) # AA-fraction long frame from the (N, 20) mole-fraction matrix: 20 rows per # entity (one per STANDARD_AAS), value NaN -> null for invalid entities so - # the 2-axis PColumn keeps a uniform shape across entities. + # the 2-axis PColumn keeps a uniform shape across entities. The long-format + # entity axis needs Python-level repetition, so materialize keys here. fractions = vec.aa_fractions(sub) # (N, 20), NaN for invalid rows + keys = key_series.to_list() aa_entity = [k for k in keys for _ in STANDARD_AAS] aa_amino = list(STANDARD_AAS) * n aa_value = fractions.reshape(-1) # row-major: entity 0's 20 AAs, then entity 1's, ... @@ -379,7 +381,7 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any if plan.get("hasFv"): log.info("Computing Fv properties (paired VH+VL)") - series: dict[str, pl.Series] = {"entity_key": pl.Series(values=reads["entity_key"].to_list(), dtype=pl.Utf8)} + series: dict[str, pl.Series] = {"entity_key": reads["entity_key"].cast(pl.Utf8)} # CDR3 per chain — IPC2_PEPTIDE, Cys included. Empty cell -> NA for this # clone (build_counts marks it invalid -> the funcs emit NaN -> null). @@ -395,14 +397,19 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any # Full chain — reconstruct then compute. IPC2_PROTEIN, Cys excluded. NA per # clone where any region is missing (reconstruction None -> invalid row). - # Cache the per-chain substrates for Fv reuse below. + # Cache the per-chain substrates for Fv reuse below. Each chain is cleaned + # ONCE: the shared `_Cleaned` feeds both the count substrate and the + # instability index, so the full chains are not cleaned twice (the clean's + # flat AA buffer is the dominant full-chain transient). The reconstructed + # `seqs` list and the `_Cleaned` are dropped at the end of each iteration — + # Fv reuses only the substrates (chain_subs), never the raw sequences. chain_subs: dict[str, vec.Substrate] = {} - chain_seqs: dict[str, list[str | None]] = {} for ch in full_chains: seqs = _reconstruct_chain_column(reads, ch) - sub = vec.build_counts(seqs) + cleaned = vec._clean_vectorized(seqs) + del seqs # only the cleaned form is needed past this point + sub = vec.counts_from_cleaned(cleaned) chain_subs[ch] = sub - chain_seqs[ch] = seqs eox, ered = vec.extinction(sub) full_arrays = { "charge": vec.charge_at_ph(sub, PH, IPC2_PROTEIN, include_cys=False), @@ -411,7 +418,7 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any "mw": vec.molecular_weight(sub), "eox": eox, "ered": ered, - "instability": vec.instability_index(seqs), + "instability": vec.instability_from_cleaned(cleaned), "aliphatic": vec.aliphatic_index(sub), "aromaticity": vec.aromaticity(sub), } @@ -441,6 +448,12 @@ def run_antibody_tcr(reads: pl.DataFrame, plan: dict[str, Any]) -> dict[str, Any } for p in FV_PROPS: series[f"{p}_Fv"] = _na_to_null(fv_arrays[p]) + del eox_vh, ered_vh, eox_vl, ered_vl, sub_vh, sub_vl, fv_arrays + + # The full-chain substrates (each an (N, 20) count matrix + length + valid) + # are the last large live arrays besides the emitted Series. Nothing past + # this point reads them, so drop them before assembling the output frame. + chain_subs.clear() out_cols = _planned_output_columns(plan) properties = pl.DataFrame({"entity_key": series["entity_key"], **{c: series[c] for c in out_cols}}) diff --git a/software/src/vectorized.py b/software/src/vectorized.py index 15fc5d0..03f2957 100644 --- a/software/src/vectorized.py +++ b/software/src/vectorized.py @@ -121,10 +121,10 @@ class _Cleaned: as one flat AA-index buffer with row boundaries, so per-char Python loops are replaced by a single numpy gather over the joined bytes. - * `cand_rows` — (C,) original row index of each candidate (into the full N). + * `cand_rows` — (C,) int32 original row index of each candidate (into N). * `aa_flat` — (T,) int8 AA index per *standard* residue, concatenated in candidate then residue order (non-standard chars dropped). - * `row_of_res` — (T,) candidate-local row id (0..C-1) for each entry of + * `row_of_res` — (T,) int32 candidate-local row id (0..C-1) for each entry of aa_flat — i.e. which candidate that residue belongs to. * `lengths` — (C,) standard-residue count per candidate (== len of its slice of aa_flat). @@ -144,13 +144,15 @@ def _clean_vectorized(seqs: list[str | None]) -> _Cleaned: `is_invalid_sequence` short-circuit). Each candidate is latin-1 encoded (one byte per char), the bytes are concatenated into a single buffer, `np.frombuffer`'d once, and mapped through `_BYTE_TO_AA`; standard residues - keep their AA index, non-standard chars (mapped to -1) are dropped. A - per-char candidate-local row id is built with `np.repeat`, then masked to - the kept residues — so `aa_flat[k]` belongs to candidate `row_of_res[k]`. + keep their AA index, non-standard chars (mapped to -1) are dropped. The + candidate-local row id of each *kept* residue is built by counting kept + residues per candidate (segment-sum of the keep mask) and repeating the + candidate ids by those counts — so `aa_flat[k]` belongs to candidate + `row_of_res[k]` without ever materializing a per-CHAR row-id array. """ n = len(seqs) cand_rows: list[int] = [] - buf = bytearray() + chunks: list[bytes] = [] # per-candidate latin-1 bytes, joined once below char_lengths: list[int] = [] # per-candidate char count (== byte count) for i, s in enumerate(seqs): if s is None or s == "" or "*" in s: @@ -158,11 +160,21 @@ def _clean_vectorized(seqs: list[str | None]) -> _Cleaned: b = s.encode("latin-1", "replace") cand_rows.append(i) char_lengths.append(len(b)) - buf += b - - cand_rows_arr = np.asarray(cand_rows, dtype=np.intp) + chunks.append(b) + # Single exact-sized join instead of an incremental `bytearray +=` loop: + # the per-append reallocation/overallocation of a many-hundred-MB buffer was + # a large RSS transient (and allocator churn over the millions of per-row + # encode() chunks). `b"".join` allocates the final buffer once. + buf = b"".join(chunks) + del chunks + + # int32 indices throughout: cand_rows are row ids into the full N and + # row_of_res are candidate-local row ids (0..C-1) — both are < 2**31 for any + # real dataset (<= ~2.1B clones), so int32 is safe and halves these + # per-residue arrays vs the numpy default int64/intp. + cand_rows_arr = np.asarray(cand_rows, dtype=np.int32) if not buf: - empty_i = np.empty(0, dtype=np.intp) + empty_i = np.empty(0, dtype=np.int32) empty_aa = np.empty(0, dtype=np.int8) return _Cleaned( n=n, @@ -174,14 +186,27 @@ def _clean_vectorized(seqs: list[str | None]) -> _Cleaned: char_len_arr = np.asarray(char_lengths, dtype=np.intp) # AA index per character of the joined buffer; non-standard chars -> -1. - aa_per_char = _BYTE_TO_AA[np.frombuffer(bytes(buf), dtype=np.uint8)] - # Candidate-local row id per character (0..C-1), then drop non-standard. - row_per_char = np.repeat(np.arange(len(char_len_arr), dtype=np.intp), char_len_arr) + # frombuffer is a zero-copy read-only view over `buf`; the gather copies the + # mapped indices into a fresh array, so `buf` can be dropped right after. + aa_per_char = _BYTE_TO_AA[np.frombuffer(buf, dtype=np.uint8)] + del buf keep = aa_per_char >= 0 aa_flat = aa_per_char[keep] - row_of_res = row_per_char[keep] - # Standard-residue count per candidate (folds the kept mask back per row). - lengths = np.bincount(row_of_res, minlength=len(char_len_arr)).astype(np.int64) + del aa_per_char + # Kept-residue count per candidate: segment-sum the keep mask over each + # candidate's char slice. Every candidate has >=1 char (empty strings are + # filtered above), so the start offsets have no zero-length segments and + # reduceat is well defined. This is `lengths` directly and avoids both the + # per-char row-id array (the dominant transient) and a separate bincount. + starts = np.empty(len(char_len_arr), dtype=np.intp) + starts[0] = 0 + np.cumsum(char_len_arr[:-1], out=starts[1:]) + lengths = np.add.reduceat(keep, starts).astype(np.int64) + del keep + # Candidate-local row id per KEPT residue: repeat each candidate id by its + # kept count — produces the (T_kept,) survivor directly, no masking. + # int32: candidate count C < 2**31 for any real dataset (see above). + row_of_res = np.repeat(np.arange(len(char_len_arr), dtype=np.int32), lengths) return _Cleaned( n=n, cand_rows=cand_rows_arr, @@ -191,17 +216,19 @@ def _clean_vectorized(seqs: list[str | None]) -> _Cleaned: ) -def build_counts(seqs: list[str | None]) -> Substrate: - """Per-residue (N, 20) count matrix + effective length + validity, matching - properties.py's scalar cleaning exactly. +def counts_from_cleaned(cl: _Cleaned) -> Substrate: + """Build the (N, 20) count substrate from an already-cleaned column. - Fully vectorized: candidates are cleaned to one flat AA-index buffer - (`_clean_vectorized`), counts are scattered with a single `np.add.at` over - the (candidate-local-row, AA-index) pairs, then mapped back to the full N - rows. A candidate is valid iff at least one standard residue remained + Counts are scattered with a single `np.add.at` over the + (candidate-local-row, AA-index) pairs, then mapped back to the full N rows. + A candidate is valid iff at least one standard residue remained (lengths > 0) — reproducing the scalar `clean_sequence(...) or None` rule. + + Splitting the clean (`_clean_vectorized`) from this derivation lets a caller + clean a sequence-set ONCE and feed the same `_Cleaned` to both + `counts_from_cleaned` and `instability_from_cleaned`, avoiding a duplicate + full-chain clean (a large transient on the full-chain path). """ - cl = _clean_vectorized(seqs) n = cl.n counts = np.zeros((n, 20), dtype=np.int64) valid = np.zeros(n, dtype=bool) @@ -216,6 +243,18 @@ def build_counts(seqs: list[str | None]) -> Substrate: return Substrate(counts=counts, length=length, valid=valid) +def build_counts(seqs: list[str | None]) -> Substrate: + """Per-residue (N, 20) count matrix + effective length + validity, matching + properties.py's scalar cleaning exactly. + + Fully vectorized: candidates are cleaned to one flat AA-index buffer + (`_clean_vectorized`), then `counts_from_cleaned` scatters the counts. Public + entry point for callers that only need counts; the pipeline's full-chain path + instead cleans once and shares the `_Cleaned` with `instability_from_cleaned`. + """ + return counts_from_cleaned(_clean_vectorized(seqs)) + + # --------------------------------------------------------------------------- # # Linear properties — pure array ops over the count matrix. Each returns a # float64 array (or pair of arrays) with NaN for invalid rows, matching the @@ -478,17 +517,18 @@ def fv_isoelectric_point( # --------------------------------------------------------------------------- # -def instability_index(seqs: list[str | None]) -> np.ndarray: - """Guruprasad instability index per sequence, float64, NaN where the scalar - oracle returns None. +def instability_from_cleaned(cl: _Cleaned) -> np.ndarray: + """Guruprasad instability index from an already-cleaned column, float64, NaN + where the scalar oracle returns None. + Splitting the clean from this derivation lets the full-chain path reuse the + same `_Cleaned` for both counts and instability (see `counts_from_cleaned`). A row is NaN when the sequence is invalid (None / empty / contains a stop codon `*` / nothing standard remains after cleaning) OR when the cleaned (standard-AA-only) length is below `_INSTABILITY_MIN_LENGTH` (= 10). Order is preserved: each row's value is a pure function of that row's sequence, with no dict/set iteration leaking into the output. """ - cl = _clean_vectorized(seqs) out = np.full(cl.n, np.nan, dtype=np.float64) n_cand = len(cl.cand_rows) if n_cand == 0: @@ -498,7 +538,11 @@ def instability_index(seqs: list[str | None]) -> np.ndarray: # to the SAME candidate (row boundaries in the joined buffer must not leak a # pair across two sequences). Mask on row_of_res, gather DIWV weights, then # sum per candidate with np.add.at — order-preserving, no per-row Python. - aa = cl.aa_flat.astype(np.intp) + # aa_flat stays int8 (no intp promotion): _DIWV fancy-indexing accepts an + # int8 index, and avoiding the int64 copy saves ~8x its size on full chains + # (the int64 promotion was a multi-hundred-MB transient). The gathered DIWV + # weights and the per-candidate sum are unchanged, so the result is identical. + aa = cl.aa_flat rows = cl.row_of_res if aa.size >= 2: same = rows[:-1] == rows[1:] @@ -517,3 +561,12 @@ def instability_index(seqs: list[str | None]) -> np.ndarray: rows_keep = cl.cand_rows[keep] out[rows_keep] = (10.0 / lengths[keep]) * score[keep] return out + + +def instability_index(seqs: list[str | None]) -> np.ndarray: + """Public entry point: clean then derive the instability index. Callers that + also need the count substrate should clean once with `_clean_vectorized` and + feed the shared `_Cleaned` to both `counts_from_cleaned` and + `instability_from_cleaned` instead of calling this and `build_counts`. + """ + return instability_from_cleaned(_clean_vectorized(seqs)) From ce92e4e1a96a1a90738f68e449eb28bf530cb7ce Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 13:24:55 -0700 Subject: [PATCH 17/21] Raise compute-properties mem allocation for vectorized footprint --- .changeset/vectorize-sequence-properties.md | 3 +++ workflow/src/main.tpl.tengo | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.changeset/vectorize-sequence-properties.md b/.changeset/vectorize-sequence-properties.md index fec440d..0a7a67a 100644 --- a/.changeset/vectorize-sequence-properties.md +++ b/.changeset/vectorize-sequence-properties.md @@ -1,5 +1,8 @@ --- '@platforma-open/milaboratories.sequence-properties.software': patch +'@platforma-open/milaboratories.sequence-properties.workflow': patch --- Vectorize the property computation — single-threaded numpy/polars array math replaces per-row BioPython. ~5x faster at 50k clones; output unchanged within the quantized-equal contract (signed-zero canonicalized). + +Reduce the vectorized engine's peak memory (int8/int32 indices, share the clean intermediate between counts and instability, drop dead retention) — ~4.85 GB/1M clones vs ~8.5 GB before. Raise the compute-properties step's `mem()` from 4GiB to 16GiB to cover ~2M clones at the new footprint with headroom. diff --git a/workflow/src/main.tpl.tengo b/workflow/src/main.tpl.tengo index ef5afae..17335db 100644 --- a/workflow/src/main.tpl.tengo +++ b/workflow/src/main.tpl.tengo @@ -342,9 +342,12 @@ wf.body(func(args) { // stats.json (dataset-level scalars consumed by the info layer — e.g. R11c // median CDR-H3 length per chain). soft := assets.importSoftware("@platforma-open/milaboratories.sequence-properties.software:compute-properties") + // Measured optimized footprint of the vectorized Python step: ~4.85 GB peak + // RSS per 1M full-paired antibody clones (so ~9.7 GB at 2M). 16GiB covers 2M + // with ~1.5x headroom; cpu(1) stays single-threaded by design. pyRun := exec.builder(). software(soft). - mem("4GiB"). + mem("16GiB"). cpu(1). addFile("input.tsv", seqTable). writeFile("plan.json", canonical.encode(plan)). From 18be6064f6400699200146ced391625dddf4473e Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 14:23:48 -0700 Subject: [PATCH 18/21] Apply code-review findings: oracle-parity + invariant + edge tests, drop vestigial constants, exclude perf from CI gate - Add pipeline<->oracle composition parity test (catches pKa/include_cys/chain-pairing wiring regressions the self-generated golden can't) - Add vectorized-engine invariant tests (invariants hold on the shipping code, not only the oracle) - Add edge tests: Fv AND-gating with a present-but-invalid chain; zero-row full-chain+Fv; non-ASCII single-byte cleaning divergence - Remove vestigial CID_QUANTIZE_PREFIXES/DECIMALS; tests derive decimals from the live _decimals_for - Exclude the @slow perf benchmark from the pytest CI gate (wall-clock budget flakes on shared runners) --- .github/workflows/python-tests.yaml | 5 +- software/src/pipeline.py | 5 - .../test_pipeline_oracle_parity.py | 133 ++++++++++++++++++ software/tests/unit/test_pipeline.py | 73 ++++++++++ software/tests/unit/test_quantization.py | 42 ++++-- .../tests/unit/test_vectorized_invariants.py | 98 +++++++++++++ .../tests/unit/test_vectorized_substrate.py | 26 ++++ 7 files changed, 365 insertions(+), 17 deletions(-) create mode 100644 software/tests/integration/test_pipeline_oracle_parity.py create mode 100644 software/tests/unit/test_vectorized_invariants.py diff --git a/.github/workflows/python-tests.yaml b/.github/workflows/python-tests.yaml index b74e8c1..f8eefbe 100644 --- a/.github/workflows/python-tests.yaml +++ b/.github/workflows/python-tests.yaml @@ -37,7 +37,10 @@ jobs: run: uv run ruff format --check - name: Run pytest - run: uv run pytest + # Exclude @slow (the perf benchmark) from the gate — it asserts a + # wall-clock budget that flakes on shared runners. Run it explicitly + # with `uv run pytest -m slow`. + run: uv run pytest -m "not slow" requirements-sync: # src/requirements.txt is generated from pyproject.toml (the single source of diff --git a/software/src/pipeline.py b/software/src/pipeline.py index df23ae4..4d7c12f 100644 --- a/software/src/pipeline.py +++ b/software/src/pipeline.py @@ -58,11 +58,6 @@ # The quantization is a *boundary* concern. Internal property functions # (`charge_at_ph`, `isoelectric_point`, etc.) keep full precision so golden- # value tests stay sharp. Only the pipeline's emitted DataFrame is rounded. -# -# `CID_QUANTIZE_PREFIXES` / `CID_QUANTIZE_DECIMALS` remain as documentation of -# the charge/chargeShift/pi family's 3-dp contract (other tests assert them). -CID_QUANTIZE_PREFIXES = ("charge_", "chargeShift_", "pi_") -CID_QUANTIZE_DECIMALS = 3 # Per-column-family rounding at the output boundary. Keys match the TSV column # *prefixes* the pipeline emits; the first matching prefix wins. Chosen below diff --git a/software/tests/integration/test_pipeline_oracle_parity.py b/software/tests/integration/test_pipeline_oracle_parity.py new file mode 100644 index 0000000..638a985 --- /dev/null +++ b/software/tests/integration/test_pipeline_oracle_parity.py @@ -0,0 +1,133 @@ +"""Pipeline-level parity: the vectorized antibody/TCR pipeline matches the +scalar BioPython oracle (properties.py) cell-for-cell, within the CID +quantization. + +The per-function unit parity tests (test_vectorized_*.py) pin each vectorized +math function to the oracle on random sequences, and the characterization +snapshot freezes the pipeline's own output. Neither pins the pipeline +COMPOSITION to BioPython: which pKa set + include_cys each column uses, the +A=VH / B=VL Fv pairing, full-chain reconstruction, and column naming. This test +runs the committed characterization corpus through both the vectorized pipeline +and an independent oracle restatement and asserts they agree on every emitted +cell — so a wiring regression (wrong pKa set, include_cys flip, chain swap) +fails here even though every column is present and the self-generated golden +would still pass. + +Tolerance is one display quantum (10**-decimals) per column: the pipeline value +is the oracle value rounded at the boundary, so they agree to display precision, +while a wiring error shifts values by far more than a quantum. + +Run from blocks/sequence-properties/software/: + uv sync + uv run pytest tests/integration/test_pipeline_oracle_parity.py +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +import properties as scalar +from io_layer import read_input_tsv, read_plan +from pipeline import _decimals_for, run +from pka_tables import IPC2_PEPTIDE, IPC2_PROTEIN + +DATA = Path(__file__).resolve().parents[1] / "data" / "characterization" + +# Cases with full-chain and/or Fv columns. peptide is fully covered by the +# per-function unit parity tests; these exercise the reconstruction + Fv wiring. +ANTIBODY_CASES = sorted( + p.name.removesuffix("_input.tsv") for p in DATA.glob("*_input.tsv") if p.name != "peptide_input.tsv" +) + +REQUIRED_FEATURES = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") + + +def _reconstruct(record: dict, chain: str) -> str | None: + """FR1+CDR1+...+FR4 for one clone; None if any region is empty or missing. + + An independent restatement of the pipeline's reconstruction contract (the + old scalar `_reconstruct_chain` this PR removed), so the comparison does not + lean on the pipeline's own polars implementation. + """ + parts = [] + for feat in REQUIRED_FEATURES: + v = record.get(f"{chain}_{feat}") + if not v: # None or "" -> missing region -> whole chain is NA + return None + parts.append(v) + return "".join(parts) + + +def _oracle_expected(record: dict, plan: dict) -> dict[str, float | None]: + """Every property column the plan emits, computed via the scalar oracle with + the SAME pKa set / include_cys rule the pipeline uses per context. The oracle + functions return None for invalid/missing input, so reconstructed-None chains + and empty CDR3 cells flow through to None naturally. + """ + exp: dict[str, float | None] = {} + + # CDR3 — IPC 2.0 peptide pKa, Cys included (free-thiol rule). + for ch in plan.get("chains", []): + cdr3 = record.get(f"{ch}_CDR3") + exp[f"charge_{ch}_CDR3"] = scalar.charge_at_ph(cdr3, 7.0, IPC2_PEPTIDE, include_cys=True) + exp[f"chargeShift_{ch}_CDR3"] = scalar.charge_shift(cdr3, IPC2_PEPTIDE, include_cys=True) + exp[f"gravy_{ch}_CDR3"] = scalar.gravy(cdr3) + + # Full chain — IPC 2.0 protein pKa, Cys excluded (disulfide-bonded rule). + for ch in plan.get("fullChains", []): + seq = _reconstruct(record, ch) + ox, red = scalar.extinction_coefficients(seq) + exp[f"charge_{ch}_VDJRegion"] = scalar.charge_at_ph(seq, 7.0, IPC2_PROTEIN, include_cys=False) + exp[f"pi_{ch}_VDJRegion"] = scalar.isoelectric_point(seq, IPC2_PROTEIN, include_cys=False) + exp[f"gravy_{ch}_VDJRegion"] = scalar.gravy(seq) + exp[f"mw_{ch}_VDJRegion"] = scalar.molecular_weight(seq) + exp[f"eox_{ch}_VDJRegion"] = ox + exp[f"ered_{ch}_VDJRegion"] = red + exp[f"instability_{ch}_VDJRegion"] = scalar.instability_index(seq) + exp[f"aliphatic_{ch}_VDJRegion"] = scalar.aliphatic_index(seq) + exp[f"aromaticity_{ch}_VDJRegion"] = scalar.aromaticity(seq) + + # Fv — additive over VH=A, VL=B with the full-chain rule (Cys excluded). + if plan.get("hasFv"): + vh = _reconstruct(record, "A") + vl = _reconstruct(record, "B") + ox, red = scalar.fv_extinction_coefficients(vh, vl) + exp["charge_Fv"] = scalar.fv_charge(vh, vl, 7.0, IPC2_PROTEIN) + exp["chargeShift_Fv"] = scalar.fv_charge_shift(vh, vl, IPC2_PROTEIN) + exp["pi_Fv"] = scalar.fv_isoelectric_point(vh, vl, IPC2_PROTEIN) + exp["eox_Fv"] = ox + exp["ered_Fv"] = red + exp["mw_Fv"] = scalar.fv_molecular_weight(vh, vl) + + return exp + + +@pytest.mark.parametrize("case", ANTIBODY_CASES) +def test_pipeline_matches_oracle(case): + reads = read_input_tsv(DATA / f"{case}_input.tsv") + plan = read_plan(DATA / f"{case}_plan.json") + out = run(reads, plan) + actual_rows = {r["entity_key"]: r for r in out["properties"].iter_rows(named=True)} + + compared = 0 + for record in reads.iter_rows(named=True): + actual = actual_rows[record["entity_key"]] + for col, expected in _oracle_expected(record, plan).items(): + got = actual[col] + if expected is None: + assert got is None, f"{case}/{record['entity_key']}/{col}: oracle NA but pipeline {got}" + continue + assert got is not None, f"{case}/{record['entity_key']}/{col}: oracle {expected} but pipeline NA" + # One display quantum below the column's rounding decimals: covers the + # boundary rounding (<=0.5 quantum) + per-function FP slack; a wiring + # error shifts values by far more. + tol = 10.0 ** (-_decimals_for(col)) + assert got == pytest.approx(expected, abs=tol), ( + f"{case}/{record['entity_key']}/{col}: pipeline {got} vs oracle {expected} (tol {tol})" + ) + compared += 1 + # Guard against the comparison silently doing nothing (column-name drift, a + # case whose rows are all-NA) reading as a pass. + assert compared > 0, f"{case}: no non-NA cells compared" diff --git a/software/tests/unit/test_pipeline.py b/software/tests/unit/test_pipeline.py index 1b9b9dc..5fb4863 100644 --- a/software/tests/unit/test_pipeline.py +++ b/software/tests/unit/test_pipeline.py @@ -333,6 +333,52 @@ def test_chain_b_dropout_for_one_clone(self): assert rows["c2"][f"{p}_Fv"] is None +class TestFvValidityGating: + """Fv columns require BOTH chains valid (AND-gating, not OR). A chain that is + present and non-empty but INVALID — a stop codon in one region — must NA + every Fv column even though the other chain is fine. Distinct from the + sc-dropout case (chain entirely empty -> reconstruction None): here the chain + reconstructs to a non-empty string that `build_counts` marks invalid. + """ + + def test_stop_codon_in_one_chain_nas_all_fv(self): + # Chain A fully valid. Chain B reconstructs to a non-empty string whose + # CDR3 carries a stop codon -> chain B is invalid (not missing). + regions = { + "A_FR1": ["EVQLVES"], + "A_CDR1": ["GFTFSSY"], + "A_FR2": ["AMSWVRQ"], + "A_CDR2": ["ISGSGGS"], + "A_FR3": ["TYYAESVKGRFTI"], + "A_CDR3": ["CARDYW"], + "A_FR4": ["WGQGTLV"], + "B_FR1": ["DIQMTQS"], + "B_CDR1": ["QSISSY"], + "B_FR2": ["LNWYQQK"], + "B_CDR2": ["AASSLQS"], + "B_FR3": ["GVPSRFSGSG"], + "B_CDR3": ["CQ*YNS"], # stop codon -> whole chain B invalid + "B_FR4": ["FGQGTKV"], + } + reads = pl.DataFrame({"entity_key": ["c1"], **regions}) + plan = { + "mode": "antibody_tcr_legacy_sc", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, + } + out = run(reads, plan) + row = out["properties"].row(0, named=True) + # Chain A full-chain props present; chain B full-chain props all NA. + assert row["mw_A_VDJRegion"] is not None + for p in FULL_CHAIN_PROPS: + assert row[f"{p}_B_VDJRegion"] is None + # Fv requires both chains valid -> every Fv column NA, even though VH is fine. + for p in FV_PROPS: + assert row[f"{p}_Fv"] is None + + class TestEmptyInput: """Zero-row inputs must produce well-formed outputs, not crash. The workflow panics earlier on missing columns, but the Python step must remain robust if @@ -380,6 +426,33 @@ def test_antibody_mode_zero_rows(self): assert f"{p}_B_CDR3" in cols assert out["stats"] == {"medianCdr3Length": {}} + # Full-chain + Fv plan with zero rows — the path that builds (0, 20) count + # matrices and runs the bisection / 10.0/len instability over EMPTY arrays. + # The simpler zero-row cases above never reach full-chain reconstruction or + # the Fv sum, so this guards the empty-array bisection specifically. + def test_full_chain_fv_mode_zero_rows(self): + regions = ("FR1", "CDR1", "FR2", "CDR2", "FR3", "CDR3", "FR4") + region_cols = {f"{ch}_{feat}": pl.Utf8 for ch in "AB" for feat in regions} + reads = pl.DataFrame(schema={"entity_key": pl.Utf8, **region_cols}) + plan = { + "mode": "antibody_tcr_legacy_bulk", + "receptor": "IG", + "chains": ["A", "B"], + "fullChains": ["A", "B"], + "hasFv": True, + } + out = run(reads, plan) + assert out["properties"].height == 0 + cols = set(out["properties"].columns) + for ch in "AB": + for p in CDR3_PROPS: + assert f"{p}_{ch}_CDR3" in cols + for p in FULL_CHAIN_PROPS: + assert f"{p}_{ch}_VDJRegion" in cols + for p in FV_PROPS: + assert f"{p}_Fv" in cols + assert out["stats"] == {"medianCdr3Length": {}} + class TestR11cStats: """`run()` returns a `stats` dict consumed by the workflow info layer for diff --git a/software/tests/unit/test_quantization.py b/software/tests/unit/test_quantization.py index 22aec33..5d39cb4 100644 --- a/software/tests/unit/test_quantization.py +++ b/software/tests/unit/test_quantization.py @@ -24,8 +24,7 @@ import pytest from pipeline import ( - CID_QUANTIZE_DECIMALS, - CID_QUANTIZE_PREFIXES, + _decimals_for, _quantize_for_cid, run, ) @@ -98,7 +97,7 @@ def test_antibody_charge_and_pi_columns_match_prefix(self): "pi_Fv", ): v = out[col][0] - assert v == pytest.approx(round(v, CID_QUANTIZE_DECIMALS), abs=0) + assert v == pytest.approx(round(v, _decimals_for(col)), abs=0) # gravy / mw round to their per-column decimals (5 dp / 4 dp): assert out["gravy_A_VDJRegion"][0] == round(-0.11111156, 5) assert out["mw_A_VDJRegion"][0] == round(6050.730289, 4) @@ -110,10 +109,27 @@ def test_passthrough_when_no_matching_columns(self): out = _quantize_for_cid(df) assert out["notaproperty_x"][0] == 0.123456789 - # Sanity — module-level constants match what the docstring promises. - def test_constants_track_documented_values(self): - assert CID_QUANTIZE_DECIMALS == 3 - assert CID_QUANTIZE_PREFIXES == ("charge_", "chargeShift_", "pi_") + # Sanity — the per-prefix decimals table (via `_decimals_for`) maps each + # property family to the decimals the docstring promises. This is the single + # source of truth the pipeline rounds against; a column with no rule returns + # None (left full-precision). + def test_decimals_for_tracks_documented_values(self): + documented = { + "charge_peptide": 3, + "chargeShift_peptide": 3, + "pi_peptide": 3, + "instability_peptide": 4, + "mw_peptide": 4, + "aliphatic_peptide": 4, + "gravy_peptide": 5, + "aromaticity_peptide": 5, + "eox_peptide": 0, + "ered_peptide": 0, + } + for col, dp in documented.items(): + assert _decimals_for(col) == dp, f"{col}: expected {dp} dp" + # A non-property column has no quantization rule. + assert _decimals_for("entity_key") is None # Signed-zero canonicalization: a `-0.0` input (FP-residual-sign drift on a # ~0 property) must emit as `+0.0`, so the TSV writer produces identical bytes @@ -144,7 +160,7 @@ def test_peptide_run_rounds_charge_and_pi(self): for c in ("charge_peptide", "pi_peptide"): v = row[c] assert v is not None - assert v == pytest.approx(round(v, CID_QUANTIZE_DECIMALS), abs=0), f"{c}={v} not at 3-decimal precision" + assert v == pytest.approx(round(v, _decimals_for(c)), abs=0), f"{c}={v} not at 3-decimal precision" # Antibody full-coverage output: charge_*, pi_*, including Fv, all land at # their 3-dp boundary. (Other families round too — covered by the helper @@ -155,7 +171,9 @@ def test_antibody_run_rounds_all_charge_and_pi_columns( out = run(antibody_full_one_clone, antibody_full_plan) row = out["properties"].row(0, named=True) - rounded_cols = [c for c in row if any(c.startswith(p) for p in CID_QUANTIZE_PREFIXES)] + # The 3-dp family (charge / chargeShift / pi) — every such column lands + # exactly on its boundary. + rounded_cols = [c for c in row if _decimals_for(c) == 3] # Sanity: at least one charge_ and one pi_ column present. assert any(c.startswith("charge_") for c in rounded_cols) assert any(c.startswith("pi_") for c in rounded_cols) @@ -163,7 +181,7 @@ def test_antibody_run_rounds_all_charge_and_pi_columns( v = row[c] if v is None: continue - assert v == pytest.approx(round(v, CID_QUANTIZE_DECIMALS), abs=0), f"{c}={v} not at 3-decimal precision" + assert v == pytest.approx(round(v, _decimals_for(c)), abs=0), f"{c}={v} not at 3-decimal precision" class TestQuantizationDoesNotPropagateInternally: @@ -182,7 +200,9 @@ def test_isoelectric_point_returns_unrounded_value(self): vh = "EVQLVESGFTFSSYAMSWVRQISGSGGSTYYAESVKGRFTICARDYWWGQGTLV" pi = isoelectric_point(vh, IPC2_PROTEIN, include_cys=False) assert pi == pytest.approx(6.006653, abs=1e-6) - assert pi != round(pi, CID_QUANTIZE_DECIMALS) + # The boundary rounds pi to its 3-dp family; the internal function must + # keep more digits than that. + assert pi != round(pi, _decimals_for("pi_peptide")) class TestExtendedQuantizationBoundary: diff --git a/software/tests/unit/test_vectorized_invariants.py b/software/tests/unit/test_vectorized_invariants.py new file mode 100644 index 0000000..e04d847 --- /dev/null +++ b/software/tests/unit/test_vectorized_invariants.py @@ -0,0 +1,98 @@ +"""Invariant (property-based) tests for the VECTORIZED engine. + +`test_invariants.py` asserts these same invariants against the scalar oracle +(properties.py). The vectorized engine is the code that actually ships, so it +must hold the invariants independently — a vectorized charge that is +non-monotonic in pH, an aa-fraction row that does not sum to 1, or a pI outside +[0, 14] is a bug even if every oracle-parity test happened to miss it. + +The engine is columnar: one `build_counts(seqs)` call covers a whole list, so +each invariant is asserted across all rows of a generated list at once. + +Run from blocks/sequence-properties/software/: + uv sync + uv run pytest tests/unit/test_vectorized_invariants.py +""" + +from __future__ import annotations + +import numpy as np +from hypothesis import given +from hypothesis import strategies as st + +from aa_tables import STANDARD_AAS +from pka_tables import IPC2_PEPTIDE +from properties import INSTABILITY_MIN_LENGTH, effective_length +from vectorized import ( + aa_fractions, + build_counts, + charge_at_ph, + instability_index, + isoelectric_point, +) + +# Lists of valid standard-AA sequences. max_size kept modest so Hypothesis +# explores list shapes cheaply. +valid_seqs = st.lists(st.text(alphabet=STANDARD_AAS, min_size=1, max_size=60), min_size=1, max_size=20) + +# Same, salted with known invalids (empty / stop codon / non-standard-only / +# mixed-with-stop) to exercise the NaN branches. +mixed_seqs = st.lists( + st.one_of( + st.text(alphabet=STANDARD_AAS, min_size=1, max_size=60), + st.sampled_from(["", "*", "BZXJ", "ACDE*FG"]), + ), + min_size=1, + max_size=20, +) + + +@given(valid_seqs) +def test_aa_fractions_sum_to_one(seqs): + # Every row is a valid standard-AA sequence -> its mole fractions sum to 1. + frac = aa_fractions(build_counts(seqs)) + assert np.allclose(frac.sum(axis=1), 1.0, atol=1e-9) + + +@given(mixed_seqs) +def test_aa_fractions_valid_sum_to_one_invalid_all_nan(seqs): + sub = build_counts(seqs) + frac = aa_fractions(sub) + for i in range(len(seqs)): + if sub.valid[i]: + assert np.isclose(frac[i].sum(), 1.0, atol=1e-9) + else: + assert np.all(np.isnan(frac[i])) + + +@given(mixed_seqs) +def test_pi_in_range_or_nan(seqs): + # Every defined pI sits in the bisection bracket [0, 14]; invalid -> NaN. + pi = isoelectric_point(build_counts(seqs), IPC2_PEPTIDE, include_cys=True) + finite = pi[~np.isnan(pi)] + assert np.all((finite >= 0.0) & (finite <= 14.0)) + + +@given( + valid_seqs, + st.floats(min_value=0.5, max_value=13.5), + st.floats(min_value=0.5, max_value=13.5), +) +def test_charge_monotonic_in_ph(seqs, ph_a, ph_b): + lo, hi = sorted((ph_a, ph_b)) + sub = build_counts(seqs) + c_lo = charge_at_ph(sub, lo, IPC2_PEPTIDE, include_cys=True) + c_hi = charge_at_ph(sub, hi, IPC2_PEPTIDE, include_cys=True) + # Net charge is non-increasing in pH for every row. + assert np.all(c_lo >= c_hi - 1e-9) + + +@given(mixed_seqs) +def test_instability_floor(seqs): + # NaN below the effective-length floor (spec R9), finite at/above it. + ii = instability_index(seqs) + for i, s in enumerate(seqs): + if effective_length(s) < INSTABILITY_MIN_LENGTH: + assert np.isnan(ii[i]) + else: + assert not np.isnan(ii[i]) diff --git a/software/tests/unit/test_vectorized_substrate.py b/software/tests/unit/test_vectorized_substrate.py index 973c0c0..437f581 100644 --- a/software/tests/unit/test_vectorized_substrate.py +++ b/software/tests/unit/test_vectorized_substrate.py @@ -30,6 +30,32 @@ def test_handles_none_and_empty(): assert int(sub.length[4]) == 1 +def test_non_ascii_chars_dropped_not_expanded(): + """build_counts cleans via a single latin-1 byte per character, applied + BEFORE upper-casing — so each character is either one standard-AA byte or + dropped, and the standard-residue count can never exceed the input length. + + This DIVERGES by construction from the scalar oracle's str.upper()-then- + filter: Python's str.upper() case-folds some non-ASCII chars to MULTIPLE + ASCII letters ('ß' -> 'SS', the 'fi' ligature -> 'FI'), which the oracle + would then count, whereas the single-byte gather drops them. Real input is + ASCII single-letter AA codes, so this never fires in production. The test + pins the intentional behavior so a future "match str.upper exactly" change + is a deliberate decision, not an accident — and `test_counts_match_scalar_ + oracle` above only generates ASCII, so it never exercises this seam. + """ + # 'ß' (U+00DF) is one latin-1 byte (0xDF); the 'fi' ligature (U+FB01) is not + # latin-1, so `errors="replace"` maps it to '?' (0x3F). Both map to -1 in the + # byte table -> dropped. Neither is expanded to the SS / FI str.upper() would. + sub = build_counts(["ACßDE", "ACfiDE"]) + assert int(sub.length[0]) == 4 # ß dropped, NOT expanded to SS (would be 6) + assert int(sub.length[1]) == 4 # fi-ligature dropped, NOT expanded to FI + for i in range(2): + counts = dict(zip(STANDARD_AAS, sub.counts[i].tolist())) + assert counts["A"] == counts["C"] == counts["D"] == counts["E"] == 1 + assert sum(sub.counts[i].tolist()) == 4 # exactly A, C, D, E — no S/F/I + + def test_byte_table_kept_set_matches_oracle(): """Structural sync guard: the vectorized byte->AA table keeps a byte iff the oracle's cleaning rule keeps that character. A future change to STANDARD_AAS From 28aeb22b0fc2953649a2e3e69daac5354141897f Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 14:23:48 -0700 Subject: [PATCH 19/21] Set compute-properties mem allocation to 12GiB ~4.85 GB/1M optimized footprint; 12GiB covers ~2M clones with headroom (was 16GiB). --- workflow/src/main.tpl.tengo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workflow/src/main.tpl.tengo b/workflow/src/main.tpl.tengo index 17335db..a312089 100644 --- a/workflow/src/main.tpl.tengo +++ b/workflow/src/main.tpl.tengo @@ -343,11 +343,11 @@ wf.body(func(args) { // median CDR-H3 length per chain). soft := assets.importSoftware("@platforma-open/milaboratories.sequence-properties.software:compute-properties") // Measured optimized footprint of the vectorized Python step: ~4.85 GB peak - // RSS per 1M full-paired antibody clones (so ~9.7 GB at 2M). 16GiB covers 2M - // with ~1.5x headroom; cpu(1) stays single-threaded by design. + // RSS per 1M full-paired antibody clones (so ~9.7 GB at 2M). 12GiB covers ~2M + // with headroom; cpu(1) stays single-threaded by design. pyRun := exec.builder(). software(soft). - mem("16GiB"). + mem("12GiB"). cpu(1). addFile("input.tsv", seqTable). writeFile("plan.json", canonical.encode(plan)). From 74b1b4e14f3138fdca109d515a8a9827fd7013b0 Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 18:50:39 -0700 Subject: [PATCH 20/21] docs(tests): add README mapping the test suite (oracle vs engine, unit/integration/data) --- software/tests/README.md | 86 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 software/tests/README.md diff --git a/software/tests/README.md b/software/tests/README.md new file mode 100644 index 0000000..90b9681 --- /dev/null +++ b/software/tests/README.md @@ -0,0 +1,86 @@ +# Sequence-properties test suite + +These tests guard one rewrite: replacing the per-row BioPython compute (`src/properties.py`) with a vectorized numpy/polars engine (`src/vectorized.py`) without changing a single emitted value. The suite exists to make that swap provably safe. + +## Mental model: oracle vs. engine + +Two implementations of the same biophysics live in `src/`: + +- **`properties.py`** — the scalar BioPython code. Per-row and slow, **retained unchanged as the reference oracle.** +- **`vectorized.py`** — the array-math engine that actually ships. + +The engine earns trust against the oracle three independent ways: + +1. **Per-function parity** (`unit/test_vectorized_*.py`) — each engine function equals the oracle within `1e-6` over random sequences. +2. **Pipeline-composition parity** (`integration/test_pipeline_oracle_parity.py`) — the whole pipeline matches the oracle cell-for-cell, catching wiring the per-function tests miss (pKa set, Fv pairing, column naming). +3. **Byte-exact golden snapshot** (`integration/test_characterization_snapshot.py`) — the pipeline's quantized output reproduces committed bytes exactly. + +The golden snapshot pins *whatever the current code produces*, not an independent answer. Closed-form and literature-referenced tests (`unit/test_properties.py`, `unit/test_m3_validation.py`) anchor the **oracle itself** to ground truth, so the chain runs ground-truth → oracle → engine. + +## Layout + +### Top level + +| File | Purpose | +|------|---------| +| `conftest.py` | Shared fixtures — synthetic antibody frames and plans reused across suites. | +| `_corpus_gen.py` | Regenerates the characterization corpus and golden snapshot. Seeded (`Random(0)`) and idempotent. | +| `__init__.py` | Marks the package so `python -m tests._corpus_gen` resolves. | + +### `unit/` — fast, one function at a time + +| File | What it checks | +|------|----------------| +| `test_properties.py` | Scalar oracle behavior against closed-form and literature values. | +| `test_golden_values.py` | Pins the oracle's BioPython numbers for representative sequences (refactor-drift guard). | +| `test_invariants.py` | Hypothesis invariants on the oracle — charge monotonic in pH, AA fractions sum to 1, pI in `[0, 14]`. | +| `test_io_layer.py` | TSV in → frame → TSV out preserves entity keys and per-cell NA. | +| `test_m3_validation.py` | External cross-checks — pinned IPC 2.0 webserver pI, and charge by hand-computed Henderson-Hasselbalch. | +| `test_pipeline.py` | Mode dispatch (peptide / antibody-tcr), output column sets, per-clone NA propagation, Fv gating. | +| `test_quantization.py` | The quantized-equal CID contract — per-family rounding below display precision. | +| `test_vectorized_substrate.py` | The `(N×20)` count-matrix substrate matches the oracle's counts, length, and validity. | +| `test_vectorized_linear.py` | Parity for gravy, mw, aromaticity, aliphatic, extinction, AA fractions. | +| `test_vectorized_charge.py` | Charge and ΔCharge parity across both pKa sets and both `include_cys` settings. | +| `test_vectorized_pi.py` | pI bisection — bit-identical to the oracle (same bracket, tol, iteration count). | +| `test_vectorized_fv_pi.py` | Fv pI parity — bisects the summed per-chain charge. | +| `test_vectorized_instability.py` | Instability index parity — the one order-dependent property. | +| `test_vectorized_invariants.py` | The `test_invariants.py` invariants, asserted on the shipped engine. | + +### `integration/` — whole pipeline, end to end + +| File | What it checks | +|------|----------------| +| `test_characterization_snapshot.py` | Byte-exact whole-frame golden over 7 modes and edge cases — the safety net. | +| `test_pipeline_oracle_parity.py` | Pipeline composition vs. oracle, cell-for-cell, within the CID quantization. | +| `test_corpus_e2e.py` | Hand-crafted corpus plus `manifest.json`; one parametrized case per clonotype, so a failure names the exact sequence. | +| `test_cli.py` | `main()` with file args — the contract the Tengo workflow calls. | +| `test_determinism.py` | Cross-process byte-stability — spawns the CLI in fresh processes to catch hash-seed-dependent ordering. | +| `test_perf_benchmark.py` | `@slow` wall-clock benchmark. Excluded from the CI gate; the real number goes in the PR. | + +### `data/` + +- **`characterization/`** — generated by `_corpus_gen.py`. Per-mode `*_input.tsv` + `*_plan.json` inputs, and `golden/` holding the frozen `*.properties.tsv` / `*.stats.json` (plus `peptide.aa_fraction.tsv`) for 7 cases: antibody bulk-full, antibody CDR3-only, antibody partial, peptide, single-cell dropout, TCR αβ, TCR γδ. +- **`corpus/`** — hand-crafted end-to-end corpus plus `manifest.json`. Has its own `README.md` describing the manifest's `na` / exact-number / `{min, max}` assertion shapes. + +## Running + +From `blocks/sequence-properties/software/`: + +```bash +uv sync +uv run pytest -m "not slow" # what CI runs +uv run pytest # full suite, including the slow benchmark +uv run pytest tests/integration/test_perf_benchmark.py -m slow # just the benchmark +``` + +A bare `uv run pytest` runs everything — the `slow` marker is defined but not deselected by default (`pyproject.toml [tool.pytest.ini_options]`). Pass `-m "not slow"` to match CI. + +## Regenerating the golden + +Regenerate intentionally, never to paper over a diff: + +```bash +uv run python -m tests._corpus_gen --write-golden +``` + +The generator is seeded and idempotent — identical inputs produce byte-identical golden. After regenerating, inspect the diff: the only expected changes are the ones your code change explains. A surprise diff is a regression, not a stale snapshot. From 36e7aabbff12265f9f352d5509cbac5413b05b1c Mon Sep 17 00:00:00 2001 From: Paul Newling Date: Wed, 10 Jun 2026 18:50:39 -0700 Subject: [PATCH 21/21] chore(workflow): trim compute-properties mem comment to drop machine-specific RSS figures --- workflow/src/main.tpl.tengo | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/workflow/src/main.tpl.tengo b/workflow/src/main.tpl.tengo index a312089..603fcf7 100644 --- a/workflow/src/main.tpl.tengo +++ b/workflow/src/main.tpl.tengo @@ -342,9 +342,9 @@ wf.body(func(args) { // stats.json (dataset-level scalars consumed by the info layer — e.g. R11c // median CDR-H3 length per chain). soft := assets.importSoftware("@platforma-open/milaboratories.sequence-properties.software:compute-properties") - // Measured optimized footprint of the vectorized Python step: ~4.85 GB peak - // RSS per 1M full-paired antibody clones (so ~9.7 GB at 2M). 12GiB covers ~2M - // with headroom; cpu(1) stays single-threaded by design. + // 12GiB (up from the 4GiB default): the vectorized step materializes full-N + // count matrices, so peak memory scales with clone count. cpu(1) is + // deliberate — the engine is single-threaded for deterministic output. pyRun := exec.builder(). software(soft). mem("12GiB").