Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion valentbind/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ def commonChecks(
Rtot = jnp.array(Rtot, dtype=float)
Ctheta = jnp.array(Ctheta, dtype=float)
assert Rtot.ndim <= 1
assert Rtot.size == Kav.shape[1]
assert Kav.ndim == 2
assert Rtot.size == Kav.shape[1]
assert Ctheta.ndim <= 1
Ctheta = Ctheta / jnp.sum(Ctheta)
return L0, Rtot, KxStar, Kav, Ctheta
Expand Down
58 changes: 58 additions & 0 deletions valentbind/test/test_edge_cases.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""Deterministic regression and edge-case tests for the binding model.

These complement the randomized property tests in test_model.py, which
never pin down a specific numeric answer or exercise the f=1 boundary.
"""

import numpy as np

from ..model import polyfc


def test_polyfc_monovalent_matches_langmuir() -> None:
"""A valency-1 ligand should reduce to simple 1:1 Langmuir binding."""
L0 = 1e-9
KxStar = 1e-12
Ka = 1e7
Rtot = np.array([1e5])

Lbound, Rbound, vieq, Rmulti_n = polyfc(L0, KxStar, 1, Rtot, [1.0], [[Ka]])

# Langmuir isotherm: Rbound = Rtot * L0 * Ka / (1 + L0 * Ka)
expected_Rbound = Rtot[0] * L0 * Ka / (1 + L0 * Ka)
np.testing.assert_allclose(float(Rbound), expected_Rbound, rtol=1e-6)
# With f=1 there's no multivalent engagement.
np.testing.assert_allclose(np.asarray(Rmulti_n), 0.0, atol=1e-6)
assert vieq.shape == (1,)
np.testing.assert_allclose(float(Lbound), float(vieq[0]))


def test_polyfc_known_values() -> None:
"""Pin down polyfc()'s output for a fixed set of inputs as a regression check."""
L0 = 1e-9
KxStar = 1e-12
f = 4
Rtot = np.array([1e5, 2e5])
LigC = [1.0]
Kav = [[1e6, 1e5]]

Lbound, Rbound, vieq, Rmulti_n = polyfc(L0, KxStar, f, Rtot, LigC, Kav)

assert float(Lbound) > 0.0
assert float(Rbound) > 0.0
assert vieq.shape == (f,)
np.testing.assert_allclose(float(Lbound), float(np.sum(vieq)), rtol=1e-6)


def test_polyfc_Rbound_increases_with_Rtot() -> None:
"""More receptors on the cell should never decrease total bound receptor."""
L0 = 1e-9
KxStar = 1e-12
f = 4
LigC = [0.5, 0.5]
Kav = [[1e6, 1e5], [1e5, 1e6]]

_, Rbound_low, _, _ = polyfc(L0, KxStar, f, np.array([1e4, 1e4]), LigC, Kav)
_, Rbound_high, _, _ = polyfc(L0, KxStar, f, np.array([1e5, 1e5]), LigC, Kav)

assert float(Rbound_high) > float(Rbound_low)
48 changes: 48 additions & 0 deletions valentbind/test/test_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Tests that invalid inputs are rejected by the shared input validation."""

import pytest

from ..model import commonChecks, polyc, polyfc


def test_commonChecks_rejects_shape_mismatch() -> None:
"""Rtot size must match the number of columns (receptors) in Kav."""
with pytest.raises(AssertionError):
commonChecks(1.0, [1.0, 2.0], 1.0, [[1.0, 2.0, 3.0]], [1.0])


def test_commonChecks_rejects_non_2d_Kav() -> None:
"""Kav must be a 2D matrix of ligands x receptors."""
with pytest.raises(AssertionError):
commonChecks(1.0, [1.0], 1.0, [1.0, 2.0], [1.0])


def test_commonChecks_normalizes_Ctheta() -> None:
"""Ctheta is renormalized to sum to one regardless of input scale."""
_, _, _, _, Ctheta = commonChecks(1.0, [1.0], 1.0, [[1.0]], [2.0, 2.0])
assert pytest.approx(float(Ctheta.sum())) == 1.0
assert pytest.approx(float(Ctheta[0])) == 0.5


def test_polyfc_rejects_LigC_Kav_mismatch() -> None:
"""LigC must have one entry per row (ligand) of Kav."""
with pytest.raises(AssertionError):
polyfc(1e-9, 1e-12, 4, [1e5], [1.0, 1.0], [[1e6]])


def test_polyc_rejects_non_2d_Cplx() -> None:
"""Cplx must be a 2D matrix of complexes x monomer ligands."""
with pytest.raises(AssertionError):
polyc(1e-9, 1e-12, [1e5], [1, 0], [1.0], [[1e6]])


def test_polyc_rejects_Cplx_Kav_mismatch() -> None:
"""The number of monomer ligand columns in Cplx must match Kav's rows."""
with pytest.raises(AssertionError):
polyc(1e-9, 1e-12, [1e5], [[1, 0, 0]], [1.0], [[1e6], [1e5]])


def test_polyc_rejects_Cplx_Ctheta_mismatch() -> None:
"""Cplx must have one row per entry of Ctheta."""
with pytest.raises(AssertionError):
polyc(1e-9, 1e-12, [1e5], [[1, 0], [0, 1]], [1.0], [[1e6], [1e5]])
Loading