@photivra/engine 2.10.0 → 2.12.0
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 +156 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +19 -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 +108 -8
- package/dist/facade/simulate-capture.js +226 -33
- 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 +73 -13
- 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 +5 -2
- 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 +47 -1
- package/docs/QUICK_START.md +150 -5
- package/docs/USAGE.md +81 -3
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +127 -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 +95 -11
- 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.12.0 / root contract 2.12.0** 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)
|
|
@@ -1206,6 +1237,8 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1206
1237
|
| [`SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS`](api/facade-simulate-capture.md#simulate_display_view_default_samples_per_axis) | Runtime |
|
|
1207
1238
|
| [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) | Runtime |
|
|
1208
1239
|
| [`SimulateCaptureAssumption`](api/facade-simulate-capture.md#simulatecaptureassumption) | Type |
|
|
1240
|
+
| [`SimulateCaptureAutoIsoAfterMaximum`](api/facade-simulate-capture.md#simulatecaptureautoisoaftermaximum) | Type |
|
|
1241
|
+
| [`SimulateCaptureAutoIsoReport`](api/facade-simulate-capture.md#simulatecaptureautoisoreport) | Type |
|
|
1209
1242
|
| [`SimulateCaptureBorderHandling`](api/facade-simulate-capture.md#simulatecaptureborderhandling) | Type |
|
|
1210
1243
|
| [`SimulateCaptureColorEncoding`](api/facade-simulate-capture.md#simulatecapturecolorencoding) | Type |
|
|
1211
1244
|
| [`SimulateCaptureCostOption`](api/facade-simulate-capture.md#simulatecapturecostoption) | Type |
|
|
@@ -1276,7 +1309,11 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1276
1309
|
|
|
1277
1310
|
| Export | Kind |
|
|
1278
1311
|
| --- | --- |
|
|
1312
|
+
| [`ChartCompositeProbeComponent`](api/gpu-image-kernel-execution.md#chartcompositeprobecomponent) | Type |
|
|
1313
|
+
| [`ChartCompositeProbeMismatch`](api/gpu-image-kernel-execution.md#chartcompositeprobemismatch) | Type |
|
|
1314
|
+
| [`ChartCompositeProbeRecord`](api/gpu-image-kernel-execution.md#chartcompositeproberecord) | Type |
|
|
1279
1315
|
| [`composeRelativeErrorBounds`](api/gpu-image-kernel-execution.md#composerelativeerrorbounds) | Runtime |
|
|
1316
|
+
| [`ImageKernelAdapterDescription`](api/gpu-image-kernel-execution.md#imagekerneladapterdescription) | Type |
|
|
1280
1317
|
| [`ImageKernelBackend`](api/gpu-image-kernel-execution.md#imagekernelbackend) | Type |
|
|
1281
1318
|
| [`ImageKernelCodeErrorBound`](api/gpu-image-kernel-execution.md#imagekernelcodeerrorbound) | Type |
|
|
1282
1319
|
| [`ImageKernelExecution`](api/gpu-image-kernel-execution.md#imagekernelexecution) | Type |
|
|
@@ -1288,6 +1325,15 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1288
1325
|
| [`WEBGPU_F32_TRANSFER_ABSOLUTE_BOUND`](api/gpu-image-kernel-execution.md#webgpu_f32_transfer_absolute_bound) | Runtime |
|
|
1289
1326
|
| [`WEBGPU_F32_UNIT_ROUNDOFF`](api/gpu-image-kernel-execution.md#webgpu_f32_unit_roundoff) | Runtime |
|
|
1290
1327
|
|
|
1328
|
+
## gpu/webgpu-switch.ts
|
|
1329
|
+
|
|
1330
|
+
[Detailed contracts](api/gpu-webgpu-switch.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/gpu/webgpu-switch.ts)
|
|
1331
|
+
|
|
1332
|
+
| Export | Kind |
|
|
1333
|
+
| --- | --- |
|
|
1334
|
+
| [`getWebGpuEnabled`](api/gpu-webgpu-switch.md#getwebgpuenabled) | Runtime |
|
|
1335
|
+
| [`setWebGpuEnabled`](api/gpu-webgpu-switch.md#setwebgpuenabled) | Runtime |
|
|
1336
|
+
|
|
1291
1337
|
## motion/camera-rotation.ts
|
|
1292
1338
|
|
|
1293
1339
|
[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
|
@@ -48,7 +48,7 @@ the example above is [simulate-capture.mjs](examples/simulate-capture.mjs).
|
|
|
48
48
|
| `raw` | The packed native RAW: codes, black levels, saturation flags and per-phase white levels |
|
|
49
49
|
| `plan` | The native RAW plan the engine executed |
|
|
50
50
|
| `request` | The complete underlying request: `environmentRaw` (the native environment RAW input), `siteInput(index)` (rebuilds the physical input of any sensor site), `development` (the RAW development input) and `whiteBalanceState` |
|
|
51
|
-
| `exposure` | Mode, resolved settings, EV100, metered scene luminance and the exposure-mode resolver's full output |
|
|
51
|
+
| `exposure` | Mode, resolved settings, EV100, `isoControl` (`"manual"` or `"automatic"`) and, with `iso: "auto"`, `autoIso` (how the ISO was chosen and what limited it), metered scene luminance, the deviation from the standard exposure in stops (`standardExposureDeviationStops`) and the exposure-mode resolver's full output |
|
|
52
52
|
| `scene` | Chart parameters, illuminant, distance, framed size and the scene's SHA-256 digest (it covers caller illuminant and reflectance spectra; see `sourceSha256`) |
|
|
53
53
|
| `diagnostics` | Radiance query count, highlight and rendering counters, per-stage timings (`timingsMs`) and, from 2.1, the opt-ins that ran (`options`); see [Cost of each option](#cost-of-each-option) |
|
|
54
54
|
| `provenance` | Status (`approximation`), camera preset identity, evidence, the composed contracts in order, and limitations |
|
|
@@ -72,9 +72,11 @@ 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
|
-
| `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). |
|
|
78
|
+
| `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). `"auto"` lets Auto ISO choose it from the meter in any mode (see [Auto ISO](#auto-iso-516)). |
|
|
79
|
+
| `exposure.autoIso` | none | `aperture-priority` with `iso: "auto"` only, and required there: `{ slowestPreferredShutterSeconds, afterMaximumIso? }` (see [Auto ISO](#auto-iso-516)). |
|
|
78
80
|
| `exposure.shutterSeconds` | 1/125 | Used in `manual` and `shutter-priority`. |
|
|
79
81
|
| `exposure.compensationStops` | 0 | Applied to the meter target. |
|
|
80
82
|
| `scene` | required | A chart preset or a caller function (below). |
|
|
@@ -358,6 +360,14 @@ The facade meters the scene like a reflected-light meter:
|
|
|
358
360
|
10·64·125/(0.65·100) = 80 000/65 ≈ 1230.77 cd/m².
|
|
359
361
|
- The meter target then goes through the engine's existing exposure-mode resolver.
|
|
360
362
|
- In `program` mode, a generic program line written by the engine chooses the aperture and shutter.
|
|
363
|
+
- `exposure.standardExposureDeviationStops` (#515) says how far the exposure used is from the standard exposure for
|
|
364
|
+
the metered luminance L, in stops, from the same two relations:
|
|
365
|
+
Δ = log₂(N²/t) − log₂(0.65·L·S/10) = `ev100` − log₂(0.65·L·S/10). Positive is darker than the standard exposure
|
|
366
|
+
(under), negative brighter (over). With the defaults it is 0; the marked f/11 gives log₂(11²/8²) ≈ +0.92, the marked
|
|
367
|
+
1/60 s gives log₂(60/125) ≈ −1.06 and ISO 200 gives −1. In an automatic mode it is that of the exposure the engine
|
|
368
|
+
used: minus the compensation (−⅔ for +⅔ stop), plus any step or limit the resolver hit (the slowest shutter, the ISO
|
|
369
|
+
range). It is `null` only when the metered luminance is 0, which has no standard exposure. The display view reports
|
|
370
|
+
the same value.
|
|
361
371
|
|
|
362
372
|
DC-004 ties the reference level to an 18 % grey (Explanation 1 and 2.2). The default illuminance
|
|
363
373
|
puts an 18 % Lambertian grey card, whose luminance is L = 0.18·E/π, exactly at that luminance:
|
|
@@ -380,6 +390,43 @@ reflector would then sit at about 73 % of white, roughly half a stop below clipp
|
|
|
380
390
|
only the mid-grey reference level, not highlight headroom: this half stop is a property of the
|
|
381
391
|
engine's generic tier presets, documented here, and the tier values were not tuned to get it.
|
|
382
392
|
|
|
393
|
+
### Auto ISO (#516)
|
|
394
|
+
|
|
395
|
+
`exposure.iso: "auto"` hands the ISO to the exposure-mode resolver's Auto ISO branch for the mode. The ISO starts
|
|
396
|
+
at the lowest selectable ISO of the body's Auto ISO range (the tiers: ISO 100 to their maximum, 6400, 12 800 or
|
|
397
|
+
25 600), snaps to the nearest selectable ISO in stops (the lower on a tie) and is held at the ends of the range.
|
|
398
|
+
Exposure compensation applies to the meter target first, as with a numeric ISO.
|
|
399
|
+
|
|
400
|
+
| Mode | With `iso: "auto"` |
|
|
401
|
+
| --- | --- |
|
|
402
|
+
| `manual` | Your aperture and shutter; the ISO makes up the exposure (`resolveManualExposureMode`, automatic ISO). |
|
|
403
|
+
| `aperture-priority` | Your aperture. The shutter follows the meter at the lowest ISO, but no slower than `exposure.autoIso.slowestPreferredShutterSeconds` (required; seconds within the body's shutter range); the ISO makes up the rest. At the Auto ISO maximum, `afterMaximumIso` decides: `"allow-slower-shutter"` (the default) lengthens the shutter to reach the target, `"hold-preferred-shutter"` keeps it and leaves the exposure under target (`resolveAperturePriorityAutoIsoExposureMode`). The engine does not choose a slowest shutter for you. |
|
|
404
|
+
| `shutter-priority` | Your shutter. The aperture is chosen at the lowest ISO, no wider than the target needs, so it stops at the widest aperture; the ISO makes up the rest (`resolveShutterPriorityExposureMode`, policy `minimum-iso-aperture-first`). |
|
|
405
|
+
| `program` | The generic program line at the lowest ISO; beyond the line's end (f/4, 1.024 s) the ISO rises (`resolveProgramAutoExposureMode`, automatic ISO). |
|
|
406
|
+
|
|
407
|
+
The result says what happened. `exposure.settings.iso` is the ISO used and `exposure.isoControl` is `"automatic"`.
|
|
408
|
+
`exposure.autoIso` reports `idealIsoBeforeConstraints`, the Auto ISO range (`autoIsoMinimum`, `autoIsoMaximum`),
|
|
409
|
+
`clamped`, `limitingConstraint` and `residualStops`. The last two are the resolver's: for example `"iso-maximum"` when
|
|
410
|
+
the exposure stays under target at the maximum ISO, `"iso-grid-quantization"` for the nearest selectable ISO, or
|
|
411
|
+
`"none"`. A positive residual is under target. In aperture priority the report also repeats `afterMaximumIso` and
|
|
412
|
+
`slowestPreferredShutterSeconds`, and says whether the slower-shutter fallback ran (`afterMaximumIsoFallbackUsed`).
|
|
413
|
+
A display view, an ideal reference and every `output.region` part of the same request meter the whole frame, so
|
|
414
|
+
they resolve the same exposure and report the same values. Requests without `"auto"` are unchanged: their
|
|
415
|
+
`isoControl` is `"manual"` and they have no `autoIso`.
|
|
416
|
+
|
|
417
|
+
```ts
|
|
418
|
+
import { simulateCapture } from "@photivra/engine";
|
|
419
|
+
|
|
420
|
+
const dusk = await simulateCapture({
|
|
421
|
+
exposure: { mode: "aperture-priority", iso: "auto", autoIso: { slowestPreferredShutterSeconds: 1 / 60, afterMaximumIso: "hold-preferred-shutter" } },
|
|
422
|
+
lens: { fNumber: 8 },
|
|
423
|
+
scene: { preset: "uniform" },
|
|
424
|
+
illuminanceLux: 200,
|
|
425
|
+
output: { width: 8, height: 8 }
|
|
426
|
+
});
|
|
427
|
+
console.log(dusk.exposure.settings, dusk.exposure.isoControl, dusk.exposure.autoIso?.limitingConstraint);
|
|
428
|
+
```
|
|
429
|
+
|
|
383
430
|
## Scene presets
|
|
384
431
|
|
|
385
432
|
All charts are analytic patterns written by the engine; no third-party chart data is bundled. Each
|
|
@@ -478,6 +525,11 @@ const view = await simulateDisplayView({
|
|
|
478
525
|
console.log(view.diagnostics.sceneLookups);
|
|
479
526
|
```
|
|
480
527
|
|
|
528
|
+
The same scene can be declared as data instead of a function: `{ kind: "material-layers", sceneId, chart, layers }`, an
|
|
529
|
+
engine chart under shape-mask layers (polygons and annuli in the mask's own units; `resolveChartMaterialLayers`). Every
|
|
530
|
+
CPU result is that of its `callerScene`, byte for byte, and `prepareDisplayView(request, { gpu: "auto" })` can render it
|
|
531
|
+
on the GPU, which a caller function cannot (docs/DISPLAY_VIEW.md, "Rendering on the GPU").
|
|
532
|
+
|
|
481
533
|
The display view's expected values match the reflectance-function form to rounding, because they are the same
|
|
482
534
|
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
535
|
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 +808,97 @@ The optics options feed only what the environment site contract accepts:
|
|
|
756
808
|
is sampled at its centre). It cannot be
|
|
757
809
|
combined with `lens.psf` or `lens.pupilSampling`.
|
|
758
810
|
|
|
811
|
+
- `lens.defocus: { model: "geometric-disc" }` (#492; the capture renders it, the display view does not yet) models defocus of the flat scene plane when
|
|
812
|
+
`lens.focusDistanceM` differs from `subjectDistanceM`.
|
|
813
|
+
- The model is a geometric approximation for an ideal uniform circular pupil. Each in-focus image point spreads
|
|
814
|
+
into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor (`calculateDefocusCircle`, thin-lens
|
|
815
|
+
model 1.1.0), at the resolved f-number. There is no diffraction of the defocused wave, no aberrations and no
|
|
816
|
+
pupil vignetting. The disc is the same at every wavelength, so defocus of a plane is one convolution.
|
|
817
|
+
- The engine samples the disc on the site lattice. Each site's weight is the exact area of the disc inside that
|
|
818
|
+
site's square cell, a closed-form circle–rectangle area, divided by the disc's area.
|
|
819
|
+
- The weights are computed with the engine's portable arithmetic, so they are bit-identical in every
|
|
820
|
+
JavaScript engine.
|
|
821
|
+
- Their exact sum is 1, to the last bit of the centre weight.
|
|
822
|
+
- A disc smaller than one site gives the identity.
|
|
823
|
+
- A test blurs a sampled straight edge with the kernel and compares it with the disc's closed-form
|
|
824
|
+
edge-spread function, the circular-segment area. They agree to rounding (2e-13) when the edge lies on cell
|
|
825
|
+
boundaries. Elsewhere they agree within the site sampling of the edge.
|
|
826
|
+
- The kernel radius is capped at 64 sites, a 129 × 129 window (`DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES`). A larger
|
|
827
|
+
blur is refused with an error that names the cap and the radius needed; the disc is never clipped.
|
|
828
|
+
- **The capture renders it** (#492, step 2a). The scene radiance on the site lattice is convolved with the disc,
|
|
829
|
+
the same kernel at every spectral node, and each site query is answered with the result. This is the object-space
|
|
830
|
+
convolution that `lens.diffraction` uses.
|
|
831
|
+
- The capture keeps its tabulated fast path. Regions, strips and DNG parts stay byte-identical to the whole frame.
|
|
832
|
+
- The `lens.defocus` assumption records `execution: "object-space-convolution"`.
|
|
833
|
+
- `diagnostics.options` lists `lens.defocus`, and `diagnostics.timingsMs.defocus` times the provider's convolution.
|
|
834
|
+
- The site inputs declare no PSF, because the defocus is applied upstream.
|
|
835
|
+
- **With `lens.diffraction`**, each spectral node's diffraction kernel is convolved with the disc and applied as
|
|
836
|
+
one kernel. The combined radius is the diffraction radius plus the defocus radius, at most 32 + 64 = 96 sites.
|
|
837
|
+
- This is a further approximation, and the assumption says so. The defocused wave PSF of a circular pupil is not
|
|
838
|
+
exactly the in-focus Airy pattern convolved with the geometric disc. Where the two differ is measured in a
|
|
839
|
+
later step of #492.
|
|
840
|
+
- Building the combined kernels cost 0.5 ms per spectral node at diffraction radius 2 with defocus radius 41. It
|
|
841
|
+
cost 90 ms per node at 32 with 64, the largest pair (Node, one thread).
|
|
842
|
+
- **The display view renders it** (#492, step 2b) with the same per-node kernels: its expected-signal tables read
|
|
843
|
+
the scene on the site lattice through the disc (with `lens.diffraction`, through each node's diffraction kernel
|
|
844
|
+
convolved with the disc), so the view's estimators run unchanged on the blurred signal. The `view.defocus`
|
|
845
|
+
assumption records the kernel and its radii. The GPU display-view renderer does not take the kernel yet:
|
|
846
|
+
`gpu: "auto"` renders such a view on the CPU, labelled `unsupported-on-backend`. Validation and cost per radius:
|
|
847
|
+
[Display view, Defocus](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#defocus).
|
|
848
|
+
- In focus (c = 0) no kernel is applied, and the image is byte-identical to the same request without the option.
|
|
849
|
+
`simulateIdealReference`, which has no camera effects, only records the disc.
|
|
850
|
+
- **Refused out of focus** with camera rotation, `lens.projection` or `lens.distortion`, and `lens.vignetting`. Each
|
|
851
|
+
of these moves the scene points that a site sees off the site lattice, so the convolution does not apply. In
|
|
852
|
+
focus they render as without `lens.defocus`.
|
|
853
|
+
- **Sampling.** The scene enters at the lattice points the sites query: site centres, or the pixel-aperture nodes
|
|
854
|
+
and anti-aliasing spots. So near an edge the result can differ from the continuous disc integral by the lattice's
|
|
855
|
+
point sampling of the edge. Measured on a 16 × 12 slanted edge, as the relative difference in expected electrons
|
|
856
|
+
per site (max / RMS):
|
|
857
|
+
|
|
858
|
+
| Disc | Pixel aperture | Convolution vs continuous integral | 4 × 8 rays | 8 × 16 rays | 16 × 16 rays |
|
|
859
|
+
| --- | --- | --- | --- | --- | --- |
|
|
860
|
+
| 5.95 sites (R 3) | point | 0.112 / 0.026 | 0.089 / 0.021 | 0.023 / 0.0049 | 0.018 / 0.0035 |
|
|
861
|
+
| 5.95 sites (R 3) | 4 × 4 box | 0.035 / 0.0065 | 0.048 / 0.010 | 0.0067 / 0.0014 | 0.0044 / 0.0007 |
|
|
862
|
+
| 13.4 sites (R 7) | point | 0.030 / 0.0056 | 0.094 / 0.032 | 0.029 / 0.0077 | 0.023 / 0.0053 |
|
|
863
|
+
| 13.4 sites (R 7) | 4 × 4 box | 0.0051 / 0.0013 | 0.070 / 0.026 | 0.012 / 0.0034 | 0.0090 / 0.0021 |
|
|
864
|
+
|
|
865
|
+
The ray columns are `lens.pupilSampling` (the independent reference, without diffraction) against the same
|
|
866
|
+
continuous integral. They converge toward it as the rays increase. Against the convolution, the 13.4-site disc
|
|
867
|
+
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
|
|
868
|
+
and the convolution agree to 1e-9 at every site more than one site beyond the disc's reach of the edge.
|
|
869
|
+
- **Validation** (`test/simulate-capture-defocus-render.test.ts`):
|
|
870
|
+
- A vertical edge halfway between two lattice columns gives the exact circular-segment fraction at every site,
|
|
871
|
+
to 1e-12.
|
|
872
|
+
- A 48 × 480 slanted-edge capture's averaged edge profile is compared with the disc's closed-form edge-spread
|
|
873
|
+
function. It uses RAW codes, green sites and quarter-site bins. The largest difference was 0.0093 of the
|
|
874
|
+
step (noise seed 1).
|
|
875
|
+
- The tabulated and environment paths give the same RAW.
|
|
876
|
+
- Regions and DNG parts equal the whole frame: chart, Quality options with diffraction, a `material-layers`
|
|
877
|
+
scene and a caller function.
|
|
878
|
+
- **Cost and the 64-site cap.** Direct convolution costs (2R + 1)² multiply-adds per site and node wherever the disc
|
|
879
|
+
sees more than one material. Uniform areas cost almost nothing on the tabulated path. Measured on the tabulated
|
|
880
|
+
capture path (Node 24, one thread, rounds interleaved over the radii, median [range] of 3 rounds; 2 at 2048 × 1366;
|
|
881
|
+
a shared 4-vCPU host):
|
|
882
|
+
|
|
883
|
+
| Scene, size | in focus | R 2 | R 8 | R 16 | R 32 | R 41 | R 64 |
|
|
884
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
885
|
+
| 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] |
|
|
886
|
+
| 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] |
|
|
887
|
+
| 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 |
|
|
888
|
+
| 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] |
|
|
889
|
+
| Siemens star, 2048 × 1366 | 1.39 s | | 1.8 s | | 10.4 s | | 38.3 s [37.5–38.3] |
|
|
890
|
+
|
|
891
|
+
- 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
|
|
892
|
+
2 m, focus 1.6 m: R = 41) with room for larger blurs.
|
|
893
|
+
- The time grows with R² per edge site, and the caller chooses it through the focus distance. Past the cap the
|
|
894
|
+
request fails and names the radius it needed.
|
|
895
|
+
- A 24 MP frame costs proportionally more. Render it in parts across workers, as for diffraction.
|
|
896
|
+
- Off the tabulated path (caller reflectance or radiance functions), the provider gathers only each 32-row band's
|
|
897
|
+
own values, not the 2R extra rows `calculateIrradianceConvolution` would return (#419). On a 256 × 171
|
|
898
|
+
function scene the convolution took 0.3 s at R 8, 3.5 s at R 32 and 12.9 s at R 64.
|
|
899
|
+
- It cannot be combined with `lens.pupilSampling` (the rays already render defocus) or `lens.psf` (a caller PSF
|
|
900
|
+
declares its own defocus). It can be combined with `lens.diffraction`.
|
|
901
|
+
|
|
759
902
|
### Pixel aperture (2.3)
|
|
760
903
|
|
|
761
904
|
By default each site samples the irradiance at its centre (`sensor.pixelAperture: "point"`). Scene
|
|
@@ -1345,14 +1488,16 @@ console.log(tile.image.width, tile.diagnostics.timingsMs.view);
|
|
|
1345
1488
|
```
|
|
1346
1489
|
|
|
1347
1490
|
`render(view)` returns what `simulateDisplayView(request, view)` returns, byte for byte (only `diagnostics.timingsMs`
|
|
1348
|
-
differ, and they cover the render only).
|
|
1491
|
+
differ, and they cover the render only). A second argument `{ gpu: "auto" | "require", gpuDevice }` (2.11, #481) asks for
|
|
1492
|
+
the WebGPU renderer, with the CPU reference as the labelled fallback or as a failure; `diagnostics.fast.execution` names
|
|
1493
|
+
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
1494
|
`signal` applies to preparing; give each render its own. Keep one prepared view per request in each worker: measured on
|
|
1350
1495
|
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
1496
|
|
|
1352
1497
|
**Not supported** (refused with an `InvalidConfigurationError`): `lens.pupilSampling`, `lens.projection`,
|
|
1353
1498
|
`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.
|
|
1499
|
+
curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `lens.defocus` is rendered as a
|
|
1500
|
+
site kernel in the expected signal (#492; see [Optics](#optics)). `onProgress` is not called; `signal` cancels.
|
|
1356
1501
|
|
|
1357
1502
|
The result is labelled `kind: "display-view"` with a `label` for the user interface; it has no RAW, plan or file, and
|
|
1358
1503
|
the `view.*` assumptions state the definition, the estimator, the noise model, the demosaic treatment and which capture
|