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.
Read all compatible pairs
| Radius (μm) | N (10²³ mol⁻¹) | Lower N | Upper N |
|---|---|---|---|
| 0.1 | 14.649 | 10.873 | 18.979 |
| 0.10778 | 13.592 | 10.088 | 17.61 |
| 0.11616 | 12.611 | 9.3602 | 16.339 |
| 0.12519 | 11.701 | 8.6848 | 15.16 |
| 0.13493 | 10.857 | 8.0581 | 14.066 |
| 0.14542 | 10.073 | 7.4767 | 13.051 |
| 0.15673 | 9.3465 | 6.9372 | 12.109 |
| 0.16892 | 8.6721 | 6.4366 | 11.236 |
| 0.18206 | 8.0463 | 5.9722 | 10.425 |
| 0.19621 | 7.4657 | 5.5412 | 9.6727 |
| 0.21147 | 6.927 | 5.1414 | 8.9747 |
| 0.22792 | 6.4272 | 4.7704 | 8.3272 |
| 0.24565 | 5.9634 | 4.4262 | 7.7263 |
| 0.26475 | 5.5331 | 4.1068 | 7.1688 |
| 0.28534 | 5.1339 | 3.8105 | 6.6515 |
| 0.30753 | 4.7634 | 3.5355 | 6.1715 |
| 0.33145 | 4.4197 | 3.2804 | 5.7262 |
| 0.35722 | 4.1008 | 3.0437 | 5.313 |
| 0.385 | 3.8049 | 2.8241 | 4.9297 |
| 0.41494 | 3.5303 | 2.6203 | 4.5739 |
| 0.44721 | 3.2756 | 2.4312 | 4.2439 |
| 0.48199 | 3.0392 | 2.2558 | 3.9377 |
| 0.51948 | 2.8199 | 2.093 | 3.6535 |
| 0.55988 | 2.6165 | 1.942 | 3.3899 |
| 0.60342 | 2.4277 | 1.8019 | 3.1453 |
| 0.65034 | 2.2525 | 1.6718 | 2.9183 |
| 0.70092 | 2.0899 | 1.5512 | 2.7078 |
| 0.75543 | 1.9391 | 1.4393 | 2.5124 |
| 0.81418 | 1.7992 | 1.3354 | 2.3311 |
| 0.8775 | 1.6694 | 1.2391 | 2.1629 |
| 0.94574 | 1.5489 | 1.1496 | 2.0068 |
| 1.0193 | 1.4372 | 1.0667 | 1.862 |
| 1.0986 | 1.3335 | 0.98972 | 1.7277 |
| 1.184 | 1.2372 | 0.91831 | 1.603 |
| 1.2761 | 1.148 | 0.85204 | 1.4873 |
| 1.3753 | 1.0651 | 0.79056 | 1.38 |
| 1.4823 | 0.98828 | 0.73352 | 1.2804 |
| 1.5975 | 0.91696 | 0.68059 | 1.188 |
| 1.7218 | 0.8508 | 0.63148 | 1.1023 |
| 1.8557 | 0.78941 | 0.58591 | 1.0228 |
| 2 | 0.73244 | 0.54363 | 0.94896 |
Unapplied radius information. The results still use the accepted information above.
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.
| Frame increment | Coordinate | Displacement (μm) |
|---|---|---|
| 1 | x | -2.3879 |
| 1 | y | -0.70732 |
| 2 | x | 0.76359 |
| 2 | y | -0.36669 |
| 3 | x | -0.78703 |
| 3 | y | 2.6303 |
| 4 | x | 1.6368 |
| 4 | y | 1.3734 |
| 5 | x | 2.6295 |
| 5 | y | 1.4292 |
| 6 | x | -1.158 |
| 6 | y | 0.3752 |
| 7 | x | -1.4365 |
| 7 | y | -0.034373 |
| 8 | x | 2.8597 |
| 8 | y | -0.38513 |
| 9 | x | -1.1757 |
| 9 | y | 0.10385 |
| 10 | x | -0.39354 |
| 10 | y | 1.5456 |
| 11 | x | 0.085157 |
| 11 | y | 1.9454 |
| 12 | x | 0.28814 |
| 12 | y | -1.5271 |
| 13 | x | 1.4515 |
| 13 | y | 0.69424 |
| 14 | x | 0.259 |
| 14 | y | 0.30462 |
| 15 | x | 0.072121 |
| 15 | y | 0.51253 |
| 16 | x | -1.0618 |
| 16 | y | -0.85806 |
| 17 | x | -0.14009 |
| 17 | y | 0.69891 |
| 18 | x | -1.4962 |
| 18 | y | -0.11731 |
| 19 | x | 1.6779 |
| 19 | y | -1.3527 |
| 20 | x | -1.0475 |
| 20 | y | -0.10093 |
| 21 | x | -1.3041 |
| 21 | y | -0.57943 |
| 22 | x | -2.2484 |
| 22 | y | 0.532 |
| 23 | x | 0.42622 |
| 23 | y | -0.40314 |
| 24 | x | -0.072652 |
| 24 | y | 1.5005 |
| 25 | x | 0.62345 |
| 25 | y | -0.67176 |
| 26 | x | -1.4449 |
| 26 | y | 0.088609 |
| 27 | x | 1.0489 |
| 27 | y | -1.0495 |
| 28 | x | 0.28654 |
| 28 | y | -2.6253 |
| 29 | x | -1.517 |
| 29 | y | 0.81859 |
| 30 | x | 3.3981 |
| 30 | y | -0.61668 |
| 31 | x | -1.2871 |
| 31 | y | 0.033954 |
| 32 | x | -0.61602 |
| 32 | y | -1.7772 |
| 33 | x | -0.44696 |
| 33 | y | 0.19549 |
| 34 | x | 0.83176 |
| 34 | y | 3.1783 |
| 35 | x | 1.1501 |
| 35 | y | -0.98462 |
| 36 | x | -1.1138 |
| 36 | y | 2.1577 |
| 37 | x | 0.39373 |
| 37 | y | -1.5193 |
| 38 | x | -0.9805 |
| 38 | y | 0.85426 |
| 39 | x | 0.48911 |
| 39 | y | 1.7621 |
| 40 | x | 0.26211 |
| 40 | y | 0.275 |
| 41 | x | 2.4597 |
| 41 | y | -0.70916 |
| 42 | x | 1.6996 |
| 42 | y | 0.41987 |
| 43 | x | -2.2061 |
| 43 | y | -1.5354 |
| 44 | x | -0.05283 |
| 44 | y | -0.76038 |
| 45 | x | 0.50145 |
| 45 | y | -2.55 |
| 46 | x | 2.9085 |
| 46 | y | -1.3428 |
| 47 | x | -0.47992 |
| 47 | y | -1.6202 |
| 48 | x | -0.9242 |
| 48 | y | 0.021895 |
| 49 | x | -1.8314 |
| 49 | y | -0.44372 |
| 50 | x | -0.57255 |
| 50 | y | 1.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.
Accepted information: one increment variance. The recorded positions have not changed.
| Moment | Sample |
|---|---|
| Increment variance | 0.81215 |
| Adjacent-increment covariance | Neighboring 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.
Compare candidate pairs without the graph
| D (μm²/s) | Noise variance (μm²) | Predicted covariance (μm²) |
|---|---|---|
| 0.011885 | 0.39617 | -0.39419 |
| 0.02377 | 0.38627 | -0.38231 |
| 0.035656 | 0.37636 | -0.37042 |
| 0.047541 | 0.36646 | -0.35854 |
| 0.059426 | 0.35656 | -0.34665 |
| 0.071311 | 0.34665 | -0.33477 |
| 0.083196 | 0.33675 | -0.32288 |
| 0.095081 | 0.32684 | -0.311 |
| 0.10697 | 0.31694 | -0.29911 |
| 0.11885 | 0.30703 | -0.28722 |
| 0.13074 | 0.29713 | -0.27534 |
| 0.14262 | 0.28722 | -0.26345 |
| 0.15451 | 0.27732 | -0.25157 |
| 0.16639 | 0.26742 | -0.23968 |
| 0.17828 | 0.25751 | -0.2278 |
| 0.19016 | 0.24761 | -0.21591 |
| 0.20205 | 0.2377 | -0.20403 |
| 0.21393 | 0.2278 | -0.19214 |
| 0.22582 | 0.21789 | -0.18026 |
| 0.2377 | 0.20799 | -0.16837 |
| 0.24959 | 0.19809 | -0.15649 |
| 0.26147 | 0.18818 | -0.1446 |
| 0.27336 | 0.17828 | -0.13272 |
| 0.28524 | 0.16837 | -0.12083 |
| 0.29713 | 0.15847 | -0.10895 |
| 0.30901 | 0.14856 | -0.097062 |
| 0.3209 | 0.13866 | -0.085177 |
| 0.33278 | 0.12876 | -0.073292 |
| 0.34467 | 0.11885 | -0.061407 |
| 0.35656 | 0.10895 | -0.049522 |
| 0.36844 | 0.099043 | -0.037636 |
| 0.38033 | 0.089139 | -0.025751 |
| 0.39221 | 0.079234 | -0.013866 |
| 0.4041 | 0.06933 | -0.0019809 |
| 0.41598 | 0.059426 | 0.0099043 |
| 0.42787 | 0.049522 | 0.021789 |
| 0.43975 | 0.039617 | 0.033675 |
| 0.45164 | 0.029713 | 0.04556 |
| 0.46352 | 0.019809 | 0.057445 |
| 0.47541 | 0.0099043 | 0.06933 |
| 0.48729 | 0 | 0.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.
| Frame increment | Coordinate | Displacement (μm) |
|---|---|---|
| 1 | x | 0.052152 |
| 1 | y | 1.1727 |
| 2 | x | 1.3394 |
| 2 | y | 0.55004 |
| 3 | x | -0.25739 |
| 3 | y | 3.3721 |
| 4 | x | 0.93817 |
| 4 | y | 1.2256 |
| 5 | x | 0.17098 |
| 5 | y | 0.23457 |
| 6 | x | -0.38416 |
| 6 | y | 0.83982 |
| 7 | x | 1.341 |
| 7 | y | 0.38545 |
| 8 | x | 0.49323 |
| 8 | y | -0.47663 |
| 9 | x | -0.17185 |
| 9 | y | 0.51197 |
| 10 | x | 1.2638 |
| 10 | y | 1.2212 |
| 11 | x | -2.0775 |
| 11 | y | 0.071756 |
| 12 | x | -1.4292 |
| 12 | y | -0.82665 |
| 13 | x | -0.48458 |
| 13 | y | 0.57606 |
| 14 | x | 1.1733 |
| 14 | y | 0.52255 |
| 15 | x | -0.21156 |
| 15 | y | 0.10566 |
| 16 | x | 1.4506 |
| 16 | y | 0.068282 |
| 17 | x | -0.36709 |
| 17 | y | 0.35189 |
| 18 | x | 0.85611 |
| 18 | y | -0.29712 |
| 19 | x | 0.70947 |
| 19 | y | -0.011905 |
| 20 | x | 1.1738 |
| 20 | y | -0.12242 |
| 21 | x | -1.2163 |
| 21 | y | -0.36858 |
| 22 | x | -1.2302 |
| 22 | y | 0.084949 |
| 23 | x | -1.0645 |
| 23 | y | 0.47693 |
| 24 | x | 0.24489 |
| 24 | y | 1.2602 |
| 25 | x | 0.60269 |
| 25 | y | -0.4288 |
| 26 | x | -0.63745 |
| 26 | y | -0.24701 |
| 27 | x | -0.88825 |
| 27 | y | 0.64947 |
| 28 | x | 1.5901 |
| 28 | y | -0.97774 |
| 29 | x | 0.9249 |
| 29 | y | -1.5412 |
| 30 | x | -0.58705 |
| 30 | y | 0.27076 |
| 31 | x | 1.067 |
| 31 | y | 0.20418 |
| 32 | x | 0.40663 |
| 32 | y | 0.65126 |
| 33 | x | -0.95103 |
| 33 | y | -3.0736 |
| 34 | x | -1.6096 |
| 34 | y | -2.1956 |
| 35 | x | -0.7874 |
| 35 | y | 1.0323 |
| 36 | x | 2.4222 |
| 36 | y | 0.72772 |
| 37 | x | 1.2379 |
| 37 | y | 0.37757 |
| 38 | x | -0.75227 |
| 38 | y | 0.56343 |
| 39 | x | -0.19214 |
| 39 | y | -0.079906 |
| 40 | x | -1.0351 |
| 40 | y | 0.023257 |
| 41 | x | 0.75696 |
| 41 | y | 1.0211 |
| 42 | x | -1.0429 |
| 42 | y | -0.6176 |
| 43 | x | 0.20427 |
| 43 | y | 0.34629 |
| 44 | x | 1.1992 |
| 44 | y | -0.033293 |
| 45 | x | 0.92087 |
| 45 | y | 0.044388 |
| 46 | x | -0.17736 |
| 46 | y | 1.2705 |
| 47 | x | -0.62433 |
| 47 | y | 2.0321 |
| 48 | x | -0.494 |
| 48 | y | 0.23545 |
| 49 | x | 1.2259 |
| 49 | y | -1.5715 |
| 50 | x | -0.51899 |
| 50 | y | 0.45366 |
| 51 | x | -0.25733 |
| 51 | y | -1.4979 |
| 52 | x | -0.47704 |
| 52 | y | 0.42919 |
| 53 | x | 0.81272 |
| 53 | y | -0.077847 |
| 54 | x | 0.15059 |
| 54 | y | -0.92853 |
| 55 | x | 0.54609 |
| 55 | y | -0.61767 |
| 56 | x | -1.5655 |
| 56 | y | -0.029961 |
| 57 | x | -0.31321 |
| 57 | y | 1.25 |
| 58 | x | -1.3451 |
| 58 | y | -0.11232 |
| 59 | x | 0.51877 |
| 59 | y | 0.92947 |
| 60 | x | 0.35182 |
| 60 | y | 0.19422 |
| 61 | x | 0.3776 |
| 61 | y | 0.15838 |
| 62 | x | -0.86027 |
| 62 | y | 0.68658 |
| 63 | x | -1.3589 |
| 63 | y | 1.0646 |
| 64 | x | -2.3538 |
| 64 | y | -0.20533 |
| 65 | x | 0.23645 |
| 65 | y | -1.171 |
| 66 | x | -0.11725 |
| 66 | y | -0.90494 |
| 67 | x | 0.57605 |
| 67 | y | -0.64669 |
| 68 | x | -0.0049188 |
| 68 | y | 1.0388 |
| 69 | x | 0.20831 |
| 69 | y | 1.1579 |
| 70 | x | -1.0463 |
| 70 | y | 0.80787 |
| 71 | x | -1.0439 |
| 71 | y | -0.49011 |
| 72 | x | 0.02682 |
| 72 | y | 0.096069 |
| 73 | x | -1.2335 |
| 73 | y | -0.23691 |
| 74 | x | 0.1886 |
| 74 | y | 0.66849 |
| 75 | x | -1.7375 |
| 75 | y | -0.65304 |
| 76 | x | 0.11388 |
| 76 | y | 0.87759 |
| 77 | x | -0.53453 |
| 77 | y | 0.39029 |
| 78 | x | -0.97624 |
| 78 | y | 1.4547 |
| 79 | x | -0.27295 |
| 79 | y | -0.35166 |
| 80 | x | 0.83092 |
| 80 | y | -1.5903 |
| 81 | x | 1.1025 |
| 81 | y | 0.20143 |
| 82 | x | -1.1893 |
| 82 | y | -0.16656 |
| 83 | x | 0.14528 |
| 83 | y | 0.37482 |
| 84 | x | -0.12985 |
| 84 | y | 0.88712 |
| 85 | x | -1.5348 |
| 85 | y | 0.7736 |
| 86 | x | -0.36071 |
| 86 | y | -0.89011 |
| 87 | x | 0.43223 |
| 87 | y | -0.64712 |
| 88 | x | 0.12764 |
| 88 | y | -0.1486 |
| 89 | x | -0.20486 |
| 89 | y | 0.035909 |
| 90 | x | 0.070332 |
| 90 | y | -0.20517 |
| 91 | x | -0.23762 |
| 91 | y | -0.59373 |
| 92 | x | -0.15501 |
| 92 | y | -0.33536 |
| 93 | x | -0.45727 |
| 93 | y | -1.4671 |
| 94 | x | 0.83387 |
| 94 | y | 1.014 |
| 95 | x | 0.43857 |
| 95 | y | -0.6013 |
| 96 | x | -0.98601 |
| 96 | y | -0.58897 |
| 97 | x | 0.15066 |
| 97 | y | -0.32546 |
| 98 | x | 0.046874 |
| 98 | y | -0.87084 |
| 99 | x | -0.077103 |
| 99 | y | 0.32433 |
| 100 | x | -0.77186 |
| 100 | y | -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 };
}