Chapter 4

Chapter 4: Binding, PK/PD and the Calculation API

How strongly a biologic binds its target, how long it stays in the body, and how much effect a given concentration produces: the equations behind these three questions, how BiopharmaSDK implements them, and where the implementation should not be trusted.

Chapter 3 described the WIA-BIO-016 type model as the standard's data format. This chapter moves from records to calculations. The standard's calculation API is small: four methods on BiopharmaSDK (calculateBindingAffinity, calculateKinetics, calculatePharmacokinetics, calculatePharmacodynamics), three of which are also exported as standalone functions, plus the calc-affinity, calc-pk, simulate-pk and simulate-pkpd commands of the wia-bio-016 shell tool. Behind them sit textbook relationships that every translational scientist already uses: the law of mass action, first-order elimination and the Hill equation.

Because the formulas are simple, the interesting part is not the arithmetic but the contract around it: which units each field expects, which fields are recorded but ignored, which outputs are physical quantities and which are heuristics chosen by the authors of the reference code. Every number printed from the SDK in this chapter was produced by running the v1.0 source, and every place where the code disagrees with the specification or with pharmacology is stated explicitly. Section 4.6 collects those defects; section 4.7 reworks the specification's own Appendix A examples.

4.1 Equilibrium Binding and calculateBindingAffinity()

The dissociation constant

For a single binding site reacting reversibly with its antigen, Ab + Ag ⇌ Ab·Ag, the law of mass action gives the equilibrium dissociation constant defined in spec §4.1.1:

Kd = [Ab]free × [Ag]free / [Ab·Ag]        (units: M = mol/L)

Kd is a concentration. Its most useful reading is that, when the ligand is in excess, Kd is the free ligand concentration at which half of the binding sites are occupied. A smaller Kd therefore means tighter binding. Kd is also directly linked to the standard free energy of binding through ΔG° = RT·ln(Kd/c°), with c° = 1 M. At 25 °C, RT is about 2.48 kJ/mol, so a Kd of 1 nM corresponds to ΔG° ≈ −51.4 kJ/mol (about −12.3 kcal/mol), and every tenfold improvement in Kd is worth only about 5.7 kJ/mol (1.36 kcal/mol). This logarithmic relationship explains why affinity maturation campaigns talk about "orders of magnitude" rather than percentages.

Mass balance: why the SDK asks for totals and the complex

In an experiment one usually knows the total amounts added and measures the amount of complex. The free species follow from mass balance:

[Ab]free = [Ab]total − [Ab·Ag]
[Ag]free = [Ag]total − [Ab·Ag]

This is exactly what calculateBindingAffinity() does. Its input type BindingAffinity has antibodyConc, antigenConc and complexConc (all in M), a required temperature in °C and optional pH and buffer. The method subtracts the complex from each total, computes Kd, and returns a BindingAffinityResult with six fields: kd, freeAntibody, freeAntigen, bindingStrength, fractionBound (complex divided by total antibody) and therapeuticSuitability.

Subtracting the complex matters whenever the concentrations are close to Kd. Many simple textbook treatments assume that the free ligand equals the total ligand, which is only valid when the ligand is in large excess over the binding sites. When that assumption fails (so-called ligand depletion), the forward problem, predicting how much complex will form from known totals and Kd, requires the exact solution of the quadratic:

[Ab·Ag] = ( S − √(S² − 4·[Ab]total·[Ag]total) ) / 2,   where S = [Ab]total + [Ag]total + Kd

The SDK does not provide this forward function, but it is a useful cross-check: plugging the Kd returned by the SDK back into the quadratic must reproduce the measured complex. Section 4.7 performs that check on Appendix A.1.

Validation rules and what is silently ignored

The method throws a BiopharmaError with code BIO007 (INVALID_PARAMETERS) in two situations: when either total concentration is zero or negative or the complex is negative, and when the complex exceeds either total. A complex of exactly zero is accepted; the method then returns kd: Infinity, bindingStrength: 'very-low' and fractionBound: 0, which is arithmetically consistent but deserves a guard in any user interface.

The temperature, pH and buffer fields are recorded but never used. That is acceptable for a pure equilibrium calculation, but it means the same record gives the same Kd whether it was measured at 25 °C or 37 °C. Affinity does change with temperature, and assay buffers influence it as well, so these fields must travel with the result into any comparison or report. They are metadata that makes a Kd interpretable, not inputs that change it.

Affinity bands: specification versus code

Spec §4.1.2 names five affinity bands; the SDK implements six, splitting the specification's open-ended "low" band at 100 nM. The table places the three sources side by side. The CLI column anticipates a defect discussed in 4.6: in the shipped shell script every label except the last is unreachable.

Kd rangeSpec §4.1.2 labelbindingStrength (SDK)CLI calc-affinitytherapeuticSuitability
< 1 pMUltra-highultra-highULTRA-HIGH (unreachable)true
1 pM – 100 pMVery highvery-highVERY HIGH (unreachable)true
100 pM – 1 nMHighhighHIGH (unreachable)true
1 nM – 10 nMModeratemoderateMODERATE (unreachable)true
10 nM – 100 nMLowlowLOWtrue only at exactly 10 nM
≥ 100 nMLowvery-lowLOWfalse

Two boundary rules follow from the code and are not written in the specification. First, every band includes its lower edge: the SDK compares with a strict "less than", so a Kd of exactly 1 nM is classified as moderate, not high. Second, therapeuticSuitability uses "less than or equal to" BIOPHARMA_CONSTANTS.MIN_THERAPEUTIC_KD (10 nM), while the classification uses "less than". A Kd of exactly 10 nM is therefore reported as low and suitable at the same time. Running the SDK with 20 nM of each partner and 10 nM of complex confirms this (kd: 1e-8, bindingStrength: 'low', therapeuticSuitability: true). The case is contrived but is exactly the kind of edge a conformance test should pin down.

Both the bands and the 10 nM flag are conventions of this standard, not regulatory criteria. Whether an affinity is "enough" depends on target abundance, the required occupancy, the dose that can be delivered and the mechanism of action. An agonist, a blocking antibody and an ADC that must be internalised can have very different requirements. Treat therapeuticSuitability as a triage flag for screening data, not as a statement that a molecule will work.

4.2 Binding Kinetics and calculateKinetics()

Rate constants and the complex half-life

Equilibrium hides the time course. Association is second order, with rate constant kon in M⁻¹s⁻¹; dissociation is first order, with koff in s⁻¹. At equilibrium the two rates are equal, which gives spec §4.2.1:

Kd = koff / kon
complex half-life = ln 2 / koff

calculateKinetics(kon, koff) returns exactly these two quantities in a KineticParameters record, after throwing BIO007 if either constant is not positive. The spec quotes typical therapeutic antibody ranges of 10⁵–10⁶ M⁻¹s⁻¹ for kon and 10⁻³–10⁻⁵ s⁻¹ for koff. Two runs of the SDK, with values chosen from inside those ranges for illustration, show why kinetics adds information:

Examplekon (M⁻¹s⁻¹)koff (s⁻¹)Kd (SDK)Complex half-life (SDK)
K-11 × 10⁵1 × 10⁻⁴1 × 10⁻⁹ M (1 nM)6,931 s ≈ 1.9 h
K-21 × 10⁶1 × 10⁻⁵1 × 10⁻¹¹ M (10 pM)69,315 s ≈ 19.3 h

A hundredfold difference in Kd is made of a tenfold gain in association and a tenfold gain in dissociation. Two molecules with the same Kd may still differ by orders of magnitude in how long each complex persists, which matters for mechanisms that depend on residence time. For that reason a characterization record should keep kon and koff, not only their ratio.

One implementation detail needs care. calculateKinetics() always writes method: 'spr' and temperature: 25 into its result, whatever instrument and temperature were actually used. The KineticParameters type allows 'spr' | 'bli' | 'ifa' | 'elisa' | 'kinexA', but the calculator never sees that information. Code that stores the returned object as an assay record must overwrite both fields with the real metadata; otherwise a BLI measurement at 37 °C will be archived as SPR at 25 °C. The method also has no standalone export, so it can only be called on an SDK instance.

Reading an SPR sensorgram with the 1:1 model

Surface plasmon resonance and bio-layer interferometry measure binding as a signal proportional to the mass captured on a surface. For a 1:1 interaction at steady state, spec §4.2.2 gives

Response = Rmax × C / (Kd + C)

where C is the analyte concentration in solution and Rmax is the signal when every immobilised site is occupied. At C = Kd the response is half of Rmax; at C = 9·Kd it is 90% of Rmax. Fitting a series of plateau responses against concentration is called steady-state affinity analysis.

The approach to that plateau follows a single exponential with observed rate kobs = kon·C + koff. For example K-1 at C = 1 nM, kobs = 10⁵ × 10⁻⁹ + 10⁻⁴ = 2 × 10⁻⁴ s⁻¹; reaching 95% of equilibrium takes about ln 20 / kobs ≈ 15,000 s, roughly four hours. Real injections are much shorter, which is why high-affinity antibodies are usually characterized by fitting the association and dissociation phases kinetically rather than by waiting for plateaus. A Kd reported from an unreached steady state is a common, and avoidable, error.

Avidity

An IgG has two identical binding arms. When the antigen is presented on a surface or a cell membrane at sufficient density, one arm can rebind before the whole molecule escapes, and the apparent dissociation becomes much slower than that of a single Fab arm. The resulting functional affinity, or avidity, can exceed the intrinsic monovalent affinity by an order of magnitude or more, depending on antigen density, geometry and format. Spec §4.3 quotes a generic enhancement range of 10–1000× over the monovalent Kd; in practice no general factor applies, and the effect depends on the assay set-up.

For data exchange this means that a kinetic record is only interpretable together with its orientation: whether the antibody was captured and the monomeric antigen flowed over it (which approximates intrinsic affinity), or the antigen was immobilised and the bivalent antibody was the analyte (which measures an avidity-influenced apparent value). KineticParameters has no field for orientation or valency. Until the type is extended, implementers should put this information in an accompanying assay description rather than lose it.

4.3 Pharmacokinetic Core and calculatePharmacokinetics()

The one-compartment model behind the SDK

The SDK uses the simplest pharmacokinetic model: a single well-mixed compartment of volume Vd from which drug is removed by first-order elimination with clearance CL. After an intravenous bolus the concentration decays as C(t) = C0·e^(−ke·t). All the relationships in spec §5.1 follow from that model:

AUC  = F × Dose / CL          (mg·h/L when Dose in mg, CL in L/h)
ke   = CL / Vd                (h⁻¹)
t½   = ln 2 / ke = 0.693 × Vd / CL   (h)
C0   = Dose / Vd              (mg/L, IV bolus only)

The input type PKParameters fixes the units: dose in mg, bioavailability between 0 and 1, clearance in L/h, volumeOfDistribution in L, a required route ('iv' | 'sc' | 'im' | 'oral' | 'inhalation') and an optional patientWeight in kg. The method throws BIO007 if dose, clearance or volume is not positive or if bioavailability lies outside [0, 1]. It returns a PKResult with auc, halfLife (hours), c0, ke and dosingInterval (hours). The type also declares optional cmax and tmax, which the method never fills.

What the model leaves out

Three simplifications are worth stating before using the numbers. First, route and patientWeight are not used in any calculation. Bioavailability enters only the AUC, and c0 is computed as Dose/Vd for every route. For a subcutaneous dose there is no C0 at all: absorption from the injection site is slow, and the peak arrives days later. Running the SDK with route: 'sc' and an illustrative F of 0.7 lowers the AUC accordingly but still reports the same C0 as the intravenous case. Consumers of PKResult should ignore c0 unless the route is an IV bolus.

Second, monoclonal antibodies are rarely well described by one compartment. Their concentration-time profiles after IV dosing usually show a distribution phase followed by a slower terminal phase, and are fitted with two-compartment models. Third, for antibodies against membrane targets, binding to the target itself can be a clearance route. This target-mediated drug disposition makes clearance concentration-dependent: faster at low concentrations when targets are unsaturated, and slower once they are saturated. A single CL value is then only valid over the concentration range in which it was estimated.

Why antibodies last for weeks

Small proteins below the renal filtration threshold are often cleared by the kidney within hours. IgG is too large for glomerular filtration and is removed mainly by pinocytosis and lysosomal catabolism. What makes it long-lived is the neonatal Fc receptor, FcRn. After uptake into endosomes, whose pH is mildly acidic, the Fc region binds FcRn; the complex is recycled to the cell surface, where the neutral pH of blood releases the antibody back into circulation instead of letting it be degraded. This pH-dependent salvage is why IgG1, IgG2 and IgG4 antibodies typically have half-lives of about two to three weeks, why Fc engineering that strengthens FcRn binding at acidic pH can extend half-life, and why fragments without an Fc region disappear much faster. The SDK's constants encode this background as IGG_HALF_LIFE: 21 (days) and FCRN_KD: 200e-9 (M, at pH 6.0); treat both as reference values of the model, not as properties of a particular product.

The same biology also bounds the volume of distribution. A 150 kDa protein crosses capillary walls slowly, so antibody Vd values are of the order of plasma volume plus part of the interstitial space. The specification's quoted range of 3–8 L for mAbs is in line with this; BIOPHARMA_CONSTANTS.PLASMA_VOLUME is 3.5 L.

The dosing-interval heuristic

dosingInterval is not a pharmacokinetic quantity. It is produced by three hard-coded rules in the reference implementation:

if (halfLife < 12)       dosingInterval = max(4, ceil(halfLife / 2))   // hours
else if (halfLife < 48)  dosingInterval = 24
else                     dosingInterval = ceil(halfLife)

The code comment says "typically 1–2 half-lives", which does not match the first rule. None of the rules looks at the therapeutic window, the target concentration or the exposure-response relationship, which are what actually set a dosing regimen. The field is useful for sorting simulated candidates into "hours", "daily" and "weeks" categories, and for nothing more. The next section shows how to derive a dose from a target concentration instead.

A concentration-time sketch

On a logarithmic concentration axis, one-compartment elimination is a straight line that halves once per half-life. The same picture describes both examples of section 4.7, which have identical C0 and ke values but in different time units. That coincidence is itself the lesson of section 4.4.

C (mg/L, log scale)
 20.0 ┤●
      │  ╲
 10.0 ┤    ●
      │      ╲
  5.0 ┤        ●
      │          ╲
  2.5 ┤            ●
      │              ╲
 1.25 ┤                ●
      └┬───────┬───────┬───────┬───────┬──→ time
       0      1 t½    2 t½    3 t½    4 t½

  Appendix A.2 protein : 1 t½ = 17.3 hours   (CL = 0.2 L/h,   Vd = 5 L)
  IgG example (4.7)    : 1 t½ = 17.3 days    (CL = 0.2 L/day, Vd = 5 L)

4.4 From Parameters to a Dose Regimen

Loading and maintenance doses

Spec §5.3 gives the two classical dosing equations. A loading dose fills the volume of distribution to the target concentration at once; a maintenance dose replaces what is cleared during each dosing interval τ:

Loading dose     = Vd × Css / F
Maintenance dose = CL × Css,avg × τ / F

The specification writes the maintenance dose without F, which is correct only for intravenous dosing. For any other route, divide by bioavailability, as for the loading dose. Css,avg is the average steady-state concentration over an interval, which is what AUC over one interval divided by τ measures. For multi-compartment drugs, the "Vd" in the loading equation should be chosen with care (the central volume if the goal is to avoid an initial overshoot, the steady-state volume if the goal is to fill all tissues), and a one-compartment calculation only gives a first estimate.

Accumulation and time to steady state

Without a loading dose, repeated dosing accumulates towards steady state. The accumulation ratio between steady-state and first-dose exposure over an interval is R = 1 / (1 − e^(−ke·τ)), and the approach to steady state depends only on the half-life: about 50% after one half-life, 90% after about 3.3, and 97% after five. For a molecule with a half-life of about 17 days, that means roughly two months before the average concentration is within 10% of its steady-state value. This is the practical reason loading doses and more frequent initial dosing are common for long-lived biologics.

Worked example (illustrative values, IV, F = 1): CL = 0.2 L/day, Vd = 5 L, so ke = 0.04 day⁻¹ and t½ ≈ 17.3 days. For an illustrative target Css,avg of 20 mg/L and τ = 14 days, the maintenance dose is 0.2 × 20 × 14 = 56 mg every two weeks, and a loading dose of 5 × 20 = 100 mg reaches the target at once. The accumulation ratio for this interval is 1 / (1 − e^(−0.56)) ≈ 2.3, and without the loading dose about 57 days (3.3 half-lives) would pass before 90% of steady state is reached. None of these numbers describes a real product; they show how the equations fit together.

Unit hygiene

The SDK is hard-wired to hours: CL in L/h, t½ and dosingInterval in hours, AUC in mg·h/L. Antibody clearance, however, is usually reported per day, and doses are often expressed in mg/kg. The table lists the conversions that must happen before data reaches the SDK. The PKParameters type has no unit fields, so nothing will warn you if they are skipped.

Quantity as reportedRequired by the SDKConversion
CL in L/dayL/hdivide by 24
CL in mL/h/kgL/h× body weight (kg) ÷ 1000
Dose in mg/kgmg× body weight (kg); patientWeight is not applied automatically
Concentration in µg/mLmg/Lnumerically identical (1 µg/mL = 1 mg/L)
AUC from the SDK (mg·h/L)mg·day/L for reportingdivide by 24
halfLife from the SDK (h)days for reportingdivide by 24

The SDK offers no regimen calculator, so applications have to write their own. A minimal helper that keeps units in the variable names might look like this; it is not part of WIA-BIO-016:

// Not part of the SDK: application-side helper, IV or known F
function regimen(clLperDay: number, vdL: number, cssMgPerL: number,
                 tauDays: number, f = 1) {
  const keperDay = clLperDay / vdL;
  return {
    loadingMg: (vdL * cssMgPerL) / f,
    maintenanceMg: (clLperDay * cssMgPerL * tauDays) / f,
    accumulation: 1 / (1 - Math.exp(-keperDay * tauDays)),
    halfLifeDays: Math.LN2 / keperDay,
  };
}

regimen(0.2, 5, 20, 14);
// => { loadingMg: 100, maintenanceMg: 56, accumulation: 2.33, halfLifeDays: 17.3 }

4.5 Pharmacodynamics and calculatePharmacodynamics()

Emax and Hill models

Pharmacodynamics links concentration to effect. The workhorse is the sigmoid Emax model, also known as the Hill equation:

E = E0 + Emax × Cⁿ / (EC50ⁿ + Cⁿ)

E0 is the baseline effect, Emax the maximal drug-induced effect, EC50 the concentration producing half of Emax, and n the Hill coefficient, which sets the steepness. With n = 1 (the ordinary Emax model of spec §5.2.1), going from 10% to 90% of the maximal effect needs an 81-fold increase in concentration; with n = 2 it needs only a ninefold increase. EC50 and Emax are estimated from data. They describe a particular endpoint in a particular system, and a biomarker EC50 in plasma is not the same number as a clinical-response EC50.

calculatePharmacodynamics() takes a PDParameters record: concentration and ec50 (both documented as mg/L), emax, an optional hillCoefficient (default 1) and an optional baseline (default 0). It throws BIO007 if the concentration or Emax is negative or EC50 is not positive, then returns effect, receptorOccupancy, effectPercentage, therapeuticRange and withinRange. The effect value is a correct implementation of the Hill equation. The other four outputs need the caveats of section 4.6.

Receptor occupancy is a binding quantity

Receptor occupancy, the fraction of target sites bound by drug, follows from the same mass action as section 4.1. With free drug in excess over the target:

RO = C / (Kd + C)

Here C and Kd must be in the same units. Serum antibody concentrations are reported in mass units, and Kd in molar units, so the molecular weight is needed. For an IgG of about 150 kDa, 1 nM corresponds to 10⁻⁹ mol/L × 150,000 g/mol = 1.5 × 10⁻⁴ g/L, that is 0.15 mg/L (0.15 µg/mL). The SDK's convertConcentration() helper cannot do this: it only converts between molar units and throws BIO007 ("Unit conversion not implemented for these units") for a mass unit, even when a molecular weight is passed.

Occupancy calculated this way is an idealisation. It assumes equilibrium, free drug concentrations and the Kd measured in vitro, typically at 25 °C in a purified system. It ignores target turnover and the fact that tissue concentrations can be much lower than plasma concentrations. Spec §5.2.2 states a target occupancy of above 80–90% "for therapeutic effect". That is a common planning assumption for blocking antibodies, not a general rule; the occupancy that matters depends on the mechanism and is ideally measured, for example by flow cytometry on blood cells, rather than calculated.

4.6 Known Defects: What to Teach, Test and Work Around

The reference implementation is short enough to read line by line, and doing so reveals a set of problems that anyone building on WIA-BIO-016 v1.0 should know. None of them is hidden. They are listed here so that implementations reproduce the standard's intent rather than its accidents.

#WhereDefectConsequenceWorkaround
D1calculatePharmacodynamicsOccupancy computed as C/(EC50 + C) instead of C/(Kd + C)Occupancy mirrors the effect curve; it is wrong whenever EC50 ≠ Kd (almost always)Compute RO yourself from Kd, with C and Kd in the same units
D2calculatePharmacodynamicstherapeuticRange = 0.5×EC50 to 9×EC50, labelled "50–90% of Emax"With n = 1 the lower edge gives 33% of Emax, not 50%; with n = 2 the range spans 20% to 98.8%Treat as a display heuristic; define the range from exposure-response data
D3calculatePharmacodynamicseffectPercentage = effect ÷ Emax × 100, with baseline included in effectExceeds 100% when a baseline is set; NaN when Emax = 0, which validation allowsUse (effect − E0) ÷ Emax, and reject Emax = 0
D4calculateKineticsHard-codes method: 'spr' and temperature: 25Stored records misreport the assay conditionsOverwrite both fields with the real assay metadata
D5calculatePharmacokineticsIgnores route and patientWeight; dosingInterval is a heuristicC0 reported for SC and oral doses; interval unrelated to the therapeutic windowUse c0 only for IV bolus; derive regimens as in 4.4
D6READMEImports simulatePharmacokinetics, which does not existThe sample does not compileUse calculatePharmacokinetics
D7index.ts, types.tsSymbols exported twice (declaration plus a trailing export { … } block)tsc fails with TS2323/TS2484, so the package cannot be built as shippedDelete the trailing re-export blocks
D8wia-bio-016 calc-affinitybc cannot parse scientific notation such as 1e-9Kd prints as 0 or the classification tests fail, and the result is always "LOW"Use the SDK, or replace bc with awk as shown in Chapter 7
D9Spec Appendix AA.1 labelled "High affinity"; A.2 labelled "BID or TID"A.1 contradicts the spec's own table and the code; A.2 contradicts the code's 24 h outputUse the corrected readings of 4.7

D1 deserves a closer look because it silently produces plausible numbers. For a 150 kDa antibody with Kd = 1 nM (0.15 mg/L), an EC50 of 1 mg/L and a concentration of 9 mg/L, the SDK reports receptorOccupancy: 0.9. The binding equation gives 9 / (0.15 + 9) ≈ 0.984. The gap is not a rounding issue: the two numbers answer different questions. Occupancy is fixed by affinity, while EC50 also depends on everything downstream of binding, including signalling amplification, receptor reserve and the chosen endpoint. When EC50 is well above Kd, which is common for functional readouts, the SDK understates occupancy; when EC50 is below Kd, it overstates it.

D7 affects anyone who tries to use the examples in this chapter. Compiling the v1.0 sources with TypeScript 5 stops at the first duplicate export, and Node.js's built-in TypeScript loader refuses the same modules for the same reason. Removing the final export { BiopharmaSDK }; line from index.ts and the closing export type { … } / export { … } block from types.ts is enough, because every symbol is already exported where it is declared. The README's npm install @wia/bio-016 line should also be read as a statement of intent: the examples below assume a local build from api/typescript, not a published package.

D8 is covered with the rest of the command-line tool in Chapter 7, but its effect on this chapter is simple: the calc-affinity command calculates the Kd correctly only when all three concentrations are given as plain decimals, and even then its classification lines compare against thresholds written as 1e-12, 100e-12 and so on, which bc also rejects, so the printed band is always "LOW" and suitability always "BORDERLINE/UNSUITABLE". The calc-pk command, whose defaults are plain decimals, reproduces the SDK's numbers.

4.7 Worked Examples: Appendix A Recomputed

A.1: binding affinity

Appendix A.1 gives 1 nM antibody, 10 nM antigen and 0.9 nM complex. The free species are 0.1 nM and 9.1 nM, so Kd = 0.1 × 9.1 / 0.9 ≈ 1.01 nM. Running the SDK:

import { BiopharmaSDK } from '../api/typescript/src';   // local build, D7 fixed

const sdk = new BiopharmaSDK();

const a1 = sdk.calculateBindingAffinity({
  antibodyConc: 1e-9,     // M, total
  antigenConc: 1e-8,      // M, total
  complexConc: 0.9e-9,    // M, measured
  temperature: 25,        // recorded, not used
});
console.log(a1);
// {
//   kd: 1.011111111111112e-9,              // ≈ 1.01 nM
//   freeAntibody: 1.0000000000000007e-10,  // ≈ 0.1 nM
//   freeAntigen: 9.1e-9,
//   bindingStrength: 'moderate',
//   fractionBound: 0.8999999999999999,
//   therapeuticSuitability: true
// }

The specification interprets this result as "High affinity (nM range)". That is inconsistent with its own table in §4.1.2, where 1–10 nM is "Moderate", and with the SDK, which classifies 1.01 nM as moderate. The corrected reading is: moderate affinity by the WIA-BIO-016 bands, and below the standard's 10 nM screening threshold, so therapeuticSuitability is true. Whether 1 nM is sufficient for a particular programme is a separate, target-specific question.

The quadratic of section 4.1 closes the loop. With totals of 1 and 10 nM and Kd = 1.0111 nM, S = 12.0111 nM and √(S² − 40) ≈ 10.2111 nM, so the predicted complex is (12.0111 − 10.2111) / 2 = 0.900 nM, matching the input. A conformance test can use exactly this round trip: any implementation whose Kd, fed back into the forward equation, does not reproduce the measured complex within floating-point tolerance is wrong. Note also that the SDK prints 1.0000000000000007e-10 for the free antibody. The subtraction of two nearly equal numbers in binary floating point is not exact, so tests should compare with a relative tolerance, never with strict equality.

A.2: pharmacokinetics, and the same numbers in days

Appendix A.2 uses a 100 mg IV dose, CL = 0.2 L/h and Vd = 5 L. The specification describes this as a hypothetical small therapeutic protein. The SDK reproduces its arithmetic. The table then adds an IgG-like case with the same numerical clearance expressed per day, 0.2 L/day, which is the order of magnitude typical for monoclonal antibodies. The SDK must receive it as 0.2 / 24 ≈ 0.00833 L/h.

const a2 = sdk.calculatePharmacokinetics({
  dose: 100, bioavailability: 1, clearance: 0.2,
  volumeOfDistribution: 5, route: 'iv',
});
// { auc: 500, halfLife: 17.325, c0: 20, ke: 0.04, dosingInterval: 24 }

const igg = sdk.calculatePharmacokinetics({
  dose: 100, bioavailability: 1, clearance: 0.2 / 24,   // 0.2 L/day
  volumeOfDistribution: 5, route: 'iv',
});
// { auc: 12000, halfLife: 415.8, c0: 20, ke: 0.001667, dosingInterval: 416 }
OutputA.2 protein (CL 0.2 L/h)IgG-like example (CL 0.2 L/day)
AUC500 mg·h/L12,000 mg·h/L (= 500 mg·day/L)
ke0.04 h⁻¹0.00167 h⁻¹ (= 0.04 day⁻¹)
t½17.3 h415.8 h ≈ 17.3 days
C020 mg/L20 mg/L
C at 24 h (C0·e^(−ke·t))≈ 7.7 mg/L≈ 19.2 mg/L
dosingInterval (heuristic)24 h416 h ≈ 17 days

Three lessons follow. First, the specification's interpretation "Dosing frequency: BID or TID" for A.2 disagrees with the reference code, which returns a 24-hour interval for any half-life between 12 and 48 hours. Neither is a dosing recommendation, and a conformance suite should test the code's documented rule rather than the prose. Second, A.2 must not be generalised: a clearance of 0.2 L/h is far faster than typical antibody clearance and would give an antibody a half-life of less than a day. Third, the two columns differ only in the time unit of clearance, and every derived quantity scales by exactly 24. A clearance entered per day into a field that expects per hour produces a half-life that is 24 times too short, with no error raised. That single unit slip is the most likely way to misuse this API.

Pharmacodynamics on the IgG example

Suppose, for illustration, that the IgG example has Kd = 1 nM (0.15 mg/L) against its target and an EC50 of 1 mg/L for some biomarker response, with n = 1. At the concentration reached 24 hours after dosing, about 19.2 mg/L, the Hill equation gives an effect of 19.2 / (1 + 19.2) ≈ 95% of Emax. The SDK reports the same value for effect and, because of D1, also 0.95 for receptorOccupancy. The binding equation gives 19.2 / (0.15 + 19.2) ≈ 0.992. Near saturation the two numbers look close, but in the trough of a dosing interval they diverge. At 1.35 mg/L, which is 9·Kd, true occupancy is 90% while the SDK's formula gives 1.35 / 2.35 ≈ 57%. Wherever occupancy drives a decision, such as the choice of the trough target in a dose-selection argument, compute it from Kd.

Chapter checklist

Review Questions

  1. An SPR experiment gives kon = 2 × 10⁵ M⁻¹s⁻¹ and koff = 4 × 10⁻⁴ s⁻¹. Compute Kd and the complex half-life, and give the SDK's bindingStrength band for that Kd.
  2. Why does calculateBindingAffinity() subtract the complex from the totals, and when would assuming free ≈ total give a noticeably wrong Kd?
  3. A colleague enters an antibody clearance of 0.25 L/day directly as clearance: 0.25. What half-life will the SDK report for Vd = 6 L, and what is the correct value?
  4. Explain, using FcRn biology, why removing the Fc region shortens half-life, and name two reasons why a single CL value may not describe an antibody over its whole concentration range.
  5. For n = 1, which fraction of Emax corresponds to the lower edge of the SDK's therapeuticRange? Why is this not what the code comment claims?
  6. Write a unit test that would fail on defect D1 and pass once occupancy is computed from Kd.