Reasoning laboratory · Brownian motion

Keep the data.
Change what you know.

A curve can fit more than one explanation. Keep the observations fixed, inspect the compatible parameters, then ask which additional information separates them.

These are two built-in synthetic datasets: ideal displacements and camera observations. This workbench does not import your current laboratory state. Changing an information choice neither resamples the observations nor changes the physical model.

One relation · two unknowns

Can displacements tell radius and molecular number apart?

Static worked example · host calculation

Keep 50 observed increments in 2 coordinates, spaced 1 s apart. Temperature 293.15 K and viscosity 1 mPa·s stay fixed.

In this synthetic recovery exercise, The gas constant is a declared scenario input. Neither modern exact kB nor modern exact NA supplies the unknown. This is not a historical molecular count.

Accepted radius information: No independent radius has been admitted. The form below is an unapplied proposal.

Diffusion estimate (μm²/s)
0.88271
95% diffusion interval (μm²/s)
0.68131 – 1.1893
Identified radius × number product (m/mol)
1.4649e17
Molecular-number estimate (mol⁻¹)
Diffusion fixes a radius–number product. Add an independently established radius interval before asking for a molecular-number interval.
Conditional molecular-number interval (mol⁻¹)
Diffusion fixes a radius–number product. Add an independently established radius interval before asking for a molecular-number interval.
Conservative simultaneous coverage lower bound (%)
Diffusion fixes a radius–number product. Add an independently established radius interval before asking for a molecular-number interval.
0.114.6490.447213.275620.73244N (10²³ mol⁻¹); logarithmic axesAssumed radius (μm)
Each point on the solid curve gives the same diffusion estimate at the stated temperature and viscosity. Dashed curves map the 95% diffusion interval into a compatible band. They do not identify a single radius or a single molecular number. This band is not the combined interval obtained after admitting a radius measurement.
Read all compatible pairs
Radius–number family for this accepted estimate; bounds from diffusion uncertainty only
Radius (μm)N (10²³ mol⁻¹)Lower NUpper N
0.114.64910.87318.979
0.1077813.59210.08817.61
0.1161612.6119.360216.339
0.1251911.7018.684815.16
0.1349310.8578.058114.066
0.1454210.0737.476713.051
0.156739.34656.937212.109
0.168928.67216.436611.236
0.182068.04635.972210.425
0.196217.46575.54129.6727
0.211476.9275.14148.9747
0.227926.42724.77048.3272
0.245655.96344.42627.7263
0.264755.53314.10687.1688
0.285345.13393.81056.6515
0.307534.76343.53556.1715
0.331454.41973.28045.7262
0.357224.10083.04375.313
0.3853.80492.82414.9297
0.414943.53032.62034.5739
0.447213.27562.43124.2439
0.481993.03922.25583.9377
0.519482.81992.0933.6535
0.559882.61651.9423.3899
0.603422.42771.80193.1453
0.650342.25251.67182.9183
0.700922.08991.55122.7078
0.755431.93911.43932.5124
0.814181.79921.33542.3311
0.87751.66941.23912.1629
0.945741.54891.14962.0068
1.01931.43721.06671.862
1.09861.33350.989721.7277
1.1841.23720.918311.603
1.27611.1480.852041.4873
1.37531.06510.790561.38
1.48230.988280.733521.2804
1.59750.916960.680591.188
1.72180.85080.631481.1023
1.85570.789410.585911.0228
20.732440.543630.94896
Add an independent measurement

For at least 95% simultaneous coverage, this procedure uses a 97.5% diffusion interval and requires at least 97.5% radius coverage. Coverage is your declared assumption; entering a description does not verify the measurement. Timing, calibration, R, temperature and viscosity remain exact within this example.

Only displacement information has been admitted.

Why does another radius on the curve work equally well?

The diffusion relation fixes the product of radius and molecular number. Doubling the radius and halving the number leaves that product unchanged. More displacements can narrow the product's uncertainty, but cannot separate its factors without another relation.

An independent radius interval restricts the family. Combining its error probability with the diffusion interval's error probability gives a conservative bound. This is not a posterior probability for a single realized interval, and a radius deduced by assuming the molecular number would only return that assumption.

Static worked solution: admit the independent radius

Suppose a separate synthetic calibration supplies 0.5 μm, with bounds 0.45–0.55 μm and declared 97.5% coverage. No measurement is claimed here. The owner's resulting conditional interval is 1.8914e23 – 4.3732e23 mol⁻¹, with conservative coverage of at least 95%.

Inspect the fixed observations and their identity

Observation digest: e9445f22730e0241c22fb7fe3d9f9d7a793b30d95fec05f3fbf07623d3c1ea33
Generator source: source:sha256:2b658a4b9461d836ec8ed1abf378bf46cab28ae0990ee15487493a19446add28

Motion seed: 1905. These built-in observations are synthetic, not an uploaded dataset or a historical measurement.

Displacements in μm, ordered by frame and coordinate
Frame incrementCoordinateDisplacement (μm)
1x-2.3879
1y-0.70732
2x0.76359
2y-0.36669
3x-0.78703
3y2.6303
4x1.6368
4y1.3734
5x2.6295
5y1.4292
6x-1.158
6y0.3752
7x-1.4365
7y-0.034373
8x2.8597
8y-0.38513
9x-1.1757
9y0.10385
10x-0.39354
10y1.5456
11x0.085157
11y1.9454
12x0.28814
12y-1.5271
13x1.4515
13y0.69424
14x0.259
14y0.30462
15x0.072121
15y0.51253
16x-1.0618
16y-0.85806
17x-0.14009
17y0.69891
18x-1.4962
18y-0.11731
19x1.6779
19y-1.3527
20x-1.0475
20y-0.10093
21x-1.3041
21y-0.57943
22x-2.2484
22y0.532
23x0.42622
23y-0.40314
24x-0.072652
24y1.5005
25x0.62345
25y-0.67176
26x-1.4449
26y0.088609
27x1.0489
27y-1.0495
28x0.28654
28y-2.6253
29x-1.517
29y0.81859
30x3.3981
30y-0.61668
31x-1.2871
31y0.033954
32x-0.61602
32y-1.7772
33x-0.44696
33y0.19549
34x0.83176
34y3.1783
35x1.1501
35y-0.98462
36x-1.1138
36y2.1577
37x0.39373
37y-1.5193
38x-0.9805
38y0.85426
39x0.48911
39y1.7621
40x0.26211
40y0.275
41x2.4597
41y-0.70916
42x1.6996
42y0.41987
43x-2.2061
43y-1.5354
44x-0.05283
44y-0.76038
45x0.50145
45y-2.55
46x2.9085
46y-1.3428
47x-0.47992
47y-1.6202
48x-0.9242
48y0.021895
49x-1.8314
49y-0.44372
50x-0.57255
50y1.4425

Return to the full inference laboratory · Return to the inference argument

One record · different information

Motion or localization error?

Static worked example · host calculation

Keep 100 camera increments in 2 coordinates. Frame spacing 1 s; known uniform exposure 0.5 s; known drift 0 μm/s. The generator's diffusion coefficient and localization variance are not inference inputs.

Which information will you admit?

Accepted information: one increment variance. The recorded positions have not changed.

Per-coordinate moments after subtracting known synthetic drift; μm²
MomentSample
Increment variance0.81215
Adjacent-increment covarianceNeighboring covariance has not been admitted to this question.
Variance at doubled spacing (μm²)
A second observation interval has not been admitted to this question.
Diffusion moment estimate (μm²/s)
One variance leaves a compatible diffusion–noise family. Admit covariance or a second spacing to separate the two unknowns.
Localization-noise variance estimate (μm²)
One variance leaves a compatible diffusion–noise family. Admit covariance or a second spacing to separate the two unknowns.
Confidence interval
Correlated camera increments do not satisfy the independent-increment chi-square procedure. These moment solutions are point estimates, not confidence intervals.
Localization variance (μm²)00.4060800.48729Diffusion coefficient (μm²/s)
The solid segment is the family compatible with the sample variance, not a confidence region. Its zero-diffusion boundary is not a measured estimate. Covariance or another spacing supplies a second relation.
Compare candidate pairs without the graph
Every row reproduces the same variance; all values are model consequences
D (μm²/s)Noise variance (μm²)Predicted covariance (μm²)
0.0118850.39617-0.39419
0.023770.38627-0.38231
0.0356560.37636-0.37042
0.0475410.36646-0.35854
0.0594260.35656-0.34665
0.0713110.34665-0.33477
0.0831960.33675-0.32288
0.0950810.32684-0.311
0.106970.31694-0.29911
0.118850.30703-0.28722
0.130740.29713-0.27534
0.142620.28722-0.26345
0.154510.27732-0.25157
0.166390.26742-0.23968
0.178280.25751-0.2278
0.190160.24761-0.21591
0.202050.2377-0.20403
0.213930.2278-0.19214
0.225820.21789-0.18026
0.23770.20799-0.16837
0.249590.19809-0.15649
0.261470.18818-0.1446
0.273360.17828-0.13272
0.285240.16837-0.12083
0.297130.15847-0.10895
0.309010.14856-0.097062
0.32090.13866-0.085177
0.332780.12876-0.073292
0.344670.11885-0.061407
0.356560.10895-0.049522
0.368440.099043-0.037636
0.380330.089139-0.025751
0.392210.079234-0.013866
0.40410.06933-0.0019809
0.415980.0594260.0099043
0.427870.0495220.021789
0.439750.0396170.033675
0.451640.0297130.04556
0.463520.0198090.057445
0.475410.00990430.06933
0.4872900.081215

Variance alone leaves diffusion and noise unresolved.

What did adding information change?

Variance mixes physical spreading, exposure averaging and localization error. Neighboring increments share a localization error with opposite signs, so their covariance contributes a different relation. Alternatively, pair adjacent increments from the same recording to inspect twice the frame spacing, with the exposure duration unchanged.

The second-spacing observations are derived from the same positions, not statistically independent new trials. The two finite-sample moment solutions can differ. Neither solution here carries a confidence interval. These are later camera models, not Einstein's derivation.

In the zero-exposure case, ignoring unknown localization error biases a variance-only diffusion estimate upward. Uniform exposure can instead reduce the observed variance. That is why the exposure assumption stays visible and fixed.

Static worked solution: admit neighboring covariance

For this same fixed recording, the covariance-based point estimates are D = 0.51042 μm²/s and localization variance = -0.019274 μm². They are not confidence limits.

Inspect the fixed observations and their identity

Observation digest: 6b66262548d6763737837290b2f451070a5c56492e6765f63e9f01ef4d60f682
Generator source: source:sha256:c7ff276529014c8c0b7bc51fdeef19fe4faf6888459839a8196da9b6d99cffe0

Motion seed: 1905. These built-in observations are synthetic, not an uploaded dataset or a historical measurement.

Displacements in μm, ordered by frame and coordinate
Frame incrementCoordinateDisplacement (μm)
1x0.052152
1y1.1727
2x1.3394
2y0.55004
3x-0.25739
3y3.3721
4x0.93817
4y1.2256
5x0.17098
5y0.23457
6x-0.38416
6y0.83982
7x1.341
7y0.38545
8x0.49323
8y-0.47663
9x-0.17185
9y0.51197
10x1.2638
10y1.2212
11x-2.0775
11y0.071756
12x-1.4292
12y-0.82665
13x-0.48458
13y0.57606
14x1.1733
14y0.52255
15x-0.21156
15y0.10566
16x1.4506
16y0.068282
17x-0.36709
17y0.35189
18x0.85611
18y-0.29712
19x0.70947
19y-0.011905
20x1.1738
20y-0.12242
21x-1.2163
21y-0.36858
22x-1.2302
22y0.084949
23x-1.0645
23y0.47693
24x0.24489
24y1.2602
25x0.60269
25y-0.4288
26x-0.63745
26y-0.24701
27x-0.88825
27y0.64947
28x1.5901
28y-0.97774
29x0.9249
29y-1.5412
30x-0.58705
30y0.27076
31x1.067
31y0.20418
32x0.40663
32y0.65126
33x-0.95103
33y-3.0736
34x-1.6096
34y-2.1956
35x-0.7874
35y1.0323
36x2.4222
36y0.72772
37x1.2379
37y0.37757
38x-0.75227
38y0.56343
39x-0.19214
39y-0.079906
40x-1.0351
40y0.023257
41x0.75696
41y1.0211
42x-1.0429
42y-0.6176
43x0.20427
43y0.34629
44x1.1992
44y-0.033293
45x0.92087
45y0.044388
46x-0.17736
46y1.2705
47x-0.62433
47y2.0321
48x-0.494
48y0.23545
49x1.2259
49y-1.5715
50x-0.51899
50y0.45366
51x-0.25733
51y-1.4979
52x-0.47704
52y0.42919
53x0.81272
53y-0.077847
54x0.15059
54y-0.92853
55x0.54609
55y-0.61767
56x-1.5655
56y-0.029961
57x-0.31321
57y1.25
58x-1.3451
58y-0.11232
59x0.51877
59y0.92947
60x0.35182
60y0.19422
61x0.3776
61y0.15838
62x-0.86027
62y0.68658
63x-1.3589
63y1.0646
64x-2.3538
64y-0.20533
65x0.23645
65y-1.171
66x-0.11725
66y-0.90494
67x0.57605
67y-0.64669
68x-0.0049188
68y1.0388
69x0.20831
69y1.1579
70x-1.0463
70y0.80787
71x-1.0439
71y-0.49011
72x0.02682
72y0.096069
73x-1.2335
73y-0.23691
74x0.1886
74y0.66849
75x-1.7375
75y-0.65304
76x0.11388
76y0.87759
77x-0.53453
77y0.39029
78x-0.97624
78y1.4547
79x-0.27295
79y-0.35166
80x0.83092
80y-1.5903
81x1.1025
81y0.20143
82x-1.1893
82y-0.16656
83x0.14528
83y0.37482
84x-0.12985
84y0.88712
85x-1.5348
85y0.7736
86x-0.36071
86y-0.89011
87x0.43223
87y-0.64712
88x0.12764
88y-0.1486
89x-0.20486
89y0.035909
90x0.070332
90y-0.20517
91x-0.23762
91y-0.59373
92x-0.15501
92y-0.33536
93x-0.45727
93y-1.4671
94x0.83387
94y1.014
95x0.43857
95y-0.6013
96x-0.98601
96y-0.58897
97x0.15066
97y-0.32546
98x0.046874
98y-0.87084
99x-0.077103
99y0.32433
100x-0.77186
100y-0.69886

Return to the full camera laboratory and interval procedures

A different kind of second measurement

The second measurement, from the viscosity of dilute solutions, is in preparation.

What the calculation does and does not establish

The first case assumes independent Gaussian increments and known zero drift. Its conservative molecular-number interval combines diffusion uncertainty with a separately declared radius interval. Timing, spatial calibration, gas constant, temperature and viscosity are treated as exact here.

The camera case assumes equally spaced positions, known uniform exposure, known drift, and independent zero-mean localization errors with one fixed variance. Its two-moment estimates are not uncertainty intervals, and a negative solution is not quietly repaired.

The camera model is a later measurement aid, not Einstein’s derivation. Synthetic recovery tests the inference machinery; it is not observational evidence for a real suspension.

Berglund (2010): camera-based single-particle tracking · Vestergaard, Blainey and Flyvbjerg (2014): covariance-based diffusion estimation

Inspect the inference calculation source used by this build

source:sha256:acea9323df21c3df2727d26a65ea1e9a9c08695e10d7db60a5a41221e81de054

src/physics/reference/inference/identifiability.ts

sha256:a9f1bc7fe62d5b2e8c34c4344031f322ba0d5a52a1a250028acbe2795791477f

/** Inverse questions for the existing Brownian owners. No random draws occur here.
 * Camera relations: Berglund (2010), doi:10.1103/PhysRevE.82.011917.
 * These are later measurement models, not Einstein's printed derivation.
 */
import { makeRefusal } from "../../../experiments/results/refusals.ts";
import type { ConstantSet } from "../constants.ts";
import {
  type Assessment,
  combinedMolecularNumberInterval,
  type DeclaredInterval,
  type Estimate,
  estimatorInterval,
  identifiabilityFamily,
  type StatisticalInterval,
} from "../inference.ts";
import { cameraMoments } from "./observation.ts";

const invalid = (requirements: string): Assessment<never> => ({
  kind: "refused",
  refusal: makeRefusal(
    "invalid-parameter",
    { capabilityId: "diffusion.inference" },
    { details: { requirements } },
  ),
});
const positive = (value: number) => Number.isFinite(value) && value > 0;

/** The same confidence interval for D maps to an entire band, not a unique N. */
export function diffusionCompatibleBand(
  input: {
    estimate: Estimate;
    T: number;
    eta: number;
    alpha: number;
    radiusRange: readonly [number, number];
    synthetic: boolean;
  },
  constants: ConstantSet,
) {
  const interval = estimatorInterval(input.estimate, input.alpha);
  if (interval.kind !== "accepted") return interval;
  const conditions = {
    T: input.T,
    eta: input.eta,
    radiusRange: input.radiusRange,
    synthetic: input.synthetic,
  };
  const center = identifiabilityFamily({ ...conditions, D: input.estimate.dHat }, constants);
  if (center.kind !== "accepted") return center;
  const lower = identifiabilityFamily({ ...conditions, D: interval.data.upper }, constants);
  if (lower.kind !== "accepted") return lower;
  const upper = identifiabilityFamily({ ...conditions, D: interval.data.lower }, constants);
  if (upper.kind !== "accepted") return upper;
  return {
    kind: "accepted" as const,
    data: Object.freeze({
      ...center.data,
      interval: interval.data,
      lowerNumbers: lower.data.numbers,
      upperNumbers: upper.data.numbers,
    }),
  };
}

export type IndependentMeasurement = Readonly<{
  value: number;
  interval: DeclaredInterval;
  provenance: string;
}>;
/** Bonferroni allocation across D and every uncertain auxiliary input.
 * The reader declares the coverage and provenance; we cannot certify a calibration.
 * Dependence between marginal intervals does not invalidate the union bound.
 * R and timing/calibration remain exact within the declared model.
 */
export function conditionalMolecularNumberInterval(
  input: {
    estimate: Estimate;
    T: number;
    eta: number;
    alpha: number;
    synthetic: boolean;
    radius: IndependentMeasurement | null;
    radiusProvenance: "independently-declared" | "same-displacements";
    temperature?: IndependentMeasurement;
    viscosity?: IndependentMeasurement;
  },
  constants: ConstantSet,
): Assessment<StatisticalInterval> {
  if (!positive(input.alpha) || input.alpha >= 1)
    return invalid("Choose a total error probability strictly between zero and one.");
  if (!input.radius)
    return {
      kind: "no-value",
      status: "underdetermined",
      reason:
        "Diffusion fixes a radius–number product. Add an independently established radius interval before asking for a molecular-number interval.",
    };
  const measurements = [input.radius, input.temperature, input.viscosity].filter(
    (value): value is IndependentMeasurement => value !== undefined,
  );
  const alphaD = input.alpha / (measurements.length + 1);
  for (const value of measurements) {
    if (
      !value ||
      typeof value.provenance !== "string" ||
      !value.provenance.trim() ||
      value.provenance.length > 512
    )
      return invalid(
        "Every added measurement needs a provenance description of at most 512 characters.",
      );
    const interval = value.interval;
    if (
      !positive(value.value) ||
      !interval ||
      !positive(interval.lower) ||
      !positive(interval.upper) ||
      interval.lower > value.value ||
      interval.upper < value.value ||
      !Number.isFinite(interval.coverage) ||
      interval.coverage > 1 ||
      interval.coverage <= 0
    )
      return invalid(
        "Positive ordered measurement bounds must contain their point value and have a declared coverage.",
      );
    if (interval.coverage < 1 - alphaD)
      return {
        kind: "no-value",
        status: "not-applicable",
        reason: `Each auxiliary interval needs coverage of at least ${100 * (1 - alphaD)} percent for this Bonferroni allocation. A lower-coverage interval cannot be relabeled.`,
      };
  }
  return combinedMolecularNumberInterval(
    {
      estimate: input.estimate,
      alphaD,
      synthetic: input.synthetic,
      T: input.temperature?.value ?? input.T,
      eta: input.viscosity?.value ?? input.eta,
      a: input.radius.value,
      radiusProvenance: input.radiusProvenance,
      inputs: {
        a: input.radius.interval,
        ...(input.temperature ? { T: input.temperature.interval } : {}),
        ...(input.viscosity ? { eta: input.viscosity.interval } : {}),
      },
    },
    constants,
  );
}

/** Inverting one camera variance gives a segment of compatible (D, sigma²) pairs.
 * The sampled points exclude D=0, which is separately reported as a boundary.
 * Their predicted covariance is evaluated by cameraMoments, not by a view.
 */
export function cameraCompatibleFamily(input: {
  variance: number;
  dt: number;
  exposure: number;
}): Assessment<
  Readonly<{
    maximumDiffusion: number;
    maximumNoiseVariance: number;
    diffusion: Float64Array;
    noiseVariance: Float64Array;
    covariance: Float64Array;
  }>
> {
  const { variance, dt, exposure } = input;
  if (
    !positive(variance) ||
    !positive(dt) ||
    !Number.isFinite(exposure) ||
    exposure < 0 ||
    exposure > dt
  )
    return invalid(
      "Use a positive coordinate variance and frame spacing, with known uniform exposure between zero and that spacing.",
    );
  const effectiveTime = dt - exposure / 3;
  const maximumDiffusion = variance / 2 / effectiveTime;
  const maximumNoiseVariance = variance / 2;
  if (!positive(maximumDiffusion) || !positive(maximumNoiseVariance))
    return invalid("The compatible segment cannot be represented at this numerical scale.");
  const diffusion = new Float64Array(41),
    noiseVariance = new Float64Array(41),
    covariance = new Float64Array(41);
  for (let i = 0; i < diffusion.length; i++) {
    const fraction = (i + 1) / diffusion.length;
    // Parameterization avoids a negative endpoint caused by subtracting rounded products.
    const D = maximumDiffusion * fraction,
      sigma2 = maximumNoiseVariance * (1 - fraction);
    const predicted = cameraMoments({ D, dt, exposure, sigma: Math.sqrt(sigma2), drift: 0, d: 1 });
    if (predicted.kind !== "accepted") return predicted;
    diffusion[i] = D;
    noiseVariance[i] = sigma2;
    covariance[i] = predicted.data.covariance;
  }
  return {
    kind: "accepted",
    data: Object.freeze({
      maximumDiffusion,
      maximumNoiseVariance,
      diffusion,
      noiseVariance,
      covariance,
    }),
  };
}

/** Two distinct frame spacings, SAME exposure duration and localization variance.
 * These are unconstrained moment estimates, not confidence intervals. Negative
 * estimates are retained as model/sample diagnostics, never clipped to zero.
 */
export function twoIntervalCameraEstimate(input: {
  firstVariance: number;
  secondVariance: number;
  firstDt: number;
  secondDt: number;
  exposure: number;
}): Assessment<Readonly<{ D: number; sigma2: number; physical: boolean }>> {
  const { firstVariance, secondVariance, firstDt, secondDt, exposure } = input;
  if (
    ![firstVariance, secondVariance, firstDt, secondDt].every(positive) ||
    !Number.isFinite(exposure) ||
    exposure < 0 ||
    exposure > Math.min(firstDt, secondDt) ||
    secondDt <= firstDt
  )
    return invalid(
      "Use positive moments at two increasing spacings and the same known exposure duration, no longer than either spacing.",
    );
  const D = (secondVariance - firstVariance) / 2 / (secondDt - firstDt);
  const sigma2 = firstVariance / 2 - D * (firstDt - exposure / 3);
  if (![D, sigma2].every(Number.isFinite))
    return invalid("The two-interval inverse is not representable.");
  return { kind: "accepted", data: Object.freeze({ D, sigma2, physical: D >= 0 && sigma2 >= 0 }) };
}

src/reasoning/infer/model.ts

sha256:45108348179f8f3ed4044d83b080a3f207aeee73ba62a33743db6b03710d0be4

import { makeRefusal } from "../../experiments/results/refusals.ts";
import type { ScientificResult } from "../../experiments/results/types.ts";
import type { OutputContract, Parameters } from "../../experiments/store/instanceStore.ts";
import {
  cameraCompatibleFamily,
  conditionalMolecularNumberInterval,
  diffusionCompatibleBand,
  twoIntervalCameraEstimate,
} from "../../physics/reference/inference/identifiability.ts";
import { covarianceEstimator } from "../../physics/reference/inference/observation.ts";
import { INFERENCE_CONSTANTS } from "../../physics/reference/inference/synthetic.ts";
import {
  type Assessment,
  estimateIncrements,
  estimatorInterval,
  invertToMolecularNumber,
} from "../../physics/reference/inference.ts";
import type { InferenceEvidence } from "./evidence.ts";

const contract = (
  unit: string,
  semanticKind: string,
  statuses: OutputContract["statuses"] = ["value"],
): OutputContract =>
  Object.freeze({ unit, semanticKind, ownerId: "inference.workbench", statuses });
const inferred = ["value", "underdetermined", "not-applicable", "outside-domain"] as const;
export const FAMILY_OUTPUTS = Object.freeze({
  diffusionCoefficientEstimate: contract("m2/s", "statistical-diffusion-estimate"),
  diffusionInterval: contract("m2/s", "chi-square-confidence-interval"),
  familyRadii: contract("m", "compatible-radius-grid"),
  familyNumbers: contract("1/mol", "synthetic-recovery"),
  familyLowerNumbers: contract("1/mol", "synthetic-recovery"),
  familyUpperNumbers: contract("1/mol", "synthetic-recovery"),
  radiusNumberProduct: contract("m/mol", "synthetic-recovery"),
  avogadroNumberEstimate: contract("1/mol", "synthetic-recovery", inferred),
  molecularInterval: contract("1/mol", "synthetic-recovery", inferred),
  simultaneousCoverage: contract("1", "conservative-coverage-lower-bound", inferred),
});
export const CAMERA_OUTPUTS = Object.freeze({
  sampleVariance: contract("m2", "known-drift-increment-second-moment"),
  sampleCovariance: contract("m2", "known-drift-adjacent-increment-product", inferred),
  secondVariance: contract("m2", "coarse-increment-second-moment", inferred),
  familyDiffusion: contract("m2/s", "compatible-camera-diffusivity"),
  familyNoise: contract("m2", "compatible-camera-localization-variance"),
  familyCovariance: contract("m2", "compatible-camera-covariance"),
  maximumDiffusion: contract("m2/s", "single-moment-compatible-bound"),
  maximumNoise: contract("m2", "single-moment-compatible-bound"),
  diffusionEstimate: contract("m2/s", "unconstrained-camera-moment-estimate", inferred),
  noiseEstimate: contract("m2", "unconstrained-localization-moment-estimate", inferred),
  physicalSolution: contract("1", "nonnegative-moment-solution", inferred),
  diffusionConfidenceInterval: contract("m2/s", "unavailable-camera-confidence-interval", inferred),
});
export const RADIUS_EXAMPLE = Object.freeze({
  radiusKnown: true,
  radius: 0.5e-6,
  radiusLower: 0.45e-6,
  radiusUpper: 0.55e-6,
  radiusCoverage: 0.975,
  radiusProvenance: "independently-declared",
  provenance:
    "Synthetic independent-radius interval for the worked example; not an actual calibration.",
});
export const RADIUS_DEFAULTS = Object.freeze({
  ...RADIUS_EXAMPLE,
  radiusKnown: false,
  provenance: "",
});
export const CAMERA_DEFAULTS = Object.freeze({ information: "variance" });
export type RadiusParameters = { [K in keyof typeof RADIUS_DEFAULTS]: (typeof RADIUS_DEFAULTS)[K] };
function outputsFor(contracts: Readonly<Record<string, OutputContract>>) {
  function meta(id: string) {
    const c = contracts[id];
    if (!c) throw new Error(`Unknown inference output: ${id}.`);
    return { quantityId: id, unit: c.unit, semanticKind: c.semanticKind, ownerId: c.ownerId };
  }
  return {
    value(id: string, value: number | Float64Array): ScientificResult {
      return { ...meta(id), status: "value", value };
    },
    absent(
      id: string,
      status: "underdetermined" | "not-applicable" | "outside-domain",
      reason: string,
    ): ScientificResult {
      return status === "underdetermined"
        ? { ...meta(id), status, compatibleFamily: reason, neededInformation: [reason] }
        : status === "outside-domain"
          ? {
              ...meta(id),
              status,
              reason,
              condition:
                "The inference requires positive, representable inputs in its admitted model.",
              domainKind: "model",
              boundary: { alternativeModel: "Inspect the stated measurement assumptions." },
            }
          : { ...meta(id), status, reason };
    },
  };
}
export const inferenceRefusal = (requirements: string): Assessment<never> => ({
  kind: "refused",
  refusal: makeRefusal(
    "invalid-parameter",
    { capabilityId: "diffusion.inference" },
    { details: { requirements } },
  ),
});
function matchingFields(input: unknown, expected: object): input is Parameters {
  return (
    !!input &&
    typeof input === "object" &&
    [Object.prototype, null].includes(Object.getPrototypeOf(input)) &&
    Reflect.ownKeys(input).length === Object.keys(expected).length &&
    Reflect.ownKeys(input).every(
      (key) =>
        typeof key === "string" &&
        Object.hasOwn(expected, key) &&
        Object.hasOwn(Object.getOwnPropertyDescriptor(input, key) ?? {}, "value"),
    )
  );
}
export function validateRadius(input: unknown): Assessment<Parameters> {
  if (
    !matchingFields(input, RADIUS_DEFAULTS) ||
    typeof input.radiusKnown !== "boolean" ||
    typeof input.provenance !== "string" ||
    input.provenance.length > 512 ||
    !["independently-declared", "same-displacements"].includes(String(input.radiusProvenance))
  )
    return inferenceRefusal("Declare the radius information using the named fields.");
  for (const key of ["radius", "radiusLower", "radiusUpper", "radiusCoverage"])
    if (typeof input[key] !== "number" || !Number.isFinite(input[key]) || input[key] <= 0)
      return inferenceRefusal("Use positive finite calibration inputs.");
  if (
    Number(input.radiusLower) > Number(input.radius) ||
    Number(input.radiusUpper) < Number(input.radius) ||
    Number(input.radiusCoverage) > 1
  )
    return inferenceRefusal(
      "Radius bounds must contain the point value; coverage must not exceed one.",
    );
  if (input.radiusKnown && !input.provenance.trim())
    return inferenceRefusal("State where the independent radius information came from.");
  return { kind: "accepted", data: Object.freeze({ ...input }) };
}
export function evaluateRadius(
  evidence: InferenceEvidence,
  input: unknown,
): Assessment<readonly ScientificResult[]> {
  if (evidence.kind !== "ideal")
    return inferenceRefusal("The radius case requires the ideal displacement example.");
  const valid = validateRadius(input);
  if (valid.kind !== "accepted") return valid;
  const p = valid.data;
  const estimate = estimateIncrements(
    Float64Array.from(evidence.increments),
    evidence.dt,
    evidence.d,
    "independent-increment-known-zero-drift",
  );
  if (estimate.kind !== "accepted") return estimate;
  const band = diffusionCompatibleBand(
    {
      estimate: estimate.data,
      T: evidence.T,
      eta: evidence.eta,
      alpha: 0.05,
      radiusRange: [0.1e-6, 2e-6],
      synthetic: true,
    },
    INFERENCE_CONSTANTS,
  );
  if (band.kind !== "accepted") return band;
  const { value, absent } = outputsFor(FAMILY_OUTPUTS);
  const b = band.data;
  const outputs: ScientificResult[] = [
    value("diffusionCoefficientEstimate", estimate.data.dHat),
    value("diffusionInterval", Float64Array.of(b.interval.lower, b.interval.upper)),
    value("radiusNumberProduct", b.product),
    value("familyRadii", b.radii),
    value("familyNumbers", b.numbers),
    value("familyLowerNumbers", b.lowerNumbers),
    value("familyUpperNumbers", b.upperNumbers),
  ];
  const conditional = conditionalMolecularNumberInterval(
    {
      estimate: estimate.data,
      T: evidence.T,
      eta: evidence.eta,
      alpha: 0.05,
      synthetic: true,
      radius: p.radiusKnown
        ? {
            value: Number(p.radius),
            interval: {
              lower: Number(p.radiusLower),
              upper: Number(p.radiusUpper),
              coverage: Number(p.radiusCoverage),
            },
            provenance: String(p.provenance),
          }
        : null,
      radiusProvenance: p.radiusProvenance as "independently-declared" | "same-displacements",
    },
    INFERENCE_CONSTANTS,
  );
  if (conditional.kind === "refused" || conditional.kind === "outcome") return conditional;
  if (conditional.kind === "no-value") {
    for (const id of ["avogadroNumberEstimate", "molecularInterval", "simultaneousCoverage"])
      outputs.push(absent(id, conditional.status, conditional.reason));
  } else {
    const pointInterval = estimatorInterval(estimate.data, 0.025);
    if (pointInterval.kind !== "accepted") return pointInterval;
    const point = invertToMolecularNumber(
      {
        T: evidence.T,
        eta: evidence.eta,
        a: Number(p.radius),
        radiusProvenance: "independently-declared",
        dHat: estimate.data.dHat,
        interval: pointInterval.data,
        synthetic: true,
      },
      INFERENCE_CONSTANTS,
    );
    if (point.kind !== "accepted") return point;
    outputs.push(
      value("avogadroNumberEstimate", point.data.estimate),
      value("molecularInterval", Float64Array.of(conditional.data.lower, conditional.data.upper)),
      value("simultaneousCoverage", conditional.data.coverage),
    );
  }
  return { kind: "accepted", data: outputs };
}

export function evaluateCamera(
  evidence: InferenceEvidence,
  input: unknown,
): Assessment<readonly ScientificResult[]> {
  if (
    evidence.kind !== "camera" ||
    !matchingFields(input, CAMERA_DEFAULTS) ||
    !["variance", "covariance", "second-interval"].includes(String(input.information))
  )
    return inferenceRefusal(
      "Choose variance alone, neighboring covariance, or a second interval for this fixed camera recording.",
    );
  const { value, absent } = outputsFor(CAMERA_OUTPUTS);
  const sample = covarianceEstimator(Float64Array.from(evidence.increments), evidence.dt, {
    d: evidence.d,
    exposure: evidence.exposure,
    knownDrift: evidence.knownDrift,
  });
  if (sample.kind !== "accepted") return sample;
  const family = cameraCompatibleFamily({
    variance: sample.data.variance,
    dt: evidence.dt,
    exposure: evidence.exposure,
  });
  if (family.kind !== "accepted") return family;
  const f = family.data,
    s = sample.data,
    method = String(input.information);
  const outputs: ScientificResult[] = [
    value("sampleVariance", s.variance),
    value("familyDiffusion", f.diffusion),
    value("familyNoise", f.noiseVariance),
    value("familyCovariance", f.covariance),
    value("maximumDiffusion", f.maximumDiffusion),
    value("maximumNoise", f.maximumNoiseVariance),
    absent(
      "diffusionConfidenceInterval",
      "not-applicable",
      "Correlated camera increments do not satisfy the independent-increment chi-square procedure. These moment solutions are point estimates, not confidence intervals.",
    ),
  ];
  outputs.push(
    method === "covariance"
      ? value("sampleCovariance", s.covariance)
      : absent(
          "sampleCovariance",
          "not-applicable",
          "Neighboring covariance has not been admitted to this question.",
        ),
  );
  let estimate: { D: number; sigma2: number; physical: boolean } | null = null;
  if (method === "second-interval") {
    // Pair adjacent increments from the SAME positions. No regeneration and no new random draw.
    const coarse = new Float64Array(evidence.increments.length / 2);
    for (let i = 0; i < coarse.length / evidence.d; i++)
      for (let c = 0; c < evidence.d; c++) {
        const left = evidence.increments[2 * i * evidence.d + c],
          right = evidence.increments[(2 * i + 1) * evidence.d + c];
        if (left === undefined || right === undefined)
          return inferenceRefusal("The paired observation layout is incomplete.");
        coarse[i * evidence.d + c] = left + right;
      }
    const second = covarianceEstimator(coarse, 2 * evidence.dt, {
      d: evidence.d,
      exposure: evidence.exposure,
      knownDrift: evidence.knownDrift,
    });
    if (second.kind !== "accepted") return second;
    outputs.push(value("secondVariance", second.data.variance));
    const solved = twoIntervalCameraEstimate({
      firstVariance: s.variance,
      secondVariance: second.data.variance,
      firstDt: evidence.dt,
      secondDt: 2 * evidence.dt,
      exposure: evidence.exposure,
    });
    if (solved.kind !== "accepted") return solved;
    estimate = solved.data;
  } else {
    outputs.push(
      absent(
        "secondVariance",
        "not-applicable",
        "A second observation interval has not been admitted to this question.",
      ),
    );
    if (method === "covariance")
      estimate = { D: s.D, sigma2: s.sigma2, physical: s.D >= 0 && s.sigma2 >= 0 };
  }
  if (estimate)
    outputs.push(
      value("diffusionEstimate", estimate.D),
      value("noiseEstimate", estimate.sigma2),
      value("physicalSolution", estimate.physical ? 1 : 0),
    );
  else
    for (const id of ["diffusionEstimate", "noiseEstimate", "physicalSolution"])
      outputs.push(
        absent(
          id,
          "underdetermined",
          "One variance leaves a compatible diffusion–noise family. Admit covariance or a second spacing to separate the two unknowns.",
        ),
      );
  return { kind: "accepted", data: outputs };
}