Skip to content

API reference

Public API

valentbind.polyfc(L0, KxStar, f, Rtot, LigC, Kav)

Solve the multivalent binding model for a single homogeneous ligand complex.

Computes bound ligand, bound receptor, and per-valency binding statistics for a population of identical ligand complexes of valency f, each assembled from a fixed mixture of monomer ligands (LigC), binding a set of receptors (Rtot) with affinities Kav.

Parameters:

Name Type Description Default
L0 float

Concentration of ligand complexes.

required
KxStar float

Detailed-balance corrected cross-linking constant.

required
f int | float

Valency of the ligand complex.

required
Rtot ArrayLike

Total abundance of each receptor type on the cell.

required
LigC ArrayLike

Relative composition of monomer ligands within the complex; renormalized to sum to one.

required
Kav ArrayLike

Matrix of monomer ligand/receptor affinities (rows are ligands, columns are receptors).

required

Returns:

Type Description
tuple[Array, Array, Array, Array]

A tuple (Lbound, Rbound, vieq, Rmulti_n) where Lbound is the total concentration of bound ligand complex, Rbound is the total abundance of bound receptor (summed across receptor types), vieq is the concentration of complex bound by exactly i receptors for each valency i from 1 to f, and Rmulti_n is the abundance of each receptor type engaged in multivalent (more than one ligand-receptor bond) binding.

Source code in valentbind/model.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def polyfc(
    L0: float,
    KxStar: float,
    f: int | float,
    Rtot: npt.ArrayLike,
    LigC: npt.ArrayLike,
    Kav: npt.ArrayLike,
) -> tuple[jax.Array, jax.Array, jax.Array, jax.Array]:
    """
    Solve the multivalent binding model for a single homogeneous ligand complex.

    Computes bound ligand, bound receptor, and per-valency binding
    statistics for a population of identical ligand complexes of valency
    ``f``, each assembled from a fixed mixture of monomer ligands
    (``LigC``), binding a set of receptors (``Rtot``) with affinities
    ``Kav``.

    :param L0: Concentration of ligand complexes.
    :param KxStar: Detailed-balance corrected cross-linking constant.
    :param f: Valency of the ligand complex.
    :param Rtot: Total abundance of each receptor type on the cell.
    :param LigC: Relative composition of monomer ligands within the
        complex; renormalized to sum to one.
    :param Kav: Matrix of monomer ligand/receptor affinities (rows are
        ligands, columns are receptors).
    :return: A tuple ``(Lbound, Rbound, vieq, Rmulti_n)`` where ``Lbound``
        is the total concentration of bound ligand complex, ``Rbound`` is
        the total abundance of bound receptor (summed across receptor
        types), ``vieq`` is the concentration of complex bound by exactly
        ``i`` receptors for each valency ``i`` from 1 to ``f``, and
        ``Rmulti_n`` is the abundance of each receptor type engaged in
        multivalent (more than one ligand-receptor bond) binding.
    """
    # Data consistency check
    L0, Rtot, KxStar, Kav, LigC = commonChecks(L0, Rtot, KxStar, Kav, LigC)
    assert LigC.size == Kav.shape[0]

    A = jnp.dot(LigC.T, Kav)

    # Find Phisum by guaranteed bracketed bisection
    solver = opt.Bisection(rtol=1e-12, atol=1e-12)
    upper = jnp.maximum(jnp.dot(A * KxStar, Rtot.T), 1e-12)
    result = opt.root_find(
        Req_polyfc,
        solver,
        y0=jnp.array(0.0),
        args=(Rtot, L0, KxStar, f, A),
        options=dict(lower=jnp.array(0.0), upper=upper),
        throw=True,
    )
    Phisum = result.value

    Lbound = L0 / KxStar * ((1 + Phisum) ** f - 1)
    Rbound = L0 / KxStar * f * Phisum * (1 + Phisum) ** (f - 1)
    vieq = (
        L0
        / KxStar
        * binom(f, np.arange(1, f + 1))
        * jnp.power(Phisum, np.arange(1, f + 1))
    )

    Req_n = Rtot / (1.0 + L0 * f * A * (1 + Phisum) ** (f - 1))
    Phi_n = A * KxStar * Req_n
    assert jnp.isclose(Phisum, jnp.sum(Phi_n))
    Rmulti_n = L0 * f / KxStar * Phi_n * ((1 + Phisum) ** (f - 1) - 1)
    return Lbound, Rbound, vieq, Rmulti_n

valentbind.polyc(L0, KxStar, Rtot, Cplx, Ctheta, Kav)

Solve the multivalent binding model for a mixture of heterogeneous ligand complexes.

Computes bound ligand, bound receptor, and free ligand statistics for a population of ligand complexes that can differ in their monomer composition (Cplx), binding a set of receptors (Rtot) with affinities Kav.

Parameters:

Name Type Description Default
L0 float

Concentration of ligand complexes.

required
KxStar float

Detailed-balance corrected cross-linking constant.

required
Rtot ArrayLike

Total abundance of each receptor type on the cell.

required
Cplx ArrayLike

Monomer ligand composition of each complex; rows are complexes, columns are monomer ligand types.

required
Ctheta ArrayLike

Relative abundance of each complex; renormalized to sum to one.

required
Kav ArrayLike

Matrix of monomer ligand/receptor affinities (rows are ligands, columns are receptors).

required

Returns:

Type Description
tuple[Array, Array, Array]

A tuple (Lbound, Rbound, Lfbnd) where Lbound is the concentration of bound ligand complex for each complex type, Rbound is the abundance of bound receptor for each complex type and receptor type, and Lfbnd is the concentration of complex bound by exactly one receptor for each complex type.

Raises:

Type Description
AssertionError

If the shapes of Cplx, Kav, or Ctheta are inconsistent with one another.

Source code in valentbind/model.py
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
def polyc(
    L0: float,
    KxStar: float,
    Rtot: npt.ArrayLike,
    Cplx: npt.ArrayLike,
    Ctheta: npt.ArrayLike,
    Kav: npt.ArrayLike,
) -> tuple[jax.Array, jax.Array, jax.Array]:
    """
    Solve the multivalent binding model for a mixture of heterogeneous ligand complexes.

    Computes bound ligand, bound receptor, and free ligand statistics for
    a population of ligand complexes that can differ in their monomer
    composition (``Cplx``), binding a set of receptors (``Rtot``) with
    affinities ``Kav``.

    :param L0: Concentration of ligand complexes.
    :param KxStar: Detailed-balance corrected cross-linking constant.
    :param Rtot: Total abundance of each receptor type on the cell.
    :param Cplx: Monomer ligand composition of each complex; rows are
        complexes, columns are monomer ligand types.
    :param Ctheta: Relative abundance of each complex; renormalized to sum
        to one.
    :param Kav: Matrix of monomer ligand/receptor affinities (rows are
        ligands, columns are receptors).
    :raises AssertionError: If the shapes of ``Cplx``, ``Kav``, or
        ``Ctheta`` are inconsistent with one another.
    :return: A tuple ``(Lbound, Rbound, Lfbnd)`` where ``Lbound`` is the
        concentration of bound ligand complex for each complex type,
        ``Rbound`` is the abundance of bound receptor for each complex type
        and receptor type, and ``Lfbnd`` is the concentration of complex
        bound by exactly one receptor for each complex type.
    """
    # Consistency check
    L0, Rtot, KxStar, Kav, Ctheta = commonChecks(L0, Rtot, KxStar, Kav, Ctheta)
    Cplx = jnp.array(Cplx)
    assert Cplx.ndim == 2
    assert Kav.shape[0] == Cplx.shape[1]
    assert Cplx.shape[0] == Ctheta.size

    # Solve Req
    Req = Req_solve(Req_polyc, Rtot, L0, KxStar, Cplx, Ctheta, Kav)

    # Calculate the results
    Psi = Req.T * Kav * KxStar
    Psi = jnp.concatenate((Psi, jnp.ones((Kav.shape[0], 1))), axis=1)
    Psirs = jnp.sum(Psi, axis=1).reshape(-1, 1)
    Psinorm = (Psi / Psirs)[:, :-1]

    Lbound = L0 / KxStar * Ctheta * jnp.expm1(jnp.dot(Cplx, jnp.log(Psirs))).flatten()
    Rbound = (
        L0
        / KxStar
        * Ctheta.reshape(-1, 1)
        * jnp.dot(Cplx, Psinorm)
        * jnp.exp(jnp.dot(Cplx, jnp.log(Psirs)))
    )
    with np.errstate(divide="ignore"):
        Lfbnd = (
            L0
            / KxStar
            * Ctheta
            * jnp.exp(jnp.dot(Cplx, jnp.log(Psirs - 1.0))).flatten()
        )
    assert len(Lbound) == len(Ctheta)
    assert Rbound.shape[0] == len(Ctheta)
    assert Rbound.shape[1] == len(Rtot)
    return Lbound, Rbound, Lfbnd

Internals

These are used internally by polyfc/polyc but are documented here since they're useful when reading or extending the model.

valentbind.model.commonChecks(L0, Rtot, KxStar, Kav, Ctheta)

Validate and normalize the inputs shared by :func:polyfc and :func:polyc.

Converts Rtot, Kav, and Ctheta to jax arrays, checks that their shapes are mutually consistent, and normalizes Ctheta so it sums to one.

Parameters:

Name Type Description Default
L0 float

Concentration of ligand complexes.

required
Rtot ArrayLike

Total abundance of each receptor type on the cell.

required
KxStar float

Detailed-balance corrected cross-linking constant.

required
Kav ArrayLike

Matrix of monomer ligand/receptor affinities (rows are ligands, columns are receptors).

required
Ctheta ArrayLike

Relative abundance of each ligand or complex; renormalized to sum to one.

required

Returns:

Type Description
tuple[float, Array, float, Array, Array]

The tuple (L0, Rtot, KxStar, Kav, Ctheta) with Rtot, Kav, and Ctheta converted to arrays and Ctheta normalized.

Raises:

Type Description
AssertionError

If the shapes of Rtot, Kav, or Ctheta are inconsistent with one another.

Source code in valentbind/model.py
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
def commonChecks(
    L0: float,
    Rtot: npt.ArrayLike,
    KxStar: float,
    Kav: npt.ArrayLike,
    Ctheta: npt.ArrayLike,
) -> tuple[float, jax.Array, float, jax.Array, jax.Array]:
    """
    Validate and normalize the inputs shared by :func:`polyfc` and :func:`polyc`.

    Converts ``Rtot``, ``Kav``, and ``Ctheta`` to ``jax`` arrays, checks
    that their shapes are mutually consistent, and normalizes ``Ctheta`` so
    it sums to one.

    :param L0: Concentration of ligand complexes.
    :param Rtot: Total abundance of each receptor type on the cell.
    :param KxStar: Detailed-balance corrected cross-linking constant.
    :param Kav: Matrix of monomer ligand/receptor affinities (rows are
        ligands, columns are receptors).
    :param Ctheta: Relative abundance of each ligand or complex; renormalized
        to sum to one.
    :raises AssertionError: If the shapes of ``Rtot``, ``Kav``, or
        ``Ctheta`` are inconsistent with one another.
    :return: The tuple ``(L0, Rtot, KxStar, Kav, Ctheta)`` with ``Rtot``,
        ``Kav``, and ``Ctheta`` converted to arrays and ``Ctheta``
        normalized.
    """
    Kav = jnp.array(Kav, dtype=float)
    Rtot = jnp.array(Rtot, dtype=float)
    Ctheta = jnp.array(Ctheta, dtype=float)
    assert Rtot.ndim <= 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

valentbind.model.Req_polyfc(Phisum, args)

Mass balance residual for the homogeneous-ligand (polyfc) binding model.

This is the root-finding target passed to the solver in :func:polyfc; it is zero when Phisum is the free-receptor-weighted binding potential that is consistent with the mass balance for the total receptor and free ligand concentrations.

Parameters:

Name Type Description Default
Phisum Array

Current guess for the binding potential (a scalar array, since the model reduces to a single scalar unknown).

required
args tuple[Array, float, float, int | float, Array]

Tuple of (Rtot, L0, KxStar, f, A) where Rtot is the total receptor abundance per receptor type, L0 is the total ligand complex concentration, KxStar is the detailed-balance corrected cross-linking constant, f is the ligand valency, and A is the ligand-composition-weighted affinity vector.

required

Returns:

Type Description
Array

The residual Phisum - sum(A * KxStar * Req), which the solver drives to zero.

Source code in valentbind/model.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def Req_polyfc(
    Phisum: jax.Array,
    args: tuple[jax.Array, float, float, int | float, jax.Array],
) -> jax.Array:
    """
    Mass balance residual for the homogeneous-ligand (polyfc) binding model.

    This is the root-finding target passed to the solver in :func:`polyfc`;
    it is zero when ``Phisum`` is the free-receptor-weighted binding
    potential that is consistent with the mass balance for the total
    receptor and free ligand concentrations.

    :param Phisum: Current guess for the binding potential (a scalar array,
        since the model reduces to a single scalar unknown).
    :param args: Tuple of ``(Rtot, L0, KxStar, f, A)`` where ``Rtot`` is the
        total receptor abundance per receptor type, ``L0`` is the total
        ligand complex concentration, ``KxStar`` is the detailed-balance
        corrected cross-linking constant, ``f`` is the ligand valency, and
        ``A`` is the ligand-composition-weighted affinity vector.
    :return: The residual ``Phisum - sum(A * KxStar * Req)``, which the
        solver drives to zero.
    """
    Rtot, L0, KxStar, f, A = args
    Req = Rtot / (1.0 + L0 * f * A * (1 + Phisum) ** (f - 1))
    return Phisum - jnp.dot(A * KxStar, Req.T)

valentbind.model.Req_polyc(log_Req, args)

Mass balance residual in log-space for the heterogeneous-complex (polyc) binding model.

This is the root-finding target passed to the solver in :func:polyc; it is zero when log_Req is the natural logarithm of free-receptor abundances consistent with the mass balance for every receptor type.

Parameters:

Name Type Description Default
log_Req Array

Current guess for the log of free receptor abundance per receptor type.

required
args tuple[Array, float, float, Array, Array, Array]

Tuple of (Rtot, L0, KxStar, Cplx, Ctheta, Kav) where Rtot is the total receptor abundance per receptor type, L0 is the total ligand complex concentration, KxStar is the detailed-balance corrected cross-linking constant, Cplx is the monomer composition of each ligand complex, Ctheta is the relative abundance of each complex, and Kav is the monomer ligand/receptor affinity matrix.

required

Returns:

Type Description
Array

The log-ratio residual log(Req + Rbound) - log(Rtot), which the solver drives to zero.

Source code in valentbind/model.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def Req_polyc(
    log_Req: jax.Array,
    args: tuple[jax.Array, float, float, jax.Array, jax.Array, jax.Array],
) -> jax.Array:
    """
    Mass balance residual in log-space for the heterogeneous-complex (polyc)
    binding model.

    This is the root-finding target passed to the solver in :func:`polyc`;
    it is zero when ``log_Req`` is the natural logarithm of free-receptor abundances
    consistent with the mass balance for every receptor type.

    :param log_Req: Current guess for the log of free receptor abundance per receptor
        type.
    :param args: Tuple of ``(Rtot, L0, KxStar, Cplx, Ctheta, Kav)`` where
        ``Rtot`` is the total receptor abundance per receptor type, ``L0``
        is the total ligand complex concentration, ``KxStar`` is the
        detailed-balance corrected cross-linking constant, ``Cplx`` is the
        monomer composition of each ligand complex, ``Ctheta`` is the
        relative abundance of each complex, and ``Kav`` is the monomer
        ligand/receptor affinity matrix.
    :return: The log-ratio residual ``log(Req + Rbound) - log(Rtot)``, which the solver
        drives to zero.
    """
    Rtot, L0, KxStar, Cplx, Ctheta, Kav = args
    Req = jnp.exp(log_Req)
    Psi = Req * Kav * KxStar
    Psirs = Psi.sum(axis=1).reshape(-1, 1) + 1
    Psinorm = Psi / Psirs

    Rbound = (
        L0
        / KxStar
        * jnp.sum(
            Ctheta.reshape(-1, 1)
            * jnp.dot(Cplx, Psinorm)
            * jnp.exp(jnp.dot(Cplx, jnp.log1p(Psirs - 1))),
            axis=0,
        )
    )
    return jnp.log(Req + Rbound) - jnp.log(Rtot)

valentbind.model.Req_solve(func, Rtot, L0, KxStar, Cplx, Ctheta, Kav)

Run Levenberg-Marquardt root finding in log-space to calculate the free receptor vector.

Initializes from an analytical 1:1 Langmuir binding approximation to ensure rapid and robust convergence.

Parameters:

Name Type Description Default
func Callable[..., Array]

Residual function to find the root of in log space; called as func(log_Req, (Rtot, L0, KxStar, Cplx, Ctheta, Kav)).

required
Rtot Array

Total abundance of each receptor type on the cell.

required
L0 float

Total ligand complex concentration.

required
KxStar float

Detailed-balance corrected cross-linking constant.

required
Cplx Array

Monomer ligand composition of each complex.

required
Ctheta Array

Relative abundance of each complex.

required
Kav Array

Matrix of monomer ligand/receptor affinities.

required

Returns:

Type Description
Array

The free receptor abundance vector Req that zeroes func.

Source code in valentbind/model.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
def Req_solve(
    func: Callable[..., jax.Array],
    Rtot: jax.Array,
    L0: float,
    KxStar: float,
    Cplx: jax.Array,
    Ctheta: jax.Array,
    Kav: jax.Array,
) -> jax.Array:
    """
    Run Levenberg-Marquardt root finding in log-space to calculate the free
    receptor vector.

    Initializes from an analytical 1:1 Langmuir binding approximation to ensure
    rapid and robust convergence.

    :param func: Residual function to find the root of in log space; called as
        ``func(log_Req, (Rtot, L0, KxStar, Cplx, Ctheta, Kav))``.
    :param Rtot: Total abundance of each receptor type on the cell.
    :param L0: Total ligand complex concentration.
    :param KxStar: Detailed-balance corrected cross-linking constant.
    :param Cplx: Monomer ligand composition of each complex.
    :param Ctheta: Relative abundance of each complex.
    :param Kav: Matrix of monomer ligand/receptor affinities.
    :return: The free receptor abundance vector ``Req`` that zeroes
        ``func``.
    """
    L_monomer = jnp.dot(Ctheta, Cplx) * L0
    A_eff = jnp.dot(L_monomer, Kav)
    Req_init = Rtot / (1.0 + A_eff)
    log_Req_0 = jnp.log(jnp.maximum(Req_init, 1e-30))

    solver = opt.LevenbergMarquardt(rtol=1e-10, atol=1e-10)
    result = opt.root_find(
        func,
        solver,
        y0=log_Req_0,
        args=(Rtot, L0, KxStar, Cplx, Ctheta, Kav),
        throw=True,
    )
    return jnp.exp(result.value)