Chapter 3

Chapter 3: Signal Processing and Performance Metrics — From Sensitivity to Stability

How a raw transducer output becomes a calibrated number, and how WIA-BIO-011 (spec §5–§6, Appendix A) defines the metrics that say how good that number is.

Chapter 2 ended at the transducer terminals: a current, a potential, a frequency shift or a fluorescence count. This chapter picks up the signal there and follows it through baseline correction, filtering, calibration and inversion, and then defines every performance metric that WIA-BIO-011 uses to describe a biosensor: sensitivity, limit of detection (LOD), limit of quantification (LOQ), dynamic range, selectivity, response time, accuracy, precision and stability. Every metric is treated as a computation on data: what goes in, what formula is applied, what unit comes out, and which spec target it is compared against. The operational side — how many standards to run, when to recalibrate, how to accept or reject a calibration — belongs to Chapter 6.

The worked numbers come from the spec's own Appendix A. Where the appendix digits do not reproduce from its data, this chapter recomputes them and says so. Where the spec, the TypeScript SDK (@wia/bio-011) and the CLI (wia-bio-011) disagree, the disagreement is stated, and one convention is chosen for the rest of the book.

3.1 The Signal Chain: Baseline, Noise and Filtering

3.1.1 Baseline correction

Almost no transducer reads zero when the analyte is absent. An amperometric electrode carries a charging (capacitive) current and a small faradaic background from the buffer; an optical sensor sees stray light and autofluorescence; a QCM drifts with temperature and with slow water uptake by the recognition layer. Spec §5.1 therefore starts the chain with a subtraction:

Signal_corrected = Signal_raw − Baseline

The spec lists four ways to estimate the baseline. A linear baseline (y = mx + b) is fitted to signal regions known to contain no analyte response, for example the seconds before the sample is applied and the tail after a flow-injection peak has washed out. A polynomial baseline does the same with a curved model when the background bends, as it often does during the first minutes after an electrode is polarized. A moving average with a window much longer than the analyte response tracks slow drift in continuous monitoring. A Savitzky–Golay filter fits a low-order polynomial within a sliding window; it is primarily a smoothing method that preserves peak height and width better than a moving average, and it also gives analytic derivatives, which is useful for locating the start and end of a response.

The choice matters more than it looks. If the baseline window overlaps the start of the response, part of the analyte signal is subtracted and the sensitivity is biased low; if the baseline is estimated long before the measurement while the background is still drifting, the error appears as an intercept offset in the calibration. In the SDK, SignalProcessingParams.baseline is a single number that processSignal subtracts; how that number was obtained is the implementer's responsibility and should be recorded with the measurement.

3.1.2 Signal-to-noise ratio

Spec §5.2.1 defines SNR = Signal_mean / Noise_stdev, with a target of SNR > 10 and preferably > 100. Two details make this definition usable. First, the numerator should be the net signal (after baseline subtraction) at the concentration of interest, and the denominator the standard deviation of repeated readings under the same conditions — usually estimated from the blank. Second, SNR is concentration-dependent: a sensor with SNR 800 at the middle of its range may be at SNR 3 near its LOD, which is exactly how the LOD is defined (Section 3.4).

Using the Appendix A.1 blank (σ ≈ 0.0070 µA) and the Appendix A.2 signal at 5 mM glucose (5.85 µA), the net SNR at 5 mM is about 5.8 / 0.0070 ≈ 830. The same blank has a mean/σ of about 8 — but that ratio describes the offset of the blank, not a signal, and should not be reported as an SNR.

3.1.3 The first-order low-pass filter

Spec §5.2.2 gives the simplest digital filter, a first-order infinite-impulse-response (IIR) low-pass, also called an exponential moving average:

y[n] = α · x[n] + (1 − α) · y[n − 1],   0 < α ≤ 1

α = 1 passes the input unchanged; small α smooths heavily. The spec gives no rule for choosing α. It follows from the sampling interval Δt and the desired time constant τf (or cutoff frequency fc = 1/(2π τf)):

α = Δt / (τ_f + Δt)          (discretized RC filter)
α ≈ 1 − exp(−Δt / τ_f)      (equivalent for Δt ≪ τ_f)
f_c = 1 / (2π τ_f)

For example, a reader sampling at 10 Hz (Δt = 0.1 s) that wants a cutoff of 0.1 Hz needs τf ≈ 1.59 s and α ≈ 0.059. The price of that smoothing is time: on its own the filter reaches 90 % of a step after about 2.3 τf ≈ 3.7 s. Filters and sensors add their lags. If the sensor itself responds with τ = 10 s (for example), the combined 90 % time is about 24.8 s instead of the sensor's own 23 s. This is why the response time reported in PerformanceMetrics.responseTime must be measured end-to-end, with the production filter settings, not quoted from the chemistry alone.

SDK status. SignalProcessingParams declares four stages: baseline, temperatureCorrection, filtering (moving-average, savitzky-golay, low-pass, median) and driftCompensation. The v1.0 processSignal implements only the first two; the filtering and drift fields are accepted but ignored, and ProcessedSignal.filtered, driftCompensated and snr are never filled. Filtering therefore has to happen in reader firmware or application code before the value reaches the SDK.

3.2 Calibration Models

3.2.1 Least-squares straight line

A calibration curve maps concentration x to signal y; the measurement later inverts it. Spec §5.3.1 uses ordinary least squares on y = a + b·x:

b  = Σ[(xᵢ − x̄)(yᵢ − ȳ)] / Σ(xᵢ − x̄)²
a  = ȳ − b · x̄
R² = 1 − SS_res / SS_tot,   SS_res = Σ(yᵢ − ŷᵢ)²,  SS_tot = Σ(yᵢ − ȳ)²
x_sample = (y_sample − a) / b

The SDK's private linearRegression implements exactly these equations (in the equivalent sum-of-products form), and both calibrateSensor and the standalone calculateSensitivity use it.

Appendix A.2 supplies six standards. Recomputing the regression from the printed data gives slightly different digits than the appendix prints:

Table 3-1. Appendix A.2 calibration, recomputed
Standard (mM)Signal (µA)Fitted ŷ (µA)Residual (µA)Residual / signal
00.050.047+0.003—
11.201.206−0.006−0.5 %
55.855.840+0.010+0.2 %
1011.5011.633−0.133−1.2 %
5058.2057.974+0.226+0.4 %
100115.80115.900−0.100−0.1 %

The fit is slope b ≈ 1.1585 µA/mM, intercept a ≈ 0.047 µA and R² ≈ 0.99999. The appendix prints 1.157, 0.043 and 0.9998, which do not follow from its data; this book uses the recomputed values. The practical effect is small — the appendix sample at 23.5 µA inverts to (23.5 − 0.047)/1.1585 ≈ 20.2 mM rather than the printed 20.3 mM — but a conformance test that compares an implementation's output against the printed digits would fail a correct implementation.

3.2.2 Why R² alone is not enough

R² measures how much of the total variance in y the line explains. When standards span two decades, SS_tot is dominated by the highest standards, and almost any monotonic data set scores R² > 0.99. The A.2 fit illustrates three things R² hides:

A residual plot — residual against concentration, ideally also relative residual — should therefore accompany every calibration record. The SDK already computes the residual vector and maxResidual in CalibrationData.qualityControl; the acceptance rules that use them are covered in Chapter 6.

3.2.3 Non-linear models: 4PL and Michaelis–Menten

Immunoassays are sigmoidal on a log-concentration axis because the binding sites saturate at high concentration and the signal merges into background at low concentration. Spec §5.3.2 specifies the four-parameter logistic (4PL) model:

y = d + (a − d) / [1 + (x / c)^b]

a : response at zero concentration
d : response at infinite concentration
c : inflection point (EC50 / IC50)
b : slope factor (Hill slope)

inverse:  x = c · [ (a − d) / (y − d) − 1 ]^(1/b)

The spec labels a as the "minimum" and d as the "maximum" asymptote. That is true for a sandwich assay, where the signal rises with concentration. In a competitive assay the signal falls, so a is the upper asymptote. The algebra is the same either way; only the labels change. Reading the parameters as "response at zero" and "response at infinity" avoids the confusion.

Enzyme electrodes follow the apparent Michaelis–Menten form introduced in Chapter 1, I = Imax[S]/(Km,app + [S]), with inverse [S] = Km,app·I/(Imax − I). Its useful property for calibration design is that the fractional deviation from the initial straight line is exactly [S]/(Km,app + [S]). At [S] = 0.1 Km,app the response is already 9.1 % below the initial slope; this is why the linear range of an enzyme electrode ends well below Km,app unless a diffusion-limiting membrane raises Km,app.

Two consequences for non-linear models apply throughout the book. The sensitivity is no longer a single number but the local derivative dy/dx, which varies across the range. The inversion can also be ill-conditioned near the asymptotes, where a small change in y maps to a large change in x. CalibrationData.type can record '4pl' or 'michaelis-menten', but v1.0's calibrateSensor always fits a straight line whatever the type says, and measureAnalyte refuses non-linear types (Chapter 5). Non-linear calibration therefore has to be fitted outside the SDK in v1.0.

3.3 Sensitivity

Sensitivity is the slope of the calibration curve, S = Δy/Δx (spec §6.1). In analytical chemistry "sensitivity" means this slope, not the clinical sensitivity (true-positive rate) used in Chapters 7–8. Its unit is always "signal per concentration" and depends on the transducer:

Table 3-2. Sensitivity units by transducer
TransducerSignalTypical sensitivity unitArea-normalized form
AmperometriccurrentµA/mM, nA/µMµA·mM⁻¹·cm⁻²
PotentiometricpotentialmV/decade (log-linear)not applicable (area-independent)
Optical (fluorescence, absorbance)RFU, AURFU/µM, AU/mMinstrument-specific
Piezoelectric (QCM)frequencyHz/ng, Hz per (ng/mL)Hz·cm²/ng (Sauerbrey)
Impedance (EIS)ΔRctΩ/decade or %ΔRct/decadeusually reported relative

Raw sensitivity rewards large electrodes. An amperometric current scales with the electrode area A (I = nFA·D·dC/dx, Chapter 2), so a 5 mm disc will always "beat" a 1 mm microelectrode made from the same chemistry. To compare recognition layers rather than geometry, divide by the geometric area. If, for example, the A.2 sensor used a 3 mm diameter disc (A ≈ 0.0707 cm²), its area-normalized sensitivity would be 1.1585 / 0.0707 ≈ 16.4 µA·mM⁻¹·cm⁻². The appendix does not state the electrode area; the figure is only to show the arithmetic.

Potentiometric sensors differ: their signal is logarithmic in activity, so the natural sensitivity is the slope in mV per decade (ideally 59.16/n mV/decade at 25 °C). A linear fit of potential against concentration would be the wrong model.

The SDK stores the slope in CalibrationData.sensitivity and builds sensitivityUnit from the signal and concentration units supplied in the request (default "μA/mM"). Because the unit is an explicit string, a record in RFU/µM can never be mistaken for one in µA/mM — provided the caller sets the units rather than relying on the defaults.

3.4 Limit of Detection and Limit of Quantification

3.4.1 Definitions

The LOD is the lowest concentration that can be reliably distinguished from a blank; the LOQ is the lowest concentration that can be measured with acceptable precision and accuracy. Spec §6.2–§6.3 uses the calibration-slope form:

LOD = 3.3 · σ / S
LOQ = 10  · σ / S
⇒ LOQ / LOD = 10 / 3.3 ≈ 3.03

Spec §6.3 also writes "LOQ = 3.3 × LOD". That is an error: the two definitions above give LOQ ≈ 3.03 × LOD, and the SDK (LOD_MULTIPLIER 3.3, LOQ_MULTIPLIER 10) follows the definitions, not the alternative formula. This book uses LOQ = 10σ/S.

The factor 3.3 is not arbitrary. With normally distributed noise, a decision threshold at 1.645σ above the blank gives a 5 % false-positive rate, and placing the LOD a further 1.645σ above that threshold gives a 5 % false-negative rate at the LOD. 2 × 1.645 ≈ 3.3. The 3.3σ/S and 10σ/S pair is the form used in the ICH Q2 analytical validation guideline; many IUPAC-style texts use a factor of 3 instead. The SDK names the 3.3 form 'iupac' and also offers '3-sigma'. Whichever is used, the method must be recorded with the result — the SDK does this in LODResult.method.

3.4.2 Where σ comes from

σ can be estimated in at least three ways, and they do not give the same answer:

  1. Standard deviation of replicate blanks — the approach of Appendix A.1 and of calculateLOD, which takes blankSignals and computes the sample standard deviation (n − 1 denominator).
  2. Residual standard deviation of the regression, sy/x = √(SS_res/(n − 2)).
  3. Standard deviation of the y-intercept from the regression, or of replicate low-level standards.

For the A.2 calibration, sy/x ≈ 0.14 µA, which gives LOD ≈ 3.3 × 0.14 / 1.1585 ≈ 0.40 mM — twenty times the blank-based value computed below. The difference is not a contradiction. The residual SD is dominated by the absolute scatter at 50–100 mM (Section 3.2.2) and says little about noise near zero. For a wide-range calibration, the blank-based σ (or the SD of a low-level standard) is the relevant one, and a LOD claim should state which σ was used.

3.4.3 Worked example: Appendix A.1

Ten blank readings (µA): 0.05, 0.06, 0.05, 0.07, 0.05, 0.06, 0.05, 0.06, 0.05, 0.06.

mean      = 0.560 / 10            = 0.056 µA
σ (n−1)   = √(0.00044 / 9)        ≈ 0.00699 µA
S         = 1.15 µA/mM   (value given in A.1)

LOD (3.3σ/S) = 3.3 × 0.00699 / 1.15  ≈ 0.020 mM  (20 µM)
LOQ (10σ/S)  = 10  × 0.00699 / 1.15  ≈ 0.061 mM  (61 µM)
3σ/S         = 3   × 0.00699 / 1.15  ≈ 0.018 mM

with the recomputed A.2 slope 1.1585:
LOD ≈ 0.0199 mM,  LOQ ≈ 0.0604 mM

The appendix result (LOD ≈ 0.020 mM) is confirmed. Two remarks. First, the readings are quantized to 0.01 µA, only slightly larger than σ itself; the LOD estimate is therefore sensitive to the reader's resolution, and a production validation should use a reader with resolution well below the blank noise. Second, the blank CV here is about 12 %, which is expected: precision targets such as CV < 5 % apply to concentrations above the LOQ, not to blanks.

3.4.4 The t-based (EPA-style) form

Spec §6.2 lists an "EPA" alternative that multiplies σ by a Student t value and a prediction-interval factor √(1 + 1/n + (x̄ − x₀)²/Σ(xᵢ − x̄)²). The U.S. EPA method detection limit procedure (40 CFR Part 136, Appendix B) uses MDL = t(n−1, 0.99) × s from replicate low-level samples. The constant 3.14 that the spec calls "typical" is the one-sided 99 % t value for six degrees of freedom — that is, for exactly n = 7 replicates (more precisely 3.143). For the ten A.1 blanks (nine degrees of freedom), the corresponding value is about 2.82.

The SDK's method: 'epa' simply multiplies σ/S by a fixed 3.14 regardless of n and ignores confidenceLevel. It is a scaled variant of the 3σ rule, not an implementation of the EPA procedure, and should not be described as EPA-compliant.

3.5 Dynamic Range versus Linear Range

Spec §6.4 defines the dynamic range as a number of decades:

DR = log₁₀(C_max / LOD)

The spec's own glossary (§1.4), however, defines "Dynamic Range" as the range over which the sensor responds linearly. These are two different things, and this book keeps them apart:

For the A.2 sensor with the A.1 blank, DR = log₁₀(100 / 0.0199) ≈ 3.7 decades, consistent with the 3–4 decades the spec gives as typical for enzymatic sensors. The linear range that the SDK records is different. calibrateSensor sets linearRange.min to the lowest non-zero standard (1 mM) and max to the highest (100 mM), so it spans two decades. Between the LOD (0.02 mM) and the lowest standard (1 mM) lies a region where the sensor detects the analyte but the calibration has no standard to support quantification. updatePerformanceMetrics computes DR as log₁₀(linearRange.max/LOD), so the SDK's DR uses the top of the calibrated range as Cmax.

signal
  ^
  |                                           saturation
  |                                  ____.-----'''''''
  |                            _.-'''
  |                       _.-'      <-- linear range -->
  |                  _.-'           (calibrated, standards present)
  |             _.-'
  |        _.-'
  |   _.-'
  |.-'   blank ± σ
  +--+----+-------+--------------------------+--------------> concentration
     LOD  LOQ     lowest standard            highest standard
     |<------------- dynamic range (decades, log scale) --------->|
Figure 3-1. Sketch of a calibration curve with LOD, LOQ, linear range and saturation. The horizontal axis is logarithmic in spirit; distances are not to scale.

3.6 Selectivity

3.6.1 A ratio of sensitivities

Spec §6.5 defines the selectivity coefficient as Kij = Si/Sj, where Si is the sensitivity to the target analyte and Sj the sensitivity to an interferent, with a target of Kij > 100:1. Because S is a slope (signal per concentration), each signal must be divided by the concentration that produced it before the ratio is taken. Comparing raw signals measured at different concentrations measures the test design, not the sensor.

Note the convention. In potentiometry, the IUPAC selectivity coefficient Kpotij (Nikolsky–Eisenman) is defined the other way round: it expresses the interferent's contribution relative to the primary ion, so a smaller value means better selectivity. WIA-BIO-011's Kij is a ratio where larger is better. A data record that mixes the two conventions would silently invert its meaning, so the convention should travel with the number.

3.6.2 Appendix A.3 reworked

Appendix A.3 divides the glucose signal at 5 mM by interferent signals measured at 5, 0.1 and 0.5 mM, reports 39:1, 73:1 and 49:1, and calls the result "excellent". Normalizing by concentration changes the picture:

Table 3-3. Appendix A.3 selectivity, concentration-normalized
SpeciesConc. (mM)Signal (µA)S = signal/conc. (µA/mM)Spec A.3 (raw ratio)Kij = Sglucose/SjKij, blank-subtracted
Glucose55.851.17———
Fructose50.150.03039:1≈ 39:1≈ 62:1
Ascorbic acid0.10.080.8073:1≈ 1.5:1≈ 4.8:1
Uric acid0.50.120.2449:1≈ 4.9:1≈ 9:1

Per millimole, this sensor responds to ascorbate almost as strongly as to glucose, and to urate about a fifth as strongly. None of the three interferents meets the spec's own target of 100:1. The appendix hides this because the ascorbate and urate tests were run at concentrations fifty and ten times lower than the glucose test.

The last column subtracts the A.1 blank mean (0.056 µA) from every signal before normalizing. The ascorbate signal of 0.08 µA is only about 0.024 µA above the blank — roughly 3.4σ, right at the detection limit. The coefficient therefore moves from 1.5 to 4.8 depending on a correction that the appendix does not specify. A single-point interference test near the blank cannot give a reliable Kij. A proper study (Chapter 6 and the CLSI EP07 approach in Chapter 8) uses blank-corrected signals at several interferent concentrations and reports the result as a bias at a stated analyte level.

The corrected numbers are consistent with well-known electrochemistry. At the +0.6 V (vs Ag/AgCl) needed to oxidize H2O2 on platinum (Chapter 2), ascorbate and urate are oxidized directly at the electrode, with no enzyme involved. This is the real reason glucose sensors use permselective membranes, redox mediators that allow a lower working potential, or a sentinel electrode without enzyme to subtract the non-specific current. The raw-ratio reading of A.3 would suggest that none of this is necessary.

3.7 Response Time and Stability

3.7.1 First-order response

Spec §6.6 models the rise of the signal after a concentration step as first-order:

S(t) = S_max · (1 − e^(−t/τ))
t₉₀ = τ · ln 10 ≈ 2.3 τ
t₉₅ = τ · ln 20 ≈ 3.0 τ
t₉₉ = τ · ln 100 ≈ 4.6 τ

For example, a sensor with τ = 10 s has t₉₀ ≈ 23 s and t₉₅ ≈ 30 s. τ can be estimated from a recorded step by plotting ln(1 − S(t)/Smax) against t: for a truly first-order sensor the points fall on a line of slope −1/τ. Curvature in that plot shows that the response is not a single exponential. Membrane-covered enzyme electrodes often show a fast initial rise followed by a slow diffusional tail, and a single τ then understates the time needed for a stable reading. README gives 1–60 s as typical t₉₀ for rapid biosensors; affinity sensors that rely on binding equilibrium usually need minutes.

Response time is stored as a plain number in seconds (PerformanceMetrics.responseTime) supplied by the caller of updatePerformanceMetrics; the SDK does not measure it. As Section 3.1.3 showed, it should be measured through the complete signal chain.

3.7.2 Decay, half-life and drift

Spec §6.9 models loss of sensitivity as first-order decay:

S(t) = S₀ · e^(−k t)
t½ = ln 2 / k
drift (%/month) = [1 − S(30 d) / S₀] × 100   (for t in days)

The spec's long-term drift target is < 5 % per month. A sensor that loses exactly 5 % of its sensitivity in 30 days has k = −ln(0.95)/30 ≈ 0.0017 d⁻¹ and t½ ≈ 405 days. The same 5 %/month compounded over six months leaves 0.95⁶ ≈ 74 % of the initial sensitivity. That is why recalibration, not drift correction, is the primary defence in the spec (Chapter 6).

Two kinds of stability must be kept apart, and PerformanceMetrics.stability has separate fields for them:

The third field, drift in %/month, is the slope that links them in continuous use. README quotes t½ values of 1–365 days as a typical range; that range is indicative only and is not a WIA requirement.

3.8 Accuracy, Precision and Temperature Compensation

3.8.1 Accuracy and recovery

Spec §6.7 expresses accuracy as relative error against a reference value:

Accuracy (%) = (Measured − True) / True × 100      target: within ±5 % (clinical)
Recovery (%) = (C_spiked − C_unspiked) / C_added × 100   target: 90–110 % (spec §7.2)

Relative error needs a "true" value, which in practice means a certified reference material or a reference method. Recovery avoids that need by adding a known amount of analyte to a real sample and asking how much of it is found. For example, a sample that reads 5.0 mM and, after adding 5.0 mM glucose, reads 9.8 mM has a recovery of (9.8 − 5.0)/5.0 = 96 %. Recovery is the better test for matrix effects, because the sample's own proteins, viscosity or hematocrit act on the added analyte in the same way as on the endogenous analyte. Spec §7.2.3 writes recovery as Cmeasured/Cspiked × 100, which is the same thing when the spike goes into an analyte-free matrix; the subtraction form above is the one to use for real samples (Chapter 6). The acceptance limits live in BIOSENSOR_CONSTANTS (MIN_RECOVERY 90, MAX_RECOVERY 110).

3.8.2 Precision

Spec §6.8 expresses precision as the coefficient of variation:

CV (%) = σ / μ × 100      target: CV < 5 % for diagnostic biosensors (MAX_CV_PERCENT = 5.0)

CV is only meaningful at concentrations well above the LOQ; at the LOQ itself, CV ≈ 10 % by construction (σ/S divided by 10σ/S). A precision statement also needs its conditions. Repeatability (same sensor, same run) is always better than intermediate precision (different days, operators or sensor lots), and a single "CV < 5 %" with no conditions attached cannot be checked. Chapter 8 maps this to CLSI EP05.

3.8.3 Temperature compensation: one formula, two directions

Enzyme kinetics, diffusion coefficients and membrane permeability all rise with temperature, so an enzymatic sensor's signal typically increases by a few percent per degree. The spec (§5.4) gives typical coefficients of 2–5 %/°C for enzymatic sensors and 0.5–2 %/°C for electrochemical sensors, and the SDK default is ENZYME_TEMP_COEFFICIENT = 3 %/°C. These are defaults; the true value depends on the enzyme, its immobilization and the membrane, and should be measured for each sensor design.

The spec and the code disagree on the direction of the correction:

spec §5.4:        Signal_comp = Signal × [1 + α (T − T_ref)]
SDK processSignal: signal      = signal / [1 + (coefficient/100) (T − T_ref)]
CLI measure:       corrected   = signal / (1 + 0.03 × (temp − 25))

Both can be right, but only with different meanings of α. If α is the sensor's temperature coefficient — the fractional increase in signal per degree, ST = Sref[1 + α(T − Tref)] — then referring a reading back to Tref requires division, as the SDK does. The spec's multiplication is correct only if α is a correction coefficient with the opposite sign: to first order αcorr = −α/[1 + α(T − Tref)].

The size of the difference is easy to underestimate. Take, for example, a reading of 20.0 µA at 30 °C, α = 3 %/°C, Tref = 25 °C and the recomputed A.2 calibration:

Table 3-4. One reading, two temperature conventions (illustrative)
ConventionCorrected signal (µA)Concentration (mM)
No correction20.00≈ 17.2
Divide (SDK/CLI, α = sensor coefficient)20.0 / 1.15 ≈ 17.39≈ 15.0
Multiply (spec text, α = +0.03)20.0 × 1.15 = 23.00≈ 19.8

Applying the correction in the wrong direction over a 5 °C difference moves the result by almost 5 mM, far more than leaving it uncorrected. Book convention: from here on, α denotes the sensor's temperature coefficient of signal, measured by calibrating at two or more temperatures, and compensation divides by [1 + α(T − Tref)], as the SDK and CLI do. A record that stores a coefficient should say which convention it uses. The SDK also sets the flag temperature-out-of-range when the sample temperature differs from 25 °C by more than 10 °C; a linear correction should not be trusted over such a span.

3.9 Putting It Together: Metrics Table and Code

3.9.1 Master table

Table 3-5. WIA-BIO-011 performance metrics
MetricSymbol / formulaUnitSpec targetTypical per READMEWhere recorded
SensitivityS = Δy/Δxsignal/conc. (µA/mM …)—0.1–1000CalibrationData.sensitivity
LinearityR² = 1 − SS_res/SS_tot—> 0.99—CalibrationData.rSquared
SNRmean/σ—> 10 (pref. > 100)—not stored in v1.0
LOD3.3σ/Sconcentrationapplication-specificpM–µMLODResult.lod, PerformanceMetrics.lod
LOQ10σ/S ≈ 3.03 × LODconcentration——LODResult.loq
Dynamic rangelog₁₀(Cmax/LOD)decades—3–6PerformanceMetrics.dynamicRange
Linear range[Clow, Chigh]concentration——CalibrationData.linearRange
SelectivityKij = Si/Sjratio> 100:1> 100:1PerformanceMetrics.selectivity
Response timet₉₀ ≈ 2.3τs—1–60 sPerformanceMetrics.responseTime
Accuracy(meas − true)/true%±5 %—PerformanceMetrics.accuracy
Recovery(Cspiked − Cunspiked)/Cadded%90–110 %—validation input
PrecisionCV = σ/µ%< 5 %—PerformanceMetrics.precision
Stabilityt½ = ln2/k; driftdays; %/monthshelf 3–12 months; 100–1000 uses; drift < 5 %/montht½ 1–365 dPerformanceMetrics.stability

The README column is indicative only. The normative targets are those of spec §6–§7.

3.9.2 Recomputing Appendix A with the SDK

The following TypeScript recomputes the A.2 regression by hand, checks it against the SDK, and derives LOD, LOQ and dynamic range from the A.1 blanks. The expected output is shown in comments. The biorecognition value is the enum member 'enzyme'; the README's 'glucose-oxidase' is not a member of BiorecognitionElement.

import { BiosensorSDK, calculateSensitivity } from '@wia/bio-011';

const conc   = [0, 1, 5, 10, 50, 100];                  // mM  (Appendix A.2)
const signal = [0.05, 1.20, 5.85, 11.50, 58.20, 115.80]; // µA
const blanks = [0.05, 0.06, 0.05, 0.07, 0.05, 0.06, 0.05, 0.06, 0.05, 0.06]; // A.1

// 1. By hand: centred least squares
const mean = (v: number[]) => v.reduce((a, b) => a + b, 0) / v.length;
const xm = mean(conc), ym = mean(signal);
let sxy = 0, sxx = 0;
conc.forEach((x, i) => { sxy += (x - xm) * (signal[i] - ym); sxx += (x - xm) ** 2; });
const b = sxy / sxx;            // 1.15853
const a = ym - b * xm;          // 0.04736
console.log(b.toFixed(4), a.toFixed(3));          // "1.1585" "0.047"

// 2. Standalone helper (same regression inside)
console.log(calculateSensitivity(conc, signal).toFixed(4)); // "1.1585"

// 3. Full SDK path
const sdk = new BiosensorSDK();
const sensor = sdk.createSensor({
  type: 'electrochemical',
  biorecognitionElement: 'enzyme',
  targetAnalyte: 'glucose',
});
const cal = sdk.calibrateSensor({
  sensorId: sensor.id,
  standardConcentrations: conc,
  measuredSignals: signal,
  concentrationUnit: 'mM',
  signalUnit: 'μA',
});
console.log(cal.rSquared.toFixed(5));             // "0.99999"
console.log(cal.sensitivityUnit);                 // "μA/mM"
console.log(cal.linearRange);                     // { min: 1, max: 100, unit: 'mM' }
console.log(cal.qualityControl?.maxResidual.toFixed(3)); // "0.226" (at 50 mM)

// 4. LOD / LOQ from the blanks, then dynamic range
const lod = sdk.calculateLOD({ blankSignals: blanks, sensitivity: cal.sensitivity });
console.log(lod.blankStdDev.toFixed(5));          // "0.00699"
console.log(lod.lod.toFixed(4), lod.loq.toFixed(4)); // "0.0199" "0.0604"
const perf = sdk.updatePerformanceMetrics(sensor.id, lod, 23 /* s, measured */);
console.log(perf.dynamicRange.toFixed(2));        // "3.70"

// Inversion of the A.2 sample (25 °C, no correction)
console.log(((23.5 - cal.regression.intercept) / cal.sensitivity).toFixed(1)); // "20.2"

Three details of this listing matter in practice. The standalone calibrateSensor helper creates a fresh SDK instance with no registered sensors, so calling it with any sensorId raises SENSOR_NOT_FOUND (BIO001); use the method on an SDK instance that has created the sensor, as above. LODResult.unit is the literal string "concentration units", so the caller must attach the real unit (here mM) when storing the result. The response time passed to updatePerformanceMetrics is an input, not a computed value — the 23 s above is a placeholder for a measured t₉₀.

3.10 Summary

A biosensor result passes through baseline subtraction, optional filtering, temperature compensation and inversion of a calibration model. Each step changes what the final number means, and each should be recorded. The performance metrics of WIA-BIO-011 are simple formulas. Their traps lie in the inputs: which σ, which concentration, which convention. With the numbers recomputed from Appendix A, the reference glucose sensor has S ≈ 1.1585 µA/mM, LOD ≈ 0.020 mM, LOQ ≈ 0.060 mM, a calibrated linear range of 1–100 mM and a dynamic range of about 3.7 decades. Once its selectivity is computed correctly, it falls well short of the spec's 100:1 target against ascorbate and urate.

The v1.0 corrections introduced in this chapter — LOQ ≈ 3.03 × LOD, the recomputed A.2 regression, the concentration-normalized A.3 selectivity, the separation of dynamic and linear range, and the division convention for temperature compensation — are collected with the other errata in Chapter 8. Chapter 4 now turns to the records in which all of these values are stored.