@photivra/engine 2.10.0 → 2.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +139 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +21 -3
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/dist/core/portable-math.d.ts +6 -0
  7. package/dist/core/portable-math.js +39 -0
  8. package/dist/core/portable-math.js.map +1 -1
  9. package/dist/core/version.d.ts +1 -1
  10. package/dist/core/version.js +1 -1
  11. package/dist/core/version.js.map +1 -1
  12. package/dist/facade/chart-material-layers.d.ts +115 -0
  13. package/dist/facade/chart-material-layers.js +224 -0
  14. package/dist/facade/chart-material-layers.js.map +1 -0
  15. package/dist/facade/chart-radiance-batch.d.ts +8 -2
  16. package/dist/facade/chart-radiance-batch.js +11 -4
  17. package/dist/facade/chart-radiance-batch.js.map +1 -1
  18. package/dist/facade/chart-scenes.d.ts +134 -0
  19. package/dist/facade/chart-scenes.js +32 -22
  20. package/dist/facade/chart-scenes.js.map +1 -1
  21. package/dist/facade/diffraction-provider.d.ts +8 -2
  22. package/dist/facade/diffraction-provider.js +71 -2
  23. package/dist/facade/diffraction-provider.js.map +1 -1
  24. package/dist/facade/display-view-gpu-session.d.ts +18 -0
  25. package/dist/facade/display-view-gpu-session.js +280 -0
  26. package/dist/facade/display-view-gpu-session.js.map +1 -0
  27. package/dist/facade/display-view-gpu.d.ts +146 -0
  28. package/dist/facade/display-view-gpu.js +59 -0
  29. package/dist/facade/display-view-gpu.js.map +1 -0
  30. package/dist/facade/display-view.d.ts +48 -1
  31. package/dist/facade/display-view.js +7 -7
  32. package/dist/facade/display-view.js.map +1 -1
  33. package/dist/facade/expected-signal-setup.d.ts +5 -0
  34. package/dist/facade/expected-signal-setup.js +16 -0
  35. package/dist/facade/expected-signal-setup.js.map +1 -1
  36. package/dist/facade/expected-signal.d.ts +48 -3
  37. package/dist/facade/expected-signal.js +8 -0
  38. package/dist/facade/expected-signal.js.map +1 -1
  39. package/dist/facade/scene-radiance.d.ts +4 -2
  40. package/dist/facade/scene-radiance.js +7 -4
  41. package/dist/facade/scene-radiance.js.map +1 -1
  42. package/dist/facade/simulate-capture.d.ts +32 -5
  43. package/dist/facade/simulate-capture.js +148 -24
  44. package/dist/facade/simulate-capture.js.map +1 -1
  45. package/dist/facade/simulate-display-view.d.ts +31 -3
  46. package/dist/facade/simulate-display-view.js +72 -12
  47. package/dist/facade/simulate-display-view.js.map +1 -1
  48. package/dist/gpu/chart-composite-gpu.d.ts +119 -0
  49. package/dist/gpu/chart-composite-gpu.js +572 -0
  50. package/dist/gpu/chart-composite-gpu.js.map +1 -0
  51. package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
  52. package/dist/gpu/chart-radiance-gpu.js +9 -11
  53. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  54. package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
  55. package/dist/gpu/display-view-chart-material-module.js +58 -0
  56. package/dist/gpu/display-view-chart-material-module.js.map +1 -0
  57. package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
  58. package/dist/gpu/display-view-composite-material-module.js +248 -0
  59. package/dist/gpu/display-view-composite-material-module.js.map +1 -0
  60. package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
  61. package/dist/gpu/display-view-quality-gpu.js +1000 -0
  62. package/dist/gpu/display-view-quality-gpu.js.map +1 -0
  63. package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
  64. package/dist/gpu/display-view-speed-gpu.js +524 -0
  65. package/dist/gpu/display-view-speed-gpu.js.map +1 -0
  66. package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
  67. package/dist/gpu/image-kernel-accelerator.js +58 -10
  68. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  69. package/dist/gpu/image-kernel-execution.d.ts +55 -3
  70. package/dist/gpu/image-kernel-execution.js.map +1 -1
  71. package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
  72. package/dist/gpu/sensor-stage-gpu.js +42 -13
  73. package/dist/gpu/sensor-stage-gpu.js.map +1 -1
  74. package/dist/gpu/webgpu-runtime.d.ts +4 -2
  75. package/dist/gpu/webgpu-runtime.js +13 -4
  76. package/dist/gpu/webgpu-runtime.js.map +1 -1
  77. package/dist/gpu/webgpu-switch.d.ts +4 -0
  78. package/dist/gpu/webgpu-switch.js +29 -0
  79. package/dist/gpu/webgpu-switch.js.map +1 -0
  80. package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
  81. package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
  82. package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
  83. package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
  84. package/dist/gpu/wgsl-chart-kernel.js +33 -27
  85. package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
  86. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
  87. package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
  88. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
  89. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
  90. package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
  91. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
  92. package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
  93. package/dist/gpu/wgsl-sensor-kernel.js +94 -50
  94. package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
  95. package/dist/index.d.ts +4 -1
  96. package/dist/index.js +2 -0
  97. package/dist/index.js.map +1 -1
  98. package/dist/optics/defocus-disc-kernel.d.ts +61 -0
  99. package/dist/optics/defocus-disc-kernel.js +191 -0
  100. package/dist/optics/defocus-disc-kernel.js.map +1 -0
  101. package/docs/API_REFERENCE.md +45 -1
  102. package/docs/QUICK_START.md +102 -3
  103. package/docs/USAGE.md +52 -2
  104. package/docs/api/core-version.md +1 -1
  105. package/docs/api/exports.json +117 -2
  106. package/docs/api/facade-chart-material-layers.md +137 -0
  107. package/docs/api/facade-chart-radiance-batch.md +7 -2
  108. package/docs/api/facade-display-view-gpu.md +94 -0
  109. package/docs/api/facade-scene-radiance.md +3 -2
  110. package/docs/api/facade-simulate-capture.md +23 -6
  111. package/docs/api/facade-simulate-display-view.md +17 -2
  112. package/docs/api/gpu-image-kernel-accelerator.md +10 -3
  113. package/docs/api/gpu-image-kernel-execution.md +71 -4
  114. package/docs/api/gpu-webgpu-switch.md +23 -0
  115. package/package.json +3 -1
@@ -0,0 +1,191 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Internal (not exported from the package): the geometric defocus disc of a flat scene, sampled on the square site
4
+ * lattice, for `lens.defocus: { model: "geometric-disc" }` (#492).
5
+ *
6
+ * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil
7
+ * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor
8
+ * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused
9
+ * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent
10
+ * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).
11
+ *
12
+ * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at
13
+ * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided
14
+ * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of
15
+ * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with
16
+ * x, y clamped to the radius r
17
+ * Q = x·y when x² + y² ≤ r²,
18
+ * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,
19
+ * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the
20
+ * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry
21
+ * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc
22
+ * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an
23
+ * odd function.
24
+ *
25
+ * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in
26
+ * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold
27
+ * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight
28
+ * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.
29
+ *
30
+ * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a
31
+ * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the
32
+ * cap and the radius asked for, instead of clipping the disc.
33
+ */
34
+ import { portableAsin } from "../core/portable-math.js";
35
+ import { InvalidScientificInputError } from "../core/validation.js";
36
+ import { calculateDefocusCircle } from "./depth-of-field.js";
37
+ /**
38
+ * Largest kernel radius in sites, R ≤ 64 (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32).
39
+ * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, "Optics"): the capture's time grows with
40
+ * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at
41
+ * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus
42
+ * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.
43
+ */
44
+ export const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;
45
+ /** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */
46
+ export function defocusDiscKernelRadius(diameterSites) {
47
+ requireDiameter(diameterSites);
48
+ const r = diameterSites / 2;
49
+ return r <= 0.5 ? 0 : Math.ceil(r - 0.5);
50
+ }
51
+ /**
52
+ * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)
53
+ * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.
54
+ */
55
+ export function requireDefocusDiscKernelRadius(diameterSites) {
56
+ const radius = defocusDiscKernelRadius(diameterSites);
57
+ if (radius > DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES)
58
+ throw new InvalidScientificInputError("The defocus disc of " + diameterSites + " sites needs a kernel radius of " + radius +
59
+ " sites; the limit is " + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + " sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).");
60
+ return radius;
61
+ }
62
+ function requireDiameter(diameterSites) {
63
+ if (typeof diameterSites !== "number" || !Number.isFinite(diameterSites) || diameterSites < 0)
64
+ throw new InvalidScientificInputError("The defocus disc diameter must be a finite number of sites, zero or more.");
65
+ }
66
+ /** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */
67
+ function quadrantArea(x, y, r) {
68
+ if (x > r)
69
+ x = r;
70
+ if (y > r)
71
+ y = r;
72
+ if (x * x + y * y <= r * r)
73
+ return x * y;
74
+ const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));
75
+ // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².
76
+ return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));
77
+ }
78
+ /** {@link quadrantArea} extended as an odd function of x and of y. */
79
+ function signedQuadrantArea(x, y, r) {
80
+ const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);
81
+ return (x < 0) !== (y < 0) ? -q : q;
82
+ }
83
+ /**
84
+ * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest
85
+ * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference
86
+ * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);
87
+ * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.
88
+ */
89
+ function cellArea(i, j, r) {
90
+ const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;
91
+ const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;
92
+ if (nx * nx + ny * ny >= r * r)
93
+ return 0;
94
+ const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));
95
+ return area > 0 ? area : 0;
96
+ }
97
+ /**
98
+ * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:
99
+ * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when
100
+ * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.
101
+ */
102
+ export function createDefocusDiscSiteKernel(diameterSites) {
103
+ const radius = requireDefocusDiscKernelRadius(diameterSites);
104
+ if (diameterSites === 0)
105
+ return null;
106
+ const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);
107
+ // One octant, mirrored into the eight symmetric cells.
108
+ for (let i = 0; i <= radius; i++)
109
+ for (let j = 0; j <= i; j++) {
110
+ const a = cellArea(i, j, r);
111
+ for (const [u, v] of [[i, j], [j, i]])
112
+ for (const su of u === 0 ? [1] : [1, -1])
113
+ for (const sv of v === 0 ? [1] : [1, -1])
114
+ weights[(sv * v + radius) * width + su * u + radius] = a;
115
+ }
116
+ normalize(weights, (width * width - 1) / 2);
117
+ return { diameterSites, radius, width, weights };
118
+ }
119
+ /**
120
+ * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a
121
+ * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in
122
+ * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight
123
+ * can move a rounded sum from just below 1 to just above it.)
124
+ */
125
+ function normalize(weights, centre) {
126
+ let total = 0;
127
+ for (let k = 0; k < weights.length; k++)
128
+ total += weights[k];
129
+ for (let k = 0; k < weights.length; k++)
130
+ weights[k] /= total;
131
+ let hi = 0, lo = 0;
132
+ for (let k = 0; k < weights.length; k++) {
133
+ if (k === centre)
134
+ continue;
135
+ const w = weights[k], s = hi + w, b = s - hi;
136
+ lo += (hi - (s - b)) + (w - b);
137
+ hi = s;
138
+ }
139
+ // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.
140
+ const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);
141
+ weights[centre] = s + (e - lo);
142
+ }
143
+ /**
144
+ * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a
145
+ * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.
146
+ */
147
+ export function resolveDefocusDisc(input) {
148
+ const { pitchMicrometers, ...circle } = input;
149
+ if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0)
150
+ throw new InvalidScientificInputError("pitchMicrometers must be a positive finite number.");
151
+ const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;
152
+ const diameterSites = diameterMicrometers / pitchMicrometers;
153
+ return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };
154
+ }
155
+ /**
156
+ * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius
157
+ * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps
158
+ * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral
159
+ * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF
160
+ * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are
161
+ * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.
162
+ */
163
+ export function convolveSiteKernels(a, radiusA, b, radiusB) {
164
+ const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;
165
+ if (a.length !== wa * wa || b.length !== wb * wb)
166
+ throw new InvalidScientificInputError("Site kernels must be (2R + 1)² taps.");
167
+ const out = new Float64Array(width * width);
168
+ // Non-zero taps of b, as (row, column, weight).
169
+ const bRows = [], bColumns = [], bWeights = [];
170
+ for (let j = 0; j < wb; j++)
171
+ for (let i = 0; i < wb; i++) {
172
+ const w = b[j * wb + i];
173
+ if (w !== 0) {
174
+ bRows.push(j);
175
+ bColumns.push(i);
176
+ bWeights.push(w);
177
+ }
178
+ }
179
+ for (let sj = 0; sj < wa; sj++)
180
+ for (let si = 0; si < wa; si++) {
181
+ const w = a[sj * wa + si];
182
+ if (w === 0)
183
+ continue;
184
+ for (let k = 0; k < bWeights.length; k++) {
185
+ const index = (sj + bRows[k]) * width + si + bColumns[k];
186
+ out[index] = out[index] + w * bWeights[k];
187
+ }
188
+ }
189
+ return out;
190
+ }
191
+ //# sourceMappingURL=defocus-disc-kernel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"defocus-disc-kernel.js","sourceRoot":"","sources":["../../src/optics/defocus-disc-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): the geometric defocus disc of a flat scene, sampled on the square site\n * lattice, for `lens.defocus: { model: \"geometric-disc\" }` (#492).\n *\n * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil\n * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor\n * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused\n * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent\n * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).\n *\n * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at\n * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided\n * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of\n * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with\n * x, y clamped to the radius r\n * Q = x·y when x² + y² ≤ r²,\n * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,\n * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the\n * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry\n * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc\n * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an\n * odd function.\n *\n * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in\n * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold\n * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight\n * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.\n *\n * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a\n * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the\n * cap and the radius asked for, instead of clipping the disc.\n */\n\nimport { portableAsin } from \"../core/portable-math.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { calculateDefocusCircle } from \"./depth-of-field.js\";\n\n/**\n * Largest kernel radius in sites, R ≤ 64 (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32).\n * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, \"Optics\"): the capture's time grows with\n * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at\n * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus\n * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.\n */\nexport const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;\n\n/** The disc sampled on the site lattice. */\nexport interface DefocusDiscSiteKernel {\n /** Disc diameter in sites (c / pitch). */\n readonly diameterSites: number;\n /** Kernel radius R in sites: the window is (2R + 1) × (2R + 1). */\n readonly radius: number;\n /** Window side, 2R + 1. */\n readonly width: number;\n /** Row-major weights, index (j + R)·width + (i + R) for the site offset (i, j); their row-major float64 sum is 1. */\n readonly weights: Float64Array;\n}\n\n/** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */\nexport function defocusDiscKernelRadius(diameterSites: number): number {\n requireDiameter(diameterSites);\n const r = diameterSites / 2;\n return r <= 0.5 ? 0 : Math.ceil(r - 0.5);\n}\n\n/**\n * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)\n * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.\n */\nexport function requireDefocusDiscKernelRadius(diameterSites: number): number {\n const radius = defocusDiscKernelRadius(diameterSites);\n if (radius > DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES)\n throw new InvalidScientificInputError(\"The defocus disc of \" + diameterSites + \" sites needs a kernel radius of \" + radius +\n \" sites; the limit is \" + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + \" sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).\");\n return radius;\n}\n\nfunction requireDiameter(diameterSites: number): void {\n if (typeof diameterSites !== \"number\" || !Number.isFinite(diameterSites) || diameterSites < 0)\n throw new InvalidScientificInputError(\"The defocus disc diameter must be a finite number of sites, zero or more.\");\n}\n\n/** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */\nfunction quadrantArea(x: number, y: number, r: number): number {\n if (x > r) x = r;\n if (y > r) y = r;\n if (x * x + y * y <= r * r) return x * y;\n const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));\n // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².\n return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));\n}\n/** {@link quadrantArea} extended as an odd function of x and of y. */\nfunction signedQuadrantArea(x: number, y: number, r: number): number {\n const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);\n return (x < 0) !== (y < 0) ? -q : q;\n}\n/**\n * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest\n * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference\n * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);\n * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.\n */\nfunction cellArea(i: number, j: number, r: number): number {\n const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;\n const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;\n if (nx * nx + ny * ny >= r * r) return 0;\n const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));\n return area > 0 ? area : 0;\n}\n\n/**\n * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:\n * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when\n * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.\n */\nexport function createDefocusDiscSiteKernel(diameterSites: number): DefocusDiscSiteKernel | null {\n const radius = requireDefocusDiscKernelRadius(diameterSites);\n if (diameterSites === 0) return null;\n const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);\n // One octant, mirrored into the eight symmetric cells.\n for (let i = 0; i <= radius; i++) for (let j = 0; j <= i; j++) {\n const a = cellArea(i, j, r);\n for (const [u, v] of [[i, j], [j, i]] as const) for (const su of u === 0 ? [1] : [1, -1]) for (const sv of v === 0 ? [1] : [1, -1])\n weights[(sv * v + radius) * width + su * u + radius] = a;\n }\n normalize(weights, (width * width - 1) / 2);\n return { diameterSites, radius, width, weights };\n}\n\n/**\n * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a\n * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in\n * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight\n * can move a rounded sum from just below 1 to just above it.)\n */\nfunction normalize(weights: Float64Array, centre: number): void {\n let total = 0;\n for (let k = 0; k < weights.length; k++) total += weights[k]!;\n for (let k = 0; k < weights.length; k++) weights[k]! /= total;\n let hi = 0, lo = 0;\n for (let k = 0; k < weights.length; k++) {\n if (k === centre) continue;\n const w = weights[k]!, s = hi + w, b = s - hi;\n lo += (hi - (s - b)) + (w - b);\n hi = s;\n }\n // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.\n const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);\n weights[centre] = s + (e - lo);\n}\n\n/** The geometric defocus disc of one request, in micrometres and in sites. */\nexport interface ResolvedDefocusDisc {\n /** Blur-circle diameter c at the sensor, µm (`calculateDefocusCircle`, thin-lens model 1.1.0). */\n readonly diameterMicrometers: number;\n /** c / pitch. */\n readonly diameterSites: number;\n /** Kernel radius R in sites (0 when c = 0). */\n readonly kernelRadiusSites: number;\n}\n\n/**\n * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a\n * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.\n */\nexport function resolveDefocusDisc(input: { focalLengthMm: number; aperture: number; focusDistanceM: number; subjectDistanceM: number;\n pitchMicrometers: number }): ResolvedDefocusDisc {\n const { pitchMicrometers, ...circle } = input;\n if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0) throw new InvalidScientificInputError(\"pitchMicrometers must be a positive finite number.\");\n const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;\n const diameterSites = diameterMicrometers / pitchMicrometers;\n return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };\n}\n\n/**\n * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius\n * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps\n * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral\n * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF\n * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are\n * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.\n */\nexport function convolveSiteKernels(a: Float64Array, radiusA: number, b: Float64Array, radiusB: number): Float64Array {\n const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;\n if (a.length !== wa * wa || b.length !== wb * wb) throw new InvalidScientificInputError(\"Site kernels must be (2R + 1)² taps.\");\n const out = new Float64Array(width * width);\n // Non-zero taps of b, as (row, column, weight).\n const bRows: number[] = [], bColumns: number[] = [], bWeights: number[] = [];\n for (let j = 0; j < wb; j++) for (let i = 0; i < wb; i++) { const w = b[j * wb + i]!; if (w !== 0) { bRows.push(j); bColumns.push(i); bWeights.push(w); } }\n for (let sj = 0; sj < wa; sj++) for (let si = 0; si < wa; si++) {\n const w = a[sj * wa + si]!;\n if (w === 0) continue;\n for (let k = 0; k < bWeights.length; k++) {\n const index = (sj + bRows[k]!) * width + si + bColumns[k]!;\n out[index] = out[index]! + w * bWeights[k]!;\n }\n }\n return out;\n}\n"]}
@@ -1,6 +1,6 @@
1
1
  # Public API reference
2
2
 
3
- Generated from the exact root exports for **@photivra/engine 2.10.0 / root contract 2.10.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
3
+ Generated from the exact root exports for **@photivra/engine 2.11.1 / root contract 2.11.1** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
4
4
 
5
5
  Use [the developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) for executable paths and scientific boundaries. Types are contracts, not runtime validation: parse untrusted data, preserve units/reference frames, read assessment blockers, and retain evidence/limitations. Optional properties do not imply a universal default. Model/schema IDs remain independent of the distribution.
6
6
 
@@ -1102,6 +1102,24 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1102
1102
  | [`ExposureProgramLineProfile`](api/exposure-program-line.md#exposureprogramlineprofile) | Type |
1103
1103
  | [`parseExposureProgramLineProfile`](api/exposure-program-line.md#parseexposureprogramlineprofile) | Runtime |
1104
1104
 
1105
+ ## facade/chart-material-layers.ts
1106
+
1107
+ [Detailed contracts](api/facade-chart-material-layers.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/chart-material-layers.ts)
1108
+
1109
+ | Export | Kind |
1110
+ | --- | --- |
1111
+ | [`CHART_MASK_INDEX_GRAIN`](api/facade-chart-material-layers.md#chart_mask_index_grain) | Runtime |
1112
+ | [`CHART_MATERIAL_LAYER_LIMITS`](api/facade-chart-material-layers.md#chart_material_layer_limits) | Runtime |
1113
+ | [`ChartMaskAnnulus`](api/facade-chart-material-layers.md#chartmaskannulus) | Type |
1114
+ | [`ChartMaskLayer`](api/facade-chart-material-layers.md#chartmasklayer) | Type |
1115
+ | [`ChartMaskPolygon`](api/facade-chart-material-layers.md#chartmaskpolygon) | Type |
1116
+ | [`ChartMaskShape`](api/facade-chart-material-layers.md#chartmaskshape) | Type |
1117
+ | [`ChartMaterialLayerScene`](api/facade-chart-material-layers.md#chartmateriallayerscene) | Type |
1118
+ | [`resolveChartMaterialLayers`](api/facade-chart-material-layers.md#resolvechartmateriallayers) | Runtime |
1119
+ | [`ResolvedChartMaskLayer`](api/facade-chart-material-layers.md#resolvedchartmasklayer) | Type |
1120
+ | [`ResolvedChartMaskShape`](api/facade-chart-material-layers.md#resolvedchartmaskshape) | Type |
1121
+ | [`ResolvedChartMaterialLayers`](api/facade-chart-material-layers.md#resolvedchartmateriallayers) | Type |
1122
+
1105
1123
  ## facade/chart-radiance-batch.ts
1106
1124
 
1107
1125
  [Detailed contracts](api/facade-chart-radiance-batch.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/chart-radiance-batch.ts)
@@ -1157,6 +1175,19 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1157
1175
  | [`UniformChartScene`](api/facade-chart-scenes.md#uniformchartscene) | Type |
1158
1176
  | [`ZonePlateChartScene`](api/facade-chart-scenes.md#zoneplatechartscene) | Type |
1159
1177
 
1178
+ ## facade/display-view-gpu.ts
1179
+
1180
+ [Detailed contracts](api/facade-display-view-gpu.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/display-view-gpu.ts)
1181
+
1182
+ | Export | Kind |
1183
+ | --- | --- |
1184
+ | [`DisplayViewExecution`](api/facade-display-view-gpu.md#displayviewexecution) | Type |
1185
+ | [`DisplayViewFastReport`](api/facade-display-view-gpu.md#displayviewfastreport) | Type |
1186
+ | [`DisplayViewGpuConformance`](api/facade-display-view-gpu.md#displayviewgpuconformance) | Type |
1187
+ | [`DisplayViewGpuFallbackReason`](api/facade-display-view-gpu.md#displayviewgpufallbackreason) | Type |
1188
+ | [`DisplayViewGpuMode`](api/facade-display-view-gpu.md#displayviewgpumode) | Type |
1189
+ | [`PrepareDisplayViewOptions`](api/facade-display-view-gpu.md#preparedisplayviewoptions) | Type |
1190
+
1160
1191
  ## facade/display-view.ts
1161
1192
 
1162
1193
  [Detailed contracts](api/facade-display-view.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/display-view.ts)
@@ -1276,7 +1307,11 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1276
1307
 
1277
1308
  | Export | Kind |
1278
1309
  | --- | --- |
1310
+ | [`ChartCompositeProbeComponent`](api/gpu-image-kernel-execution.md#chartcompositeprobecomponent) | Type |
1311
+ | [`ChartCompositeProbeMismatch`](api/gpu-image-kernel-execution.md#chartcompositeprobemismatch) | Type |
1312
+ | [`ChartCompositeProbeRecord`](api/gpu-image-kernel-execution.md#chartcompositeproberecord) | Type |
1279
1313
  | [`composeRelativeErrorBounds`](api/gpu-image-kernel-execution.md#composerelativeerrorbounds) | Runtime |
1314
+ | [`ImageKernelAdapterDescription`](api/gpu-image-kernel-execution.md#imagekerneladapterdescription) | Type |
1280
1315
  | [`ImageKernelBackend`](api/gpu-image-kernel-execution.md#imagekernelbackend) | Type |
1281
1316
  | [`ImageKernelCodeErrorBound`](api/gpu-image-kernel-execution.md#imagekernelcodeerrorbound) | Type |
1282
1317
  | [`ImageKernelExecution`](api/gpu-image-kernel-execution.md#imagekernelexecution) | Type |
@@ -1288,6 +1323,15 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
1288
1323
  | [`WEBGPU_F32_TRANSFER_ABSOLUTE_BOUND`](api/gpu-image-kernel-execution.md#webgpu_f32_transfer_absolute_bound) | Runtime |
1289
1324
  | [`WEBGPU_F32_UNIT_ROUNDOFF`](api/gpu-image-kernel-execution.md#webgpu_f32_unit_roundoff) | Runtime |
1290
1325
 
1326
+ ## gpu/webgpu-switch.ts
1327
+
1328
+ [Detailed contracts](api/gpu-webgpu-switch.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/gpu/webgpu-switch.ts)
1329
+
1330
+ | Export | Kind |
1331
+ | --- | --- |
1332
+ | [`getWebGpuEnabled`](api/gpu-webgpu-switch.md#getwebgpuenabled) | Runtime |
1333
+ | [`setWebGpuEnabled`](api/gpu-webgpu-switch.md#setwebgpuenabled) | Runtime |
1334
+
1291
1335
  ## motion/camera-rotation.ts
1292
1336
 
1293
1337
  [Detailed contracts](api/motion-camera-rotation.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/motion/camera-rotation.ts)
@@ -72,6 +72,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
72
72
  | `lens.distortion` | none | `{ kind: "radial", profile }` or `{ kind: "brown-conrady", profile }`. |
73
73
  | `lens.vignetting` | none | An `IlluminationVignettingProfile` (radial relative illumination). |
74
74
  | `lens.psf` | none | A `{ kind: "sampled-local", configuration }` sampled lens PSF, passed to the environment contract unchanged. |
75
+ | `lens.defocus` | off | `{ model: "geometric-disc" }`: defocus of the scene plane as the geometric blur disc (see [Optics](#optics)). `simulateCapture` and `simulateDisplayView` render it. |
75
76
  | `lens.diffraction` | off | 2.1: `"circular-aperture"` or `{ kind: "circular-aperture", kernelRadiusSamples, fast }`: diffraction blur of an ideal lens at the resolved f-number (see [Optics](#optics)). `aperture` is accepted as a deprecated alias of `kind` until 3.0.0. |
76
77
  | `exposure.mode` | `"manual"` | Also `"aperture-priority"`, `"shutter-priority"`, `"program"`. |
77
78
  | `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). |
@@ -478,6 +479,11 @@ const view = await simulateDisplayView({
478
479
  console.log(view.diagnostics.sceneLookups);
479
480
  ```
480
481
 
482
+ The same scene can be declared as data instead of a function: `{ kind: "material-layers", sceneId, chart, layers }`, an
483
+ engine chart under shape-mask layers (polygons and annuli in the mask's own units; `resolveChartMaterialLayers`). Every
484
+ CPU result is that of its `callerScene`, byte for byte, and `prepareDisplayView(request, { gpu: "auto" })` can render it
485
+ on the GPU, which a caller function cannot (docs/DISPLAY_VIEW.md, "Rendering on the GPU").
486
+
481
487
  The display view's expected values match the reflectance-function form to rounding, because they are the same
482
488
  quantities summed in another order. Measured on the app's chart with a maker label (a 24 MP frame, the 2028 × 1352 view's
483
489
  centre strip, one thread), the material form took 1.15 s instead of 3.2 s with the defaults, and 3.0 s instead of 111 s
@@ -756,6 +762,97 @@ The optics options feed only what the environment site contract accepts:
756
762
  is sampled at its centre). It cannot be
757
763
  combined with `lens.psf` or `lens.pupilSampling`.
758
764
 
765
+ - `lens.defocus: { model: "geometric-disc" }` (#492; the capture renders it, the display view does not yet) models defocus of the flat scene plane when
766
+ `lens.focusDistanceM` differs from `subjectDistanceM`.
767
+ - The model is a geometric approximation for an ideal uniform circular pupil. Each in-focus image point spreads
768
+ into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor (`calculateDefocusCircle`, thin-lens
769
+ model 1.1.0), at the resolved f-number. There is no diffraction of the defocused wave, no aberrations and no
770
+ pupil vignetting. The disc is the same at every wavelength, so defocus of a plane is one convolution.
771
+ - The engine samples the disc on the site lattice. Each site's weight is the exact area of the disc inside that
772
+ site's square cell, a closed-form circle–rectangle area, divided by the disc's area.
773
+ - The weights are computed with the engine's portable arithmetic, so they are bit-identical in every
774
+ JavaScript engine.
775
+ - Their exact sum is 1, to the last bit of the centre weight.
776
+ - A disc smaller than one site gives the identity.
777
+ - A test blurs a sampled straight edge with the kernel and compares it with the disc's closed-form
778
+ edge-spread function, the circular-segment area. They agree to rounding (2e-13) when the edge lies on cell
779
+ boundaries. Elsewhere they agree within the site sampling of the edge.
780
+ - The kernel radius is capped at 64 sites, a 129 × 129 window (`DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES`). A larger
781
+ blur is refused with an error that names the cap and the radius needed; the disc is never clipped.
782
+ - **The capture renders it** (#492, step 2a). The scene radiance on the site lattice is convolved with the disc,
783
+ the same kernel at every spectral node, and each site query is answered with the result. This is the object-space
784
+ convolution that `lens.diffraction` uses.
785
+ - The capture keeps its tabulated fast path. Regions, strips and DNG parts stay byte-identical to the whole frame.
786
+ - The `lens.defocus` assumption records `execution: "object-space-convolution"`.
787
+ - `diagnostics.options` lists `lens.defocus`, and `diagnostics.timingsMs.defocus` times the provider's convolution.
788
+ - The site inputs declare no PSF, because the defocus is applied upstream.
789
+ - **With `lens.diffraction`**, each spectral node's diffraction kernel is convolved with the disc and applied as
790
+ one kernel. The combined radius is the diffraction radius plus the defocus radius, at most 32 + 64 = 96 sites.
791
+ - This is a further approximation, and the assumption says so. The defocused wave PSF of a circular pupil is not
792
+ exactly the in-focus Airy pattern convolved with the geometric disc. Where the two differ is measured in a
793
+ later step of #492.
794
+ - Building the combined kernels cost 0.5 ms per spectral node at diffraction radius 2 with defocus radius 41. It
795
+ cost 90 ms per node at 32 with 64, the largest pair (Node, one thread).
796
+ - **The display view renders it** (#492, step 2b) with the same per-node kernels: its expected-signal tables read
797
+ the scene on the site lattice through the disc (with `lens.diffraction`, through each node's diffraction kernel
798
+ convolved with the disc), so the view's estimators run unchanged on the blurred signal. The `view.defocus`
799
+ assumption records the kernel and its radii. The GPU display-view renderer does not take the kernel yet:
800
+ `gpu: "auto"` renders such a view on the CPU, labelled `unsupported-on-backend`. Validation and cost per radius:
801
+ [Display view, Defocus](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#defocus).
802
+ - In focus (c = 0) no kernel is applied, and the image is byte-identical to the same request without the option.
803
+ `simulateIdealReference`, which has no camera effects, only records the disc.
804
+ - **Refused out of focus** with camera rotation, `lens.projection` or `lens.distortion`, and `lens.vignetting`. Each
805
+ of these moves the scene points that a site sees off the site lattice, so the convolution does not apply. In
806
+ focus they render as without `lens.defocus`.
807
+ - **Sampling.** The scene enters at the lattice points the sites query: site centres, or the pixel-aperture nodes
808
+ and anti-aliasing spots. So near an edge the result can differ from the continuous disc integral by the lattice's
809
+ point sampling of the edge. Measured on a 16 × 12 slanted edge, as the relative difference in expected electrons
810
+ per site (max / RMS):
811
+
812
+ | Disc | Pixel aperture | Convolution vs continuous integral | 4 × 8 rays | 8 × 16 rays | 16 × 16 rays |
813
+ | --- | --- | --- | --- | --- | --- |
814
+ | 5.95 sites (R 3) | point | 0.112 / 0.026 | 0.089 / 0.021 | 0.023 / 0.0049 | 0.018 / 0.0035 |
815
+ | 5.95 sites (R 3) | 4 × 4 box | 0.035 / 0.0065 | 0.048 / 0.010 | 0.0067 / 0.0014 | 0.0044 / 0.0007 |
816
+ | 13.4 sites (R 7) | point | 0.030 / 0.0056 | 0.094 / 0.032 | 0.029 / 0.0077 | 0.023 / 0.0053 |
817
+ | 13.4 sites (R 7) | 4 × 4 box | 0.0051 / 0.0013 | 0.070 / 0.026 | 0.012 / 0.0034 | 0.0090 / 0.0021 |
818
+
819
+ The ray columns are `lens.pupilSampling` (the independent reference, without diffraction) against the same
820
+ continuous integral. They converge toward it as the rays increase. Against the convolution, the 13.4-site disc
821
+ with the box aperture gives 0.070, 0.012 and 0.0093 (max). In the test (the 5.95-site disc, point aperture), the rays
822
+ and the convolution agree to 1e-9 at every site more than one site beyond the disc's reach of the edge.
823
+ - **Validation** (`test/simulate-capture-defocus-render.test.ts`):
824
+ - A vertical edge halfway between two lattice columns gives the exact circular-segment fraction at every site,
825
+ to 1e-12.
826
+ - A 48 × 480 slanted-edge capture's averaged edge profile is compared with the disc's closed-form edge-spread
827
+ function. It uses RAW codes, green sites and quarter-site bins. The largest difference was 0.0093 of the
828
+ step (noise seed 1).
829
+ - The tabulated and environment paths give the same RAW.
830
+ - Regions and DNG parts equal the whole frame: chart, Quality options with diffraction, a `material-layers`
831
+ scene and a caller function.
832
+ - **Cost and the 64-site cap.** Direct convolution costs (2R + 1)² multiply-adds per site and node wherever the disc
833
+ sees more than one material. Uniform areas cost almost nothing on the tabulated path. Measured on the tabulated
834
+ capture path (Node 24, one thread, rounds interleaved over the radii, median [range] of 3 rounds; 2 at 2048 × 1366;
835
+ a shared 4-vCPU host):
836
+
837
+ | Scene, size | in focus | R 2 | R 8 | R 16 | R 32 | R 41 | R 64 |
838
+ | --- | --- | --- | --- | --- | --- | --- | --- |
839
+ | Siemens star, 1024 × 683 | 0.37 s | 0.37 s | 0.51 s | 0.98 s | 2.9 s | 4.7 s | 11.6 s [11.4–11.8] |
840
+ | Dead leaves, 1024 × 683 | 0.37 s | 0.38 s | 0.66 s | 1.6 s | 5.0 s | 7.9 s | 17.7 s [17.6–18.0] |
841
+ | Slanted edge, 1024 × 683 | 0.32 s | 0.33 s | 0.35 s | 0.40 s | 0.70 s | 1.05 s | 2.7 s |
842
+ | Siemens star, 1024 × 683, Quality (diffraction, four-spot, 2 × 2 box, AHD) | 0.81 s | 0.87 s | 1.7 s | 4.1 s | 12.5 s | 20.0 s | 52.9 s [52.0–53.2] |
843
+ | Siemens star, 2048 × 1366 | 1.39 s | | 1.8 s | | 10.4 s | | 38.3 s [37.5–38.3] |
844
+
845
+ - The cap stays at 64 sites, set from these numbers. It covers the 1.4 µm example of #492 (50 mm f/2.8, subject
846
+ 2 m, focus 1.6 m: R = 41) with room for larger blurs.
847
+ - The time grows with R² per edge site, and the caller chooses it through the focus distance. Past the cap the
848
+ request fails and names the radius it needed.
849
+ - A 24 MP frame costs proportionally more. Render it in parts across workers, as for diffraction.
850
+ - Off the tabulated path (caller reflectance or radiance functions), the provider gathers only each 32-row band's
851
+ own values, not the 2R extra rows `calculateIrradianceConvolution` would return (#419). On a 256 × 171
852
+ function scene the convolution took 0.3 s at R 8, 3.5 s at R 32 and 12.9 s at R 64.
853
+ - It cannot be combined with `lens.pupilSampling` (the rays already render defocus) or `lens.psf` (a caller PSF
854
+ declares its own defocus). It can be combined with `lens.diffraction`.
855
+
759
856
  ### Pixel aperture (2.3)
760
857
 
761
858
  By default each site samples the irradiance at its centre (`sensor.pixelAperture: "point"`). Scene
@@ -1345,14 +1442,16 @@ console.log(tile.image.width, tile.diagnostics.timingsMs.view);
1345
1442
  ```
1346
1443
 
1347
1444
  `render(view)` returns what `simulateDisplayView(request, view)` returns, byte for byte (only `diagnostics.timingsMs`
1348
- differ, and they cover the render only). Renders may run concurrently and share only read-only setup. The request's own
1445
+ differ, and they cover the render only). A second argument `{ gpu: "auto" | "require", gpuDevice }` (2.11, #481) asks for
1446
+ the WebGPU renderer, with the CPU reference as the labelled fallback or as a failure; `diagnostics.fast.execution` names
1447
+ what ran (see [Rendering on the GPU](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#rendering-on-the-gpu)). Renders may run concurrently and share only read-only setup. The request's own
1349
1448
  `signal` applies to preparing; give each render its own. Keep one prepared view per request in each worker: measured on
1350
1449
  a 24 MP request, an 8 × 8 region took 6–40 ms through a prepared view instead of 0.24–0.41 s as its own call (Speed).
1351
1450
 
1352
1451
  **Not supported** (refused with an `InvalidConfigurationError`): `lens.pupilSampling`, `lens.projection`,
1353
1452
  `lens.distortion`, `lens.vignetting`, `lens.psf`, camera rotation, `output.whiteBalance: "auto"`, the luminance tone
1354
- curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `onProgress` is not called;
1355
- `signal` cancels.
1453
+ curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `lens.defocus` is rendered as a
1454
+ site kernel in the expected signal (#492; see [Optics](#optics)). `onProgress` is not called; `signal` cancels.
1356
1455
 
1357
1456
  The result is labelled `kind: "display-view"` with a `label` for the user interface; it has no RAW, plan or file, and
1358
1457
  the `view.*` assumptions state the definition, the estimator, the noise model, the demosaic treatment and which capture
package/docs/USAGE.md CHANGED
@@ -1552,6 +1552,8 @@ console.log(components.value.composition.status); // "not-composed"
1552
1552
 
1553
1553
  The foundation deliberately **does not combine** geometric defocus and circular diffraction into a synthetic PSF, MTF, convolution kernel, combined blur radius or "lens sharpness" score.
1554
1554
 
1555
+ This foundation API is separate from the capture facade. With `lens.defocus` and `lens.diffraction` together, `simulateCapture` convolves each spectral node's diffraction kernel with the geometric defocus disc (#492). That is a labelled approximation, and it has its own `lens.defocus` assumption. See [Optics](QUICK_START.md#optics).
1556
+
1555
1557
  It already records field position, even though the current defocus and Airy diagnostics do not vary across the field. Future field-curvature, pupil-clipping, bokeh and aberration contributions can then use the same context without reinterpreting existing outputs.
1556
1558
 
1557
1559
  The spectral basis is explicitly monochromatic (one wavelength) for the Airy diagnostic. This does not create a spectral lens or sensor-colour model.
@@ -7300,18 +7302,19 @@ console.log(merged.raw.codes.length, merged.providerEvaluationCount);
7300
7302
  - By default they run on the float64 CPU reference.
7301
7303
  - If you opt in, they run on the `webgpu-f32` backend (WebGPU, 32-bit floats).
7302
7304
  - Every result's `execution` label names the backend that actually ran, any reason for falling back, and the error bound against the reference, stated in advance.
7305
+ - One boolean chooses: `{ webgpu: true }` asks for the GPU and runs the reference if it is missing; `{ webgpu: false }` is the reference. `setWebGpuEnabled(false)` turns the GPU off for the whole realm, whatever callers ask, and labels those results `webgpu-disabled`.
7303
7306
 
7304
7307
  See [WebGPU backend](https://github.com/Photivra/photivra/blob/main/docs/WEBGPU.md) for the bounds, the supported domain and the browser test.
7305
7308
 
7306
7309
  ```ts
7307
- import { createImageKernelAccelerator, calculateIrradianceConvolution } from "@photivra/engine";
7310
+ import { createImageKernelAccelerator, calculateIrradianceConvolution, getWebGpuEnabled, setWebGpuEnabled } from "@photivra/engine";
7308
7311
 
7309
7312
  const width = 16, height = 8, kernelRadius = 1;
7310
7313
  const input = { schemaVersion: "0.1.0" as const, width, height, kernelRadius, patchWidth: width, patchHeight: height,
7311
7314
  boundary: "zero-outside-grid" as const, samples: new Float64Array(width * height).fill(2),
7312
7315
  kernels: Float64Array.from([0, .1, 0, .1, .6, .1, 0, .1, 0]) };
7313
7316
 
7314
- const accelerator = await createImageKernelAccelerator({ backend: "webgpu-f32", fallback: "cpu-reference" });
7317
+ const accelerator = await createImageKernelAccelerator({ webgpu: true }); // the same as { backend: "webgpu-f32", fallback: "cpu-reference" }
7315
7318
  const result = await accelerator.convolveIrradiance(input);
7316
7319
  // Without WebGPU (e.g. Node) this ran the reference and says so.
7317
7320
  console.log(result.execution.backend, result.execution.fallbackReason, result.execution.bitIdenticalToReference);
@@ -7319,6 +7322,13 @@ console.log(result.execution.backend, result.execution.fallbackReason, result.ex
7319
7322
  const reference = calculateIrradianceConvolution(input).values;
7320
7323
  console.log(result.execution.backend === "cpu-reference" ? result.values.every((v, i) => v === reference[i]) : result.execution.errorBound);
7321
7324
  accelerator.dispose();
7325
+
7326
+ // The realm's switch: off, every request for the GPU runs the reference and says why.
7327
+ setWebGpuEnabled(false);
7328
+ const cpuOnly = await createImageKernelAccelerator({ webgpu: true });
7329
+ console.log(cpuOnly.backend, cpuOnly.fallbackReason, getWebGpuEnabled()); // "cpu-reference" "webgpu-disabled" false
7330
+ cpuOnly.dispose();
7331
+ setWebGpuEnabled(true);
7322
7332
  ```
7323
7333
 
7324
7334
  Two image-formation stages use the same accelerator:
@@ -7516,6 +7526,27 @@ const view = await simulateDisplayView({ scene: { preset: "slanted-edge" }, sens
7516
7526
  console.log(view.kind, view.view.scaleX, view.assumptions.find((a) => a.id === "view.definition")?.value);
7517
7527
  ```
7518
7528
 
7529
+ #### Rendering a prepared view on the GPU (2.11, #481)
7530
+
7531
+ `prepareDisplayView(request, { gpu })` chooses the renderer: `"off"` (the default) is the CPU reference, `"auto"` the GPU
7532
+ when a conforming device is available and the request is supported (else the CPU, labelled), `"require"` fails instead of
7533
+ falling back. `gpuDevice` hands the view a worker's `navigator.gpu` or an `ImageKernelAccelerator` whose device it shares.
7534
+ Every render says which path ran in `diagnostics.fast.execution`, in the accelerator's words. No GPU display-view renderer
7535
+ exists yet (#478), so today `"auto"` runs the CPU reference labelled `unsupported-on-backend` without requesting a device;
7536
+ see [Rendering on the GPU](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#rendering-on-the-gpu).
7537
+
7538
+ ```ts
7539
+ import { prepareDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
7540
+
7541
+ const request: SimulateCaptureOptions = { scene: { preset: "siemens-star" }, sensor: { pixelPitchMicrometers: 4 }, output: { width: 600, height: 400 } };
7542
+ const prepared = await prepareDisplayView(request, { gpu: "auto" }); // acquisition failures fall back; with "require" they throw here
7543
+ const tile = await prepared.render({ width: 300, height: 200, region: { x: 120, y: 80, width: 32, height: 32 } });
7544
+ const ran = tile.diagnostics.fast.execution;
7545
+ // "cpu-reference" when the reference ran (and why), "webgpu-f32" with its bound and adapter when the GPU did.
7546
+ console.log(ran.backend, ran.fallbackReason, ran.assurance, ran.errorBound, ran.adapter, tile.diagnostics.fast.assurance);
7547
+ prepared.dispose(); // releases a device the view acquired; a shared accelerator's device stays its owner's
7548
+ ```
7549
+
7519
7550
  #### Sharing AHD correction states between workers
7520
7551
 
7521
7552
  With `output.demosaic: "ahd"`, low-signal display pixels read correction states that each realm (a page, a Web Worker)
@@ -7592,6 +7623,25 @@ const barrel = await simulateCapture({
7592
7623
  console.log(barrel.request.siteInput(0).environment.lensFieldMapping?.distortion.kind);
7593
7624
  ```
7594
7625
 
7626
+ `lens.defocus: { model: "geometric-disc" }` (#492) renders defocus of the flat scene plane when
7627
+ `lens.focusDistanceM` differs from `subjectDistanceM`. The thin-lens blur circle becomes a uniform disc on the site
7628
+ lattice, and the capture convolves the scene radiance with it, which is a geometric approximation. With
7629
+ `lens.diffraction`, each spectral node's diffraction kernel is convolved with the disc, which is a further
7630
+ approximation. The `lens.defocus` assumption records the diameter in µm and in sites, the kernel radius and the
7631
+ execution. `simulateDisplayView` does not render it yet. See [Optics](QUICK_START.md#optics).
7632
+
7633
+ ```ts
7634
+ import { simulateCapture } from "@photivra/engine";
7635
+
7636
+ const blurred = await simulateCapture({
7637
+ scene: { preset: "slanted-edge" },
7638
+ subjectDistanceM: 2,
7639
+ lens: { focalLengthMm: 50, fNumber: 4, focusDistanceM: 1.8, defocus: { model: "geometric-disc" } },
7640
+ output: { width: 16, height: 12 }
7641
+ });
7642
+ console.log(blurred.assumptions.find((a) => a.id === "lens.defocus")?.value); // diameterSites ≈ 5.95, kernelRadiusSites 3
7643
+ ```
7644
+
7595
7645
  `motion.angularVelocityRadPerSec` rotates the camera during the exposure (8 time midpoints by
7596
7646
  default). `motion.shutter: "rolling"` derives each site's exposure window from the body's rolling
7597
7647
  readout, with `resolveElectronicRollingShutterExposureSchedule`. See [Motion](QUICK_START.md#motion).
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.10.0" as const
14
+ ENGINE_API_VERSION = "2.11.1" as const
15
15
  ```