@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.
- package/CHANGELOG.md +139 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +21 -3
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/core/portable-math.d.ts +6 -0
- package/dist/core/portable-math.js +39 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/chart-material-layers.d.ts +115 -0
- package/dist/facade/chart-material-layers.js +224 -0
- package/dist/facade/chart-material-layers.js.map +1 -0
- package/dist/facade/chart-radiance-batch.d.ts +8 -2
- package/dist/facade/chart-radiance-batch.js +11 -4
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +134 -0
- package/dist/facade/chart-scenes.js +32 -22
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +8 -2
- package/dist/facade/diffraction-provider.js +71 -2
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.d.ts +18 -0
- package/dist/facade/display-view-gpu-session.js +280 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -0
- package/dist/facade/display-view-gpu.d.ts +146 -0
- package/dist/facade/display-view-gpu.js +59 -0
- package/dist/facade/display-view-gpu.js.map +1 -0
- package/dist/facade/display-view.d.ts +48 -1
- package/dist/facade/display-view.js +7 -7
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +5 -0
- package/dist/facade/expected-signal-setup.js +16 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +48 -3
- package/dist/facade/expected-signal.js +8 -0
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -2
- package/dist/facade/scene-radiance.js +7 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +32 -5
- package/dist/facade/simulate-capture.js +148 -24
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +31 -3
- package/dist/facade/simulate-display-view.js +72 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +119 -0
- package/dist/gpu/chart-composite-gpu.js +572 -0
- package/dist/gpu/chart-composite-gpu.js.map +1 -0
- package/dist/gpu/chart-radiance-gpu.d.ts +6 -6
- package/dist/gpu/chart-radiance-gpu.js +9 -11
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-chart-material-module.d.ts +10 -0
- package/dist/gpu/display-view-chart-material-module.js +58 -0
- package/dist/gpu/display-view-chart-material-module.js.map +1 -0
- package/dist/gpu/display-view-composite-material-module.d.ts +68 -0
- package/dist/gpu/display-view-composite-material-module.js +248 -0
- package/dist/gpu/display-view-composite-material-module.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +185 -0
- package/dist/gpu/display-view-quality-gpu.js +1000 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -0
- package/dist/gpu/display-view-speed-gpu.d.ts +156 -0
- package/dist/gpu/display-view-speed-gpu.js +524 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -0
- package/dist/gpu/image-kernel-accelerator.d.ts +23 -4
- package/dist/gpu/image-kernel-accelerator.js +58 -10
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +55 -3
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.d.ts +11 -0
- package/dist/gpu/sensor-stage-gpu.js +42 -13
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +4 -2
- package/dist/gpu/webgpu-runtime.js +13 -4
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/webgpu-switch.d.ts +4 -0
- package/dist/gpu/webgpu-switch.js +29 -0
- package/dist/gpu/webgpu-switch.js.map +1 -0
- package/dist/gpu/wgsl-chart-composite-kernel.d.ts +52 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js +515 -0
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -0
- package/dist/gpu/wgsl-chart-kernel.d.ts +11 -9
- package/dist/gpu/wgsl-chart-kernel.js +33 -27
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +95 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +769 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +75 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +386 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -0
- package/dist/gpu/wgsl-sensor-kernel.d.ts +28 -9
- package/dist/gpu/wgsl-sensor-kernel.js +94 -50
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +61 -0
- package/dist/optics/defocus-disc-kernel.js +191 -0
- package/dist/optics/defocus-disc-kernel.js.map +1 -0
- package/docs/API_REFERENCE.md +45 -1
- package/docs/QUICK_START.md +102 -3
- package/docs/USAGE.md +52 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +117 -2
- package/docs/api/facade-chart-material-layers.md +137 -0
- package/docs/api/facade-chart-radiance-batch.md +7 -2
- package/docs/api/facade-display-view-gpu.md +94 -0
- package/docs/api/facade-scene-radiance.md +3 -2
- package/docs/api/facade-simulate-capture.md +23 -6
- package/docs/api/facade-simulate-display-view.md +17 -2
- package/docs/api/gpu-image-kernel-accelerator.md +10 -3
- package/docs/api/gpu-image-kernel-execution.md +71 -4
- package/docs/api/gpu-webgpu-switch.md +23 -0
- 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"]}
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Public API reference
|
|
2
2
|
|
|
3
|
-
Generated from the exact root exports for **@photivra/engine 2.
|
|
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)
|
package/docs/QUICK_START.md
CHANGED
|
@@ -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).
|
|
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`). `
|
|
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).
|
package/docs/api/core-version.md
CHANGED
|
@@ -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.
|
|
14
|
+
ENGINE_API_VERSION = "2.11.1" as const
|
|
15
15
|
```
|