Reasoning laboratory · Brownian motion
Keep the data. Change what you know.
A curve can fit more than one explanation. Holding the synthetic data fixed, what extra information tells them apart?
One relation · two unknowns
Can displacements tell radius and molecular number apart?
Static worked example
Held fixed: 50 synthetic increments in 2 coordinates, 1 s apart, at 293.15 K and 1 mPa·s.
Unapplied radius information. The results still use the accepted information above.
Only displacement information has been admitted.
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.4649 × 10¹⁷
- 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 |
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.
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.8914 × 10²³ – 4.3732 × 10²³ mol⁻¹, with conservative coverage of at least 95%.
Inspect the fixed observations and their identity
Observation digest: e9445f22730e0241c22fb7fe3d9f9d7a793b30d95fec05f3fbf07623d3c1ea33
Generator source: source:sha256:d7c143066043c23f1cba39a975b3928834a1275abe090374b8853e99570ecfc5
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
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.
Variance alone leaves diffusion and noise unresolved.
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 |
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:37d9f7d44016660356c07919cd2309def99b7f3bf545d811a2b28076a63be54b
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.
The same set of observations can fit more than one explanation. Here the data stay fixed, and you add one independent piece of information at a time to see which question it settles.
Two cases hold a synthetic record fixed. In the first, 50 steps in two directions give D = 0.883 μm2/s with a 95 percent interval of 0.681 to 1.189, and Section 5's relation then fixes only the product of radius and molecular number, a·N = 1.46 × 1017 m/mol. Every point on the curve N = 1.46 × 1017/a fits equally well: a radius of 0.1 μm with 14.6 × 1023, 0.5 μm with 2.93 × 1023, 2 μm with 0.73 × 1023. Only an independent measurement of the radius picks a point: with 0.5 μm known to lie between 0.45 and 0.55 μm at 97.5 percent coverage, N lies between 1.89 and 4.37 × 1023 with at least 95 percent coverage. In the second case a camera record's step variance, 0.812 μm2, could come from the particle's motion or from errors in locating it. The two trade off along a line, and only a second relation, the covariance of neighbouring steps or the variance at twice the spacing, separates them.
Case one. From the 100 squared steps, D̂ = 0.883 μm2/s, and the chi-square bounds for 100 degrees of freedom, 74.2 and 129.6, turn that into 0.681 to 1.189 μm2/s. Einstein's D = RT/(6πηaN) can be turned around for the product: a·N = RT/(6πηD̂) = 2437/(6 × 3.1416 × 0.001 × 8.83 × 10−13) = 1.465 × 1017 m/mol. Doubling a and halving N leaves that unchanged, so the data define a curve, not a point. Now admit a radius interval, 0.45 to 0.55 μm, that is right with probability at least 97.5 percent, and use the 97.5 percent interval for D, which is a little wider than the 95 percent one. If both are right, which fails with probability at most 2.5 + 2.5 = 5 percent, N lies between the smallest and largest values the two ranges allow, from RT/(6πη Dmax amax) to RT/(6πη Dmin amin): 1.89 to 4.37 × 1023. That is a conservative bound, not a probability for this one interval. Case two. With localization errors of variance σ2 and an exposure of half the frame spacing, a step's variance is 2D(τ − Te/3) + 2σ2 = 1.667D + 2σ2. One number, 0.812 μm2, cannot fix two unknowns: D = 0.238 μm2/s with σ2 = 0.208 μm2 fits it exactly, and so does D = 0.012 with σ2 = 0.396. The neighbouring covariance, DTe/3 − σ2, differs along that line, which is why admitting it supplies the second equation.
Modern qualifications
Section 5 of the Brownian paper already gives N only in terms of a known radius, and Perrin measured his grains' radii separately, by their rate of settling and by weighing, before counting molecules. Separating motion from measurement error, and combining two intervals by adding their error probabilities, are modern statistics. The records here are synthetic, made by a known generator, so the exercise tests the reasoning rather than the world.
What the calculation does and does not establish
The two datasets are built in: 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.
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:e8f8db96507a9df478019b1fe3e8b822330a887950cac0fdac391500280ed1af
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 };
}