@photivra/engine 2.9.0 → 2.10.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 +85 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/native-environment-lean.js +89 -5
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +13 -0
- package/dist/core/portable-math.js +43 -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/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/diffraction-provider.js +50 -15
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +75 -0
- package/dist/facade/display-view.js +471 -109
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +32 -3
- package/dist/facade/expected-signal.js +192 -24
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -4
- package/dist/facade/simulate-capture.js +130 -31
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +25 -2
- package/dist/facade/simulate-display-view.js +55 -22
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +5 -0
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/docs/API_REFERENCE.md +16 -1
- package/docs/QUICK_START.md +18 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +42 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-simulate-capture.md +60 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.10.0 — unreleased (prepared October 27, 2026)
|
|
4
|
+
|
|
5
|
+
A minor release: one exact-or-fast flag for every slow option (no option has a fast method yet, so every result is still exact), export and import of the display view's AHD correction states so workers can share them, and faster display-view prepares and parts and faster Quality captures. Samples, RAW codes and files are the same as 2.9.0; captures and display views gain the `diagnostics.fast` report.
|
|
6
|
+
- **New:** `fast`, `SIMULATE_CAPTURE_FAST_OPTIONS` and `diagnostics.fast` (#395, stage 2). `serializeDisplayViewCorrectionStates`, `bindDisplayViewCorrectionStates` and three `PreparedDisplayView` methods share the AHD correction states between workers (#447); no speed-up is claimed until the app measures it.
|
|
7
|
+
- **Faster prepare** (Node, one thread): `prepareDisplayView` after an aperture change takes 54–67 ms instead of 378–430 ms in Quality (#443), and after an ISO, white-balance or shutter change 9–16 ms instead of 37–45 ms at 6 µm f/11 (#446).
|
|
8
|
+
- **Faster parts and captures** (Node, one thread, each against the build before it): Quality display-view part medians −8 to −14 % (#441), −3 to −11 % (#445) and −3 to −8 % (#442); Speed parts −4 to −7 % (#442) and up to −9 % whole-view CPU (#444); the AHD demosaic stage −40 % (#448); the app's Quality 96 × 64 detail tile median 128 → 115 ms (#449, first part) and 116 → 95 ms (#449, second part), then 102 → 90 ms with the verification sites' chain re-validating less (#453). Browser wall time is not measured.
|
|
9
|
+
- **Compatibility:** eight new root exports (1,742); new optional `fast` fields and an `output.demosaic` record form; `lens.diffraction`'s `aperture` key is deprecated in favour of `kind`. Query points use a portable `hypot3`, unchanged on V8 (#452).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **One exact-or-fast flag for every slow option (#395, stage 2): `fast`, `SIMULATE_CAPTURE_FAST_OPTIONS` and `diagnostics.fast`.** No option has a fast method yet, so every result is the same as before.
|
|
14
|
+
- **The flag:** `fast?: boolean` on the `simulateCapture` request (and so on `simulateDisplayView` and `prepareDisplayView`, which take the same request). `false` or omitted is exact, the engine reference; `true` asks each listed option for its fast method, a measured, bounded approximation. An option's record `fast` overrides the top-level flag for that option only.
|
|
15
|
+
- **The options:** `SIMULATE_CAPTURE_FAST_OPTIONS` lists `lens.diffraction`, `sensor.antiAliasing`, `sensor.pixelAperture.box` and `output.demosaic.ahd`. Each takes a record with `fast`: `lens.diffraction` `{ kind: "circular-aperture", kernelRadiusSamples?, fast? }`, `output.demosaic` (new record form) `{ kind: "bilinear" | "ahd", fast? }`, and the existing `sensor.antiAliasing` and `sensor.pixelAperture` records gain `fast?`. An option without a fast method accepts the flag, runs exact and reports `false`.
|
|
16
|
+
- **The report:** captures and display views carry `diagnostics.fast = { requested, applied, assurance }`: whether `fast: true` was given anywhere, each listed option that ran (true where its fast method ran) and `engine-reference` or `approximation-with-a-priori-bound`. The WebGPU image-kernel execution record gains `fast` (`false` for `cpu-reference`, `true` for `webgpu-f32`).
|
|
17
|
+
- **Same output:** every request gives the same samples, RAW and files as before; a new test (`test/fast-flag-convention.test.ts`) checks each listed option with the flag on, off and overridden, and enforces the convention documented in `docs/API_STYLE.md`, "Exact and fast modes".
|
|
18
|
+
- New root exports: `SIMULATE_CAPTURE_FAST_OPTIONS` and the types `SimulateCaptureFastOption`, `SimulateCaptureFastReport` and `SimulateCaptureDemosaicRecord`.
|
|
19
|
+
|
|
20
|
+
- **Display-view benchmark and validation on a device-pixel request (#454).** Measurement and documentation only; no result changes.
|
|
21
|
+
- **Benchmark:** `node scripts/benchmark-display-view.mjs --app` times a 2028 × 1352 view of a 24 MP, 6 µm frame with the 2 × 2 box, a caller material scene and a stand-in label, prepared once and rendered in 96 × 96 parts centre first, in Speed and Quality at f/8 and f/11. It prints per-part medians and maxima, µs per display pixel, the estimation method, AHD-corrected pixels, the load average, the Node version and the commit; with `DIST_B` it runs a second build interleaved. Measured on 2.9.0 (Node 24.21.0, one thread, 4-vCPU Xeon): Speed about 0.09 s per part, Quality about 0.18 s, prepare 0.2 s and 0.5 s. The 2.6.0 whole-view table is now labelled as the 2.6.0 record.
|
|
22
|
+
- **Validation set F:** `node scripts/validate-display-view.mjs --set F` (outside the default run) checks the view at device-pixel widths on the full-frame (2028 wide) and medium-format (4000 wide) sensors. Speed passes every limit. Quality misses two, now recorded in `docs/DISPLAY_VIEW.md` and `docs/validation/display-view-2026-10-26/`: the predicted noise is 1.13–1.20 of the reference's (the known limit, #379), and at the Siemens star's centre the view's AHD result differs from the capture's by 5.1 codes RMS, at most 34 (full frame).
|
|
23
|
+
- **Test:** parts of a prepared device-pixel view of a caller material scene with the box and the Quality options equal separate `simulateDisplayView` calls, by SHA-256 (`test/display-view-prepared.test.ts`).
|
|
24
|
+
|
|
25
|
+
- **Display view: export, import, collect and compute of the AHD low-signal correction states, so workers can split and share them (#447).** No result changes.
|
|
26
|
+
- **Why:** each correction state costs 27–30 ms (2048 drawn neighbourhoods through the AHD steps; Node, one thread, measured for #447), and the states are kept per JavaScript realm. Each Web Worker of an app computed the same states again, and each worker's first part waited for all of them: 897 states for one 48 × 48 part at the centre of the app's Quality view at 6 µm ISO 400.
|
|
27
|
+
- **The API:** `serializeDisplayViewCorrectionStates()` returns one `{ key, gridKeys, values }` entry per cached setup (an `Int32Array` of grid keys and a `Float64Array` of three values per state, 28 bytes per state, both transferable). `bindDisplayViewCorrectionStates(entries)` adds the states a setup does not hold yet and returns the count added. It checks every entry first (a non-empty key, grid keys in [0, 97³), 3 × that many finite values) and fails with an `InvalidConfigurationError`, leaving the cache unchanged. The cache's bounds apply as in a render (4 setups, 65 536 states each), so import only keys a prepared view reported. `PreparedDisplayView` gains `correctionKey()` (null without AHD), `collectCorrectionStates(view)` (the grid keys of every state a render of the view reads, from a render pass that records them instead of computing them; it returns no image and leaves the cache untouched) and `computeCorrectionStates(gridKeys, { signal })` (computes the given states as a render does, stores them and returns them; it yields and can be aborted like `render`). Documented in `docs/DISPLAY_VIEW.md`, "AHD at low signal", with a two-worker example in `docs/USAGE.md`.
|
|
28
|
+
- **Same output:** renders are unchanged. The state computation moved, unchanged, from the render into a shared function used by the render and `computeCorrectionStates`. Samples, expected values, noise, every diagnostic (including `sceneLookups` and `ahdCorrectedPixelCount`) and the assumptions are identical (SHA-256) to the previous build on 12 renders: the issue's comparison set (the app's Quality request at ISO 3200 at a frame corner and in the chart area, at 1.4 µm in 16 bits at the centre, at 6 µm ISO 400 on the coarse 192 × 128 view at the centre and the bottom-right corner, and at 6 µm ISO 200 and ISO 400 at the centre in 8 and 16 bits; spectral patches with mirror borders at 2 µm ISO 1600; a radiance-function scene at 1.4 µm ISO 3200; dead leaves with the Reinhard tone curve at 4 µm ISO 6400 in 16 bits). Each of the 12 also has the same hash when its states are collected, computed in two halves in two cleared caches (one half in reversed order), exchanged by export, `structuredClone` and import in reversed order, and then rendered; the collect pass returned exactly the states of the cold render (0 to 914 per render). The existing hash locks pass unchanged. New tests in `test/display-view.test.ts` render a region after importing a neighbour's states in reversed order and after merging two realms' sets, and compare it with a cold render. They also check that the collect pass returns exactly the states a cold render computes, that two realms hold the same bits for common states, and that the import rejects malformed states and leaves the cache unchanged.
|
|
29
|
+
- **Measured** (the app's Quality request, one 48 × 48 part at the view centre, a fresh process per run, against the previous build a58cbe8, three rounds alternating the builds; Node 24.21.0, one thread, 4-vCPU Xeon, load average 0.6–1.0): the per-state cost (cold minus warm render time over the states) is unchanged, 25.10–25.30 → 25.07–25.29 ms at 6 µm ISO 400 (764 states) and 27.4–30.9 → 27.4–29.2 ms at ISO 200 (32 states). The collect pass took 209–217, 126–132 and 76–77 ms in three successive calls, against 129–139, 100–105 and 94–98 ms for a warm render of the same part. `structuredClone` of the exported set (764 states, 21,392 bytes) took 10–11 µs at the median and 15–16 µs at the 90th percentile of 200 clones; serializing it took 0.19–0.21 ms. No speed-up is claimed: any gain in the app depends on its workers splitting and exchanging the states (Photivra/apps#113), which has to be measured in the browser.
|
|
30
|
+
- **Exports:** `serializeDisplayViewCorrectionStates` (the export), `bindDisplayViewCorrectionStates` (the import) and the types `DisplayViewCorrectionStateSet` and `DisplayViewCorrectionStateKeys`; root exports grow from 1,738 to 1,742. The two functions take the `serialize` and `bind` verb families of `docs/API_STYLE.md` (no new verb family).
|
|
31
|
+
|
|
32
|
+
### Faster, same output
|
|
33
|
+
|
|
34
|
+
- **Display view, exact path with diffraction (device-pixel views such as the app's Quality parts): each native site's value, material and readout moments are computed once per render (#441).**
|
|
35
|
+
- **What changed:** a real pixel's demosaic reads 27 native sites (AHD) or 7 (bilinear), and each site was read through the expected signal and clipped to its readout moments again for every real pixel that reads it, about 25 times per site on the app's Quality request. A per-render ring of native rows over the part's columns now keeps each site's expected electrons, chart material and moments after its first read; a new internal `markReads` tells the expected signal's window which of its sites a real pixel read from the ring, so `diagnostics.sceneLookups` is the count reading them would give. Also: no jitter stream on the exact path (it never draws one), the site moments read the slot arrays instead of taking boxed arguments, a record's unwritten fields are zeroed one by one instead of filling the whole record, and two-argument maxima replace a four-argument `Math.max`. The ring is used only where the expected signal fills its windows (chart materials with diffraction); coarse (stratified) views, views without diffraction (Speed, where the ring measured 2–4 % slower on the `--app` request) and radiance scenes with diffraction keep the per-read path and allocate no ring. Memory: 7 arrays of about 44 bytes per ring entry; for a 96 × 96 part at 3 native pixels per display pixel about 16 native rows × 300 columns, about 0.2 MB per render, and never more than 2^18 entries (about 11.5 MB; above that the ring is off).
|
|
36
|
+
- **Same bytes:** samples, expected values, noise, every diagnostic (including `sceneLookups` and `siteEvaluations`) and the assumptions are identical (SHA-256) to the previous build on 224 renders: the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11 (centre, corners, edges and the label area), 16-bit output, coarse stratified views (K = 4), mirror borders, ISO 3200 and a 1.4 µm sensor with the AHD correction, dead leaves with the Reinhard tone curve and a radiance-function scene, rendered in sequence, separately and concurrently. The existing hash locks pass unchanged. A new test (`test/display-view-site-ring.test.ts`) checks overlapping and adjacent parts of one prepared view in both orders and concurrently against separate calls and the whole view, with mirror borders and the AHD correction, and that `markReads` gives a filled and a direct window the lookup count and later reads of `site`.
|
|
37
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average 1.0–1.7, `DIST_B=… node scripts/benchmark-display-view.mjs --app`, 30 of 294 parts, 3 repeats per build, two separate runs, builds alternated): Quality part medians 169–194 → 148–164 ms at f/8 and 177–187 → 154–164 ms at f/11 (−8 to −14 %); Speed, where the ring is not used, within ±3 %. Browser wall time is not measured here.
|
|
38
|
+
- **Display view prepare and captures: diffraction kernels are kept by value, so an ISO, white-balance or shutter change reuses them (#446).**
|
|
39
|
+
- **What changed:** the per-node site diffraction kernels (`prepareDisplayView`, the capture's tabulated RAW path and the object-space diffraction convolution) were cached by the identity of the PSF record, which the facade builds anew for every request, so every prepare resolved them again. They are now kept by value: the key is the whole `createDiffractionLimitedPsfProfile` input (aperture, focal length, f-number, focus, spectral nodes, pitch, kernel radius, field extent, profile id), the PSF record's other fields and the query's wavelength, pitch and radius. The last four optics settings are kept (least recently used out first; about 2.7 MB with 20 spectral nodes at the 32-site radius); each caller still gets its own copy. An aperture, focal-length, focus, pitch or radius change resolves the kernels again.
|
|
40
|
+
- **Same bytes:** a kept kernel is the same pure function of the same inputs; a new test (`test/site-diffraction-kernel-cache.test.ts`) checks that kept and freshly resolved kernels are equal element by element (`Object.is`) for pitches 1.0–8.4 µm and f/1.4–f/22, that a change of any input misses, that writing to a returned kernel changes nothing later, and that prepares after an ISO, white-balance or shutter change and captures after a prepare render the bytes of a cold run. Samples, RAW codes, diagnostics without timings and assumptions are identical (SHA-256) to the previous build on 55 cases (229 digests, including the whole result without timings): the `--app` request of `scripts/benchmark-display-view.mjs` in Quality through ISO, white-balance, shutter, f-number and focus changes, the 1.4 µm sensor at f/8, f/22 at 6 µm, captures on the tabulated path and on the object-space diffraction path.
|
|
41
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average about 1.2, the app-request shape in Quality, five rounds in one process alternating the two builds; ranges): `prepareDisplayView` after an ISO change 37–45 → 9–16 ms at 6 µm f/11, 76–84 → 10–12 ms at 6 µm f/22 and 241–276 → 15–19 ms at 1.4 µm f/11; after a white-balance change 38–41 → 9–11 ms, 77–88 → 10–12 ms and 241–279 → 16–17 ms; after a shutter change 37–41 → 9–13 ms, 76–121 → 10–12 ms and 234–305 → 15–19 ms. Browser wall time is not measured here.
|
|
42
|
+
|
|
43
|
+
- **Display view and tabulated capture: the scene lookup count of a filled window is computed once per group shape (#445).**
|
|
44
|
+
- **What changed:** `diagnostics.sceneLookups` (and the capture's tabulated `providerEvaluationCount`) counts, for each filled window (chart materials with diffraction), the lattice points the per-pixel lazy caches would have looked up, a box dilation of the window's computed sites per lattice group. The count is a function of those sites and each group's rectangle and cache sizes only, so a group shaped like an earlier one now takes that group's count (on the app's Quality request all four groups have one shape), in both the bit-mask and the general branch; the OR of the site rows that reach a cache row comes from prefix and suffix ORs of the row masks, computed once per window; and a window whose every site was computed, with no site read outside it (the capture path's windows), counts every point of its caches without dilating. No image value is computed by this count.
|
|
45
|
+
- **Same bytes and the same count:** samples, every diagnostic (`sceneLookups` compared explicitly) and the assumptions are identical (SHA-256) to the previous build on the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11 (centre, corners, edges and the label area), coarse stratified views (K = 4), mirror borders, ISO 3200, a 1.4 µm sensor, dead leaves and a radiance-function scene. A new test (`test/expected-signal-lookup-count.test.ts`) compares the open and closed windows' counts with a copy of the previous algorithm and with a brute-force count of the definition over seeded random window sizes on both sides of the 31-site bit-mask limit, windows at the raster edges, kernel radii, group shapes (four-spot, two-spot, different and random offsets), partial, banded and fully read windows, reads outside the window, repeated reads and `markReads`. The existing hash locks pass unchanged.
|
|
46
|
+
- **Measured** (`DIST_B=… node scripts/benchmark-display-view.mjs --app`): Quality part medians 149–152 → 132–144 ms at f/8 and 155–158 → 138–156 ms at f/11 (−3 to −11 %; Node 24.21.0, one thread, 4-vCPU Xeon, load average 1.3, 30 of 294 parts, 3 repeats per build, builds alternated, against #441); Speed has no filled windows and is unchanged (84–91 → 85–90 ms). Browser wall time is not measured here.
|
|
47
|
+
- **Display view render loop: typed and flat per-render tables, so V8 keeps its optimized code (#442).**
|
|
48
|
+
- **What changed:** in `renderDisplayView` the readout tables (`k`, `dark`, `c0`, `readStd`) and the homogeneity white (`labWhite`) are built with `Float64Array.from` instead of `.map`; the per-phase candidate tables are read from flat arrays of typed arrays (`phase * 2 + candidate`, and `* 3 + channel` for the slots and weights) instead of nested arrays; and a site's window offset is read from two per-render `Int32Array`s (`(slot % side) - reach`, `((slot / side) | 0) - reach`, computed once) instead of a modulo and a division per site read. The number of candidates (1 bilinear, 2 AHD) is now checked to be the same for every phase. Keyed loads on the earlier tables met several array shapes, and V8 discarded the whole function's optimized code on them (`wrong map` deoptimizations) several times per view: with `node --trace-deopt --trace-opt`, one run each against #441, 15 → 2 over 36 parts of the app's recorded Quality request (completed TurboFan compiles of the function 14 → 6), 9 → 2 over 24 Quality Siemens-star parts and 16 → 4 over 24 Speed ones. The `exit from OSR'd inner loop` deoptimizations vary from run to run and are not claimed. Memory: two `Int32Array`s of at most 49 entries and four small arrays of references per render.
|
|
49
|
+
- **Same bytes:** the tables hold the same doubles and integers and every operation runs on the same values in the same order. Samples, expected values, noise, every diagnostic (including `sceneLookups` and `siteEvaluations`) and the assumptions are identical (SHA-256) to the previous build on the 224-render set of #441 (the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11 at the centre, corners, edges and label area, prepared parts in sequence and concurrently; 16-bit; coarse stratified views, K = 4; mirror borders; ISO 3200 and a 1.4 µm sensor with the AHD correction; the Reinhard tone curve; a radiance-function scene). The existing hash locks pass unchanged. A new test (`test/display-view-render-tables.test.ts`) checks the offset tables against the modulo and division and that the flat tables hold the nested tables' own arrays.
|
|
50
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average 1.3, 30 of 294 parts, 3 repeats per build, builds alternated, `DIST_B=… node scripts/benchmark-display-view.mjs --app`, against #441): Quality part medians 148–156 → 143 ms at f/8 and 154–155 → 149–151 ms at f/11 (−3 to −8 %); Speed 84–87 → 79–81 ms at f/8 and 83–91 → 80–84 ms at f/11 (−4 to −7 %). Browser wall time is not measured here.
|
|
51
|
+
|
|
52
|
+
- **Display view without diffraction (the Speed path): one-material samples are replayed there too (#444).**
|
|
53
|
+
- **What changed:** the display view replays the recorded terms of a sample whose demosaic sites all read one chart material (the one-material memo), but the expected signal reported the material only on the diffraction path, so every Speed sample was computed in full. Without diffraction a site now records whether all its query points (four with the 2 × 2 box) read one material, and the signal's direct ring and window cache keep that material beside the site's value, so the memo applies to chart materials (presets and caller material scenes) on the Speed path. Radiance scenes are not tracked. A sample whose sites read two materials cannot be replayed, so from the first site of a second material on it takes the site statistics at once instead of in a second pass, which limits the extra work on parts with no one-material samples (the Siemens-star centre).
|
|
54
|
+
- **Same bytes:** a one-material site sums the same operands in the same order as every other such site of its channel, so a replayed sample adds exactly the terms it would have computed; a site's statistics depend on that site only, so taking them earlier changes no bit. Samples, every diagnostic (including `sceneLookups` and `siteEvaluations`) and the assumptions are identical (SHA-256) to the previous build on 120 regions of 28 requests: the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11 (star centre, label area, step wedge and square patches, line grid, corners, edges and the 24 × 24 region of #440), 16-bit output, ISO 3200, a 1.4 µm sensor, coarse stratified views at K = 4 and 16, mirror borders, four-spot anti-aliasing and AHD without diffraction at ISO 100 and 3200, the dead-leaves and Siemens-star presets, and radiance-, reflectance- and material-function scenes; expected values and noise too on 7 of them, and RAW codes and images of 6 captures. A new test (`test/display-view-speed-material.test.ts`) checks the reported material on ring misses, ring hits and window-cache hits against the query points, that one-material sites of a channel hold one value, and that radiance scenes are not tracked, and locks five Speed renders (caller material scene with mirror borders, a corner part at 16 bits, the stratified estimator, AHD at ISO 3200 and a radiance scene) to hashes recorded before the change. The existing hash locks pass unchanged.
|
|
55
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average 0.8–1.6, `DIST_B=… node scripts/benchmark-display-view.mjs --app`, 30 of 294 parts, four repeats per build, builds alternated, against #445): Speed whole-view CPU, extrapolated from the sampled parts, 31.2–32.1 → 29.1–32.0 s at f/8 and 31.0–31.8 → 28.8–30.1 s at f/11 (up to −9 %); part medians 82–87 → 73–84 ms and 83–89 → 76–89 ms; the fastest parts (one material) 7.9–8.5 → 6.7–6.9 µs per display pixel. **Slower:** the Siemens-star centre part, whose samples read two materials and are never replayed, still pays the tracking: alone in a process (`PARTS=1`, six repeats, including JIT warm-up) 230–342 → 246–341 ms at f/8 and 202–262 → 234–271 ms at f/11; without the guard that takes such a sample's statistics inline it was 260–335 ms at f/8. Quality already tracked the material and is unchanged (part medians 133–147 → 131–135 ms at f/8, 136–142 → 135–141 ms at f/11). Browser wall time is not measured here.
|
|
56
|
+
|
|
57
|
+
- **Display view prepare: the channel response runs the sensor chain once per channel, and a chart, illuminant or illuminance change keeps it (#443).**
|
|
58
|
+
- **What changed:** the channel response (electrons per unit spectral radiance at each spectral node, used by `prepareDisplayView`, `simulateIdealReference` and the capture's tabulated RAW path) ran the facade's environment chain once per spectral node and channel plus once per channel to find the nodes: 63 runs on a 20 nm request. It now runs it once per channel, with unit radiance at every node, and reads each node's electrons from that node's own rate term times the exposure time. The chain is planned as engine-owned, photo-only data, as the capture path already does. The cached response is keyed without the scene bindings (scene, illumination and material profiles), which are checked but never enter its numbers, so a chart, illuminant or illuminance change reuses it; an aperture, shutter, focus or sensor change still recomputes it. ISO and white-balance changes already reused it.
|
|
59
|
+
- **Same bytes:** the per-node sums are compensated sums of per-node terms, so the earlier one-node runs returned exactly that node's term. Samples, RAW codes, diagnostics (including `sceneLookups`) and assumptions are identical (SHA-256) to the previous build on 84 cases: the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11 in five regions each, a sequence of prepares with aperture, shutter, chart, illuminant, illuminance, ISO and white-balance changes in Speed and Quality, captured regions with diffraction, a 1.4 µm capture, a DNG, two ideal references and a radiance-function scene. A new test (`test/channel-response.test.ts`) compares each node's value with the earlier one-node runs (`Object.is`) over tiers, pitches, filters, box sizes, apertures, shutters, spectral steps, illuminants, scene kinds and exposure modes, and checks that a chart, illuminant, illuminance or scene change keeps the entry with the maps and bytes of a cold run. The existing hash locks pass unchanged.
|
|
60
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon 2.80 GHz, load average 1.3; the app-request shape of `scripts/benchmark-display-view.mjs --app`, five rounds in one process alternating the two builds, each round a new f-number and shutter): `prepareDisplayView` after an aperture change 378–430 ms → 54–67 ms in Quality and 110–142 ms → 15–19 ms in Speed; after a shutter change 379–420 → 55–69 ms and 112–118 → 18–20 ms; after a chart change 379–435 → 34–39 ms and 108–115 → 6–7 ms; after an illuminant change 370–385 → 35–47 ms and 108–120 → 6–7 ms; after an illuminance change 373–405 → 28–37 ms and 112–124 → 7–11 ms. Each worker of an app prepares once per settings change, so this is time before its first part. Browser wall time is not measured here.
|
|
61
|
+
- **AHD demosaic: an interior fast path and no per-pixel allocations (#448).**
|
|
62
|
+
- **What changed:** `createAhdDemosaicTask` read every neighbour through the reflect-101 border mapping and created two closures and four small arrays per pixel. Sites whose neighbourhood lies inside the plane (two sites from the edge for the directional greens, one for the colour differences, homogeneity maps and 3 × 3 sums) now read their neighbours by direct index offsets, where the mapping returns its argument anyway; border sites keep the mapping. The homogeneity loop keeps its 16 differences in locals instead of nested arrays, and the colour matrix entries are read once. Every floating-point expression keeps its operands, order and grouping.
|
|
63
|
+
- **Same bytes:** a new test (`test/processing-ahd-exactness.test.ts`) compares `data` and `direction` bit for bit with a reference copy of the previous algorithm on seeded mosaics of every size from 3 × 3 to 7 × 7 and larger odd sizes, all four CFA phases, and windows of one frame at even and odd offsets including every border, and locks SHA-256 hashes recorded before the change. RAW codes, samples and PNG bytes are identical (SHA-256) for Quality captures (diffraction, four-spot anti-aliasing, AHD) of a 1024² region of a 24 MP frame, the four frame corners with margin and mirror borders, odd offsets, 16-bit output, ISO 3200, a 1.4 µm sensor and whole small frames, and for display views with AHD, which do not run this task.
|
|
64
|
+
- **Measured** in Node, one thread, before and after interleaved: the demosaic stage (`diagnostics.timingsMs.demosaic`) of a 1024² Quality region of a 24 MP frame 594–678 ms → 350–361 ms (−40 %, median 601 → 358 ms; five rounds, fresh processes, the two builds alternated, the second capture of each process). Run directly on the demosaic task (nine rounds, one process): a 1028² plane 450–505 → 272–331 ms (median −38 %), a 100 × 68 plane, the 96 × 64 detail tile with its margin, 3.0 → 1.7 ms (median −43 %). Inside a whole 96 × 64 tile capture the demosaic stage's own timing (11–21 ms) varied more than that and showed no measurable change. Display views, the preview and the Speed preset (bilinear demosaic) do not run this task.
|
|
65
|
+
- **Captures with diffraction keep the diffraction profile across calls, and the tabulated RAW loop does less work per site (#449, first part).**
|
|
66
|
+
- **What changed:** `simulateCapture` built the diffraction profile (`createDiffractionLimitedPsfProfile`) on every call, so every region part and detail tile of one request built the same one again. The profiles are now kept across calls by the content of their input (aperture, focal length, f-number, focus, the spectral nodes, pitch, kernel radius, field extent and profile ID), the last 8, as the channel responses already are. Each call gets its own copy of the kept record, as mutable as before, so a caller's change to `request.diffraction.profile` never reaches another call. Metering reads the CIE 1931 ȳ values of its 81 nodes from a table built once instead of evaluating the colour-matching functions for each of the 256 zones. In the tabulated RAW loop, a schedule whose opening and closing boundaries are both simultaneous (the global shutter) reads the exposure window once for the region instead of once per site; rolling-shutter schedules still read it per site. The sensor stage of each verified CFA phase reads its readout constants once, keeps the dark Poisson draw's threshold e^(−mean) while the dark mean repeats, does the ADC arithmetic inline when there is no integral non-linearity (with one it calls `adcTransfer` as before) and reuses one output record; it is checked against the full site computation at the verification sites, as before.
|
|
67
|
+
- **Same bytes:** RAW codes, saturation flags, black levels, saturation codes, samples, PNG and DNG bytes, diagnostics without timings and assumptions are identical (SHA-256) to the previous build, on the first and a repeated call in one process, for the app's Quality and Speed requests: the 96 × 64 detail tile, the 192 × 128 detail box, a 1024² region of a 24 MP frame, the frame corners with margin and mirror borders and odd offsets, 16-bit, PNG and DNG output, ISO 3200, a 1.4 µm sensor, whole small frames, a rolling shutter, a radiance-function scene, two captures awaited together, and three display-view parts. The existing hash locks pass unchanged. New tests: `test/capture-set-up-caches.test.ts` (the profile is built once for the parts of a request, any changed input misses and matches a cold run, the cache keeps 8, a caller's change to the returned record does not reach the next call, concurrent captures match sequential ones, the metered luminance keeps its bits, and the exposure window is read once for a global shutter and per site for a rolling one, with the same codes for a still scene) and `test/native-site-readout-flat.test.ts` (every site equals `calculateNativeRawSite` with fixed-pattern noise, digital gain, integral non-linearity, extra read-noise components, saturation and dark means of 0, 8 and 80 e⁻).
|
|
68
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average about 1.3, five rounds alternating the builds, a fresh process per scenario, warm calls): the app's Quality 96 × 64 detail tile median 128 → 115 ms (range 94–170 → 95–145 ms; `timingsMs.raw` median 96 → 92 ms); the 192 × 128 box and a 1024² Quality region within ±3 % (medians 177 → 172 ms and 2520 → 2468 ms). In a CPU profile the profile cache saves 1.5–2 ms and the colour-matching table 3.5–4.5 ms per call. With #446's kernel cache the larger saving the ticket measured on the PSF set-up is already in main.
|
|
69
|
+
- **Captures with diffraction keep the expected signal's chart tables across calls (#449, second part).**
|
|
70
|
+
- **What changed:** the tabulated RAW path built the expected signal's tables (the materials, the lattice groups of the query offsets, the per-group, per-channel combined diffraction kernels over every spectral node, and each material's whole-kernel sum) for every `simulateCapture` call, because they were kept per input object and every call builds a new one; every region part and detail tile of one request built the same tables again. For chart materials and caller material scenes the tables are now also kept by their content: every number they are built from (the spectral nodes, the channel responses, the query offsets and weights, the kernel radius and taps, each material's reflectance or radiance and the white radiance), compared bit for bit, the last 4. Each call still gets its own window state (each lattice group's window cache and its position and size, the bands, the site rings and the lookup bookkeeping), and the lattice material rings, which depend on the scene's lookup function, stay per input, so two captures or a capture and a display view awaited together share only the tables, which are never written after they are built. Radiance scenes are not kept by content. Single `simulateDisplayView` calls of one request share the tables the same way; a prepared view already kept them for its renders.
|
|
71
|
+
- **Same bytes:** RAW codes, saturation flags, black levels, saturation codes, samples, PNG and DNG bytes, diagnostics without timings (including `providerEvaluationCount` and `sceneLookups`) and assumptions are identical (SHA-256) to the previous build, on the first and a repeated call in one process, for the app's Quality and Speed requests with a printed label: the 96 × 64 detail tile, the 192 × 128 detail box, a 1024² region of a 24 MP frame, the frame corners with margin and mirror borders and odd offsets, 16-bit, PNG and DNG output, ISO 3200, a 1.4 µm sensor, whole small frames, a rolling shutter, a radiance-function scene, two captures and a display-view part awaited together, and display-view parts of the `--app` request of `scripts/benchmark-display-view.mjs` in Speed and Quality at f/8 and f/11. New tests (`test/expected-signal-table-cache.test.ts`): region parts of one request build the tables once and match a cold run; a changed f-number, pixel pitch, spectral step or material list misses and matches a cold run bit for bit, as does a change of any one number of the tables' input (including the sign of a zero tap); equal inputs read with their windows interleaved give the values and lookup counts of fresh tables; two captures and a display view awaited together give the bytes of sequential runs; the cache keeps 4, least recently used out first.
|
|
72
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average 0.5–1.3, five rounds alternating the builds, a fresh process per build and request, six warm calls after a cold one, against #444): the app's Quality 96 × 64 detail tile 92–174 → 74–135 ms per call, median 116 → 95 ms (`timingsMs.raw` median 94 → 77 ms); the 192 × 128 detail box 136–195 → 125–181 ms, median 160 → 153 ms (`timingsMs.raw` median 124 → 114 ms). The first call per settings still builds the tables (cold calls 301–326 → 306–317 ms and 370–377 → 362–373 ms). The display view's `--app` rows (prepared views, five repeats) are unchanged: part medians 68–73 ms in Speed and 124–135 ms in Quality with both builds. Browser wall time is not measured here.
|
|
73
|
+
- **Captures of chart and material scenes: the tabulated RAW path's four verification sites skip re-validating engine-built records, and the diffraction convolution fills its lattice from one material lookup per point (#453).**
|
|
74
|
+
- **What changed:** every tabulated `simulateCapture` call runs one site of each CFA phase through the environment chain to verify the tabulated electrons and to reproduce the readout. That chain re-validated, at each of its 656 queries per site (the app's Quality request), records the engine had built and already validated: the full provider-result parse, the scene-binding profile checks and the optical bridge's profile, focal length, f-number, focus and working f-number checks, plus a result envelope per query and one per site. It now runs through an internal engine-owned execution (`executeEngineOwnedEnvironmentPhotoSignal`): the first query runs every check as before, later queries keep every per-query check (result values, request and result identities, wavelength basis, transmission, filters, field throughput, finite irradiance) and skip only those re-validations; the photo signal is formed by the same code from the same values. With diffraction, the verification's object-space convolution filled each lattice point once per spectral node through `radianceAt`; for chart presets and caller material scenes, where `radianceAt(u, v, λ)` is `materialRadiance(chartMaterialAt(u, v), λ)`, it now looks the material up once per point and takes each node's value from one `materialRadiance` call per material present, and the shifts × nodes convolutions of a band share one validation of their geometry (each still checks its own samples and kernel). Radiance and reflectance function scenes keep the per-point fill. The per-frame (rather than per-part) verification the ticket also discusses is not part of this change.
|
|
75
|
+
- **Same bytes:** RAW codes, saturation flags, black levels, saturation codes, samples, PNG bytes, diagnostics without timings (including `providerEvaluationCount` and `sceneLookups`) and assumptions are identical (SHA-256) to the previous build, on the first and a repeated call in one process, for the app's Quality and Speed requests with a printed label: the 96 × 64 detail tile, the 192 × 128 detail box, a 1024² region of a 24 MP frame, the frame corners with margin and mirror borders and odd offsets, 16-bit, PNG and DNG output, ISO 3200, a 1.4 µm sensor, whole small frames, a rolling shutter, a radiance-function scene, two captures awaited together, and three display-view parts. The existing hash locks pass unchanged. New tests (`test/tabulated-verification-chain.test.ts`): each tabulated call runs the chain at exactly four sites, and their photo signals equal the full executor's bit for bit (Speed, Quality, a spectral material scene with a chart size, spectral patches with a rolling shutter in a region, ISO 3200 at 1.4 µm); a tabulated signal off by 10⁻⁶ relative at one CFA phase is rejected and the capture gives the environment path's bytes; `radianceAt` equals `materialRadiance(chartMaterialAt)` bit for bit over a lattice at six wavelengths for every chart preset (with spectral materials where the preset takes them), with and without a fixed-size surround, and for material scenes; the material fill gives the per-point fill's convolved values bit for bit; the shared-geometry convolutions equal `calculateIrradianceConvolution` and reject bad arrays with its messages.
|
|
76
|
+
- **Measured** (Node 24.21.0, one thread, 4-vCPU Xeon, load average 0.4–1.6, five rounds alternating the builds, a fresh process per build and request, one cold and six warm calls, against #449's table cache): the verification span (the four sites' chain, timed with two trace points in a copy of each build, 25 warm calls per process) 32.8–73.5 → 22.7–55.8 ms, median 43.2 → 27.0 ms, on the app's Quality 96 × 64 detail tile and 35.0–67.8 → 23.5–47.7 ms, median 39.3 → 25.4 ms, on the 192 × 128 detail box. Per call: the tile 84–170 → 75–121 ms, median 102 → 90 ms (`timingsMs.raw` median 79 → 67 ms); the box 127–172 → 110–159 ms, median 158 → 137 ms (`timingsMs.raw` 114 → 99 ms); the Speed tile median 32.6 → 29.0 ms. The 1024² Quality and Speed parts are unchanged (medians 2295 → 2296 ms and 990 → 993 ms): the verification is a fixed cost per call. Browser wall time is not measured here.
|
|
77
|
+
|
|
78
|
+
### Changed
|
|
79
|
+
|
|
80
|
+
- **The capture's query points have the same bits in every JavaScript engine (#452).** Its three-argument norms (the environment ray, its unit-direction checks, pupil-sampled aperture rays and the display view's copy of the query point) use a new internal `hypot3`: V8's own three-argument `Math.hypot` sequence written as correctly rounded operations. On V8 (Node, Chromium) every capture, RAW code, file and display view is unchanged; on JavaScriptCore and SpiderMonkey the query points move to V8's bits where their `Math.hypot` differs. This is a portability change, not a speed-up: no end-to-end gain was measured. The display view still takes other native `Math` inputs from the capture (`docs/DISPLAY_VIEW.md`, "Same bytes in every engine").
|
|
81
|
+
- **Tests:** `test/portable-hypot3.test.ts` locks fixed bits that hold in every engine, including the non-finite rules, and on V8 compares `hypot3` with `Math.hypot` on 10^6 seeded triples across magnitude regimes (0 differences).
|
|
82
|
+
|
|
83
|
+
### Deprecated
|
|
84
|
+
|
|
85
|
+
- **`lens.diffraction`'s `aperture` key (#395): use `kind`.** `{ aperture: "circular-aperture" }` is still accepted, with the same result, until 3.0.0; giving both `kind` and `aperture` is an error.
|
|
86
|
+
|
|
87
|
+
## 2.9.0 — published October 26, 2026
|
|
4
88
|
|
|
5
89
|
A minor release: new functions for writing a whole-frame DNG from region parts and a Print JPEG in strips, and a faster display view and Quality preview. Results are the same as 2.8.1, except display views and binned frames whose diffraction cells have β ≥ 32, which change in the last bits.
|
|
6
90
|
- **New:** `serializeSimulatedCaptureDngFromParts` joins region parts' RAW into a DNG with the bytes of one whole-frame run. `createPrintJpegStrip` and `composePrintJpegStrips` write a 1:1 Print JPEG's rows on separate workers and join them into the bytes of `createPrintJpegTask`. In the Photivra app, on four browser workers:
|
package/CITATION.cff
CHANGED
|
@@ -9,7 +9,7 @@ abstract: "An open TypeScript engine for the science and mathematics of photogra
|
|
|
9
9
|
repository-code: "https://github.com/photivra/photivra"
|
|
10
10
|
url: "https://photivra.com"
|
|
11
11
|
license: "Apache-2.0"
|
|
12
|
-
version: "2.
|
|
12
|
+
version: "2.10.0"
|
|
13
13
|
keywords:
|
|
14
14
|
- photography
|
|
15
15
|
- camera optics
|
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ The source is licensed under Apache-2.0.
|
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
|
-
Install the published ESM package below. The [2.
|
|
35
|
+
Install the published ESM package below. The [2.10.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_10_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install @photivra/engine@2.
|
|
38
|
+
npm install @photivra/engine@2.10.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
|
|
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
|
|
|
75
75
|
|
|
76
76
|
Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
|
|
77
77
|
|
|
78
|
-
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.
|
|
78
|
+
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.10.0` tag.
|
|
79
79
|
|
|
80
80
|
## Documentation
|
|
81
81
|
|
|
@@ -363,13 +363,13 @@ The application can then use those results, while the photography calculations t
|
|
|
363
363
|
|
|
364
364
|
## Status
|
|
365
365
|
|
|
366
|
-
- Repository package version: `2.
|
|
367
|
-
- Engine API contract: `2.
|
|
366
|
+
- Repository package version: `2.10.0` (release candidate prepared October 27, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.10.0`
|
|
368
368
|
- Versioned generic tier reference assets: [GENERIC_TIER_PRESETS.md](https://github.com/Photivra/photivra/blob/main/docs/GENERIC_TIER_PRESETS.md).
|
|
369
369
|
- Composed POC simulation API contract: `1.0.0`
|
|
370
370
|
- Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
|
|
371
371
|
|
|
372
|
-
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.
|
|
372
|
+
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.10.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
|
|
373
373
|
|
|
374
374
|
A breaking change to the root contracts now needs a new package major version. Additive and corrective changes follow [compatibility and migration](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_3_0.md). A 1.0 package is not a claim of calibrated physical accuracy.
|
|
375
375
|
|
package/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@ Please do not open a public issue that contains vulnerability details.
|
|
|
4
4
|
|
|
5
5
|
## Supported versions
|
|
6
6
|
|
|
7
|
-
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.
|
|
7
|
+
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.9.0**, published on October 26, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
|
|
8
8
|
|
|
9
9
|
Security fixes target the current maintained development and release line. Do not assume that older 1.x releases, snapshots or pre-1.0 releases will receive fixes.
|
|
10
10
|
|
package/THIRD_PARTY.md
CHANGED
|
@@ -23,6 +23,8 @@ Node built-in modules are used only by development tooling and the repository-lo
|
|
|
23
23
|
|
|
24
24
|
`src/sensor/stochastic-seed.ts` independently implements the MurmurHash3_x86_32 algorithm and its `fmix32` finalizer (Austin Appleby). The upstream `MurmurHash3.cpp` header states the algorithm "is placed in the public domain" and disclaims copyright. The SMHasher test suite that contains it is a separate work: its README says "SMHasher is released under the MIT license". No SMHasher or MurmurHash3 source file is vendored or copied. `test/sensor-raw-noise-independence.test.ts` reimplements, in its own code, SMHasher's verification procedure (keys of 0–255 bytes hashed with seed 256 − i, then the hash of those hashes) and compares the result with the published MurmurHash3_x86_32 verification value 0xB0F57EE3, a published fact. Test vectors earlier copied from a Stack Overflow answer (CC BY-SA 3.0) were removed.
|
|
25
25
|
|
|
26
|
+
`hypot3` in `src/core/portable-math.ts` independently implements the operation sequence of V8's three-argument `Math.hypot` (`MathHypot` and `FastMathHypot` in V8's `src/builtins/math.tq`; V8 is distributed under a BSD-style licence): a maximum, a normalisation and a Kahan-compensated sum, written as plain JavaScript arithmetic so the bits match V8's. No V8 source file is vendored or copied; the sequence is a method, and its equality with `Math.hypot` is checked by `test/portable-hypot3.test.ts`.
|
|
27
|
+
|
|
26
28
|
`src/optics/longitudinal-chromatic-aberration.ts` uses three physical constants: the Fraunhofer d (helium, 587.5618 nm), F (hydrogen, 486.1327 nm) and C (hydrogen, 656.2725 nm) spectral-line wavelengths used to define the Abbe number (as tabulated in SCHOTT Technical Information TIE-29, *Refractive Index and Dispersion*, 2023, free; also the reference wavelengths of ISO 7944). They are factual measured constants of nature, not a copied dataset or table; no glass catalogue data is included and refractive index and Abbe number are caller inputs. The 2.0 projection, distortion, Zernike/Seidel, vignetting, polarizer and haze models are independent implementations of published equations cited in `docs/PHYSICS_FOUNDATION.md` and `docs/PSF_FOUNDATION.md`; they contain no third-party code or numeric tables.
|
|
27
29
|
|
|
28
30
|
This inventory does not replace a transitive dependency/SBOM review before a release. New runtime dependencies, scientific datasets, model weights, or calibration material require explicit provenance and license review.
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
3
|
+
import { hypot3 } from "../core/portable-math.js";
|
|
3
4
|
import { apertureRadianceQuery } from "../sensor/environment-photo-signal.js";
|
|
4
5
|
import { createSensorEqeTemporalPhotoSignal } from "../sensor/temporal-photo-signal.js";
|
|
5
6
|
import { isDeeplyFrozen } from "../core/internal-radiance-evaluator.js";
|
|
@@ -273,7 +274,7 @@ function geometry(t, k, frame, environment, index, access) {
|
|
|
273
274
|
if (!Number.isFinite(reference.z) || reference.z <= 0)
|
|
274
275
|
return undefined;
|
|
275
276
|
const rx = pd * (reference.x / reference.z), ry = pd * (reference.y / reference.z);
|
|
276
|
-
const length =
|
|
277
|
+
const length = hypot3(rx, ry, pd);
|
|
277
278
|
const ray = { x: rx / length, y: ry / length, z: pd / length };
|
|
278
279
|
// Build the parseSceneRadianceEvaluationRequest output directly. Every other field is a template constant already
|
|
279
280
|
// parsed for the template site. The sample ID is a public opaque ID (trimming cannot change it) and the time is
|
|
@@ -281,7 +282,7 @@ function geometry(t, k, frame, environment, index, access) {
|
|
|
281
282
|
const direction = { x: -ray.x, y: -ray.y, z: -ray.z };
|
|
282
283
|
if (!Number.isFinite(direction.x) || !Number.isFinite(direction.y) || !Number.isFinite(direction.z))
|
|
283
284
|
return undefined;
|
|
284
|
-
const norm =
|
|
285
|
+
const norm = hypot3(direction.x, direction.y, direction.z);
|
|
285
286
|
if (!Number.isFinite(norm) || Math.abs(norm - 1) > 1e-9)
|
|
286
287
|
return undefined;
|
|
287
288
|
// #394: the key the evaluator would read for this request (undefined where it would throw: the full path decides).
|
|
@@ -1011,6 +1012,84 @@ function leanSensorSite(t, h, index, seed) {
|
|
|
1011
1012
|
flags: Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2,
|
|
1012
1013
|
whiteCode: t.whiteCode, photoRealized, darkRealized };
|
|
1013
1014
|
}
|
|
1015
|
+
function flatSensorStage(t) {
|
|
1016
|
+
const regime = t.readout.regime;
|
|
1017
|
+
return { darkRate: t.darkRate, capacity: t.capacity, pattern: t.pattern, whiteCode: t.whiteCode, regime, adcMaximumCode: t.readout.adcMaximumCode,
|
|
1018
|
+
inlDefined: regime.adc.integralNonLinearity !== undefined, gain: regime.systemConversionGainElectronsPerCode.value, black: regime.adc.blackLevelCode,
|
|
1019
|
+
digitalGain: regime.adc.digitalGain, digitalSaturationCode: regime.adc.digitalSaturationCode, threshold: regime.preAdcSaturationElectronEquivalent.value,
|
|
1020
|
+
rms: Float64Array.from(regime.readNoiseComponents, (component) => component.rmsElectrons.value), darkMean: NaN, darkThreshold: NaN,
|
|
1021
|
+
out: { rawCode: 0, blackLevelCode: 0, digitalSaturationCode: 0, flags: 0, whiteCode: 0 } };
|
|
1022
|
+
}
|
|
1023
|
+
/** Writes the site's outputs into `f.out`; false where leanSensorSite returns undefined. */
|
|
1024
|
+
function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
|
|
1025
|
+
const ratio = total / f.capacity, headroom = f.capacity - total;
|
|
1026
|
+
if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
|
|
1027
|
+
return false;
|
|
1028
|
+
const pattern = f.pattern === undefined ? null : evaluateParsedFixedPatternSite(f.pattern, { x, y });
|
|
1029
|
+
const photoMean = pattern === null ? photoE : photoE * pattern.photoResponseGainFactor;
|
|
1030
|
+
const darkMean = pattern === null ? darkE : darkE * pattern.darkCurrentFactor;
|
|
1031
|
+
if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
|
|
1032
|
+
return false;
|
|
1033
|
+
const random = chargeStream;
|
|
1034
|
+
random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
|
|
1035
|
+
const photoRealized = samplePoisson(photoMean, random);
|
|
1036
|
+
let darkRealized;
|
|
1037
|
+
if (darkMean > 0 && darkMean < 30) {
|
|
1038
|
+
// samplePoisson's branch for 0 < mean < 30 (Knuth's product of uniforms) with its threshold Math.exp(−mean) kept while the
|
|
1039
|
+
// mean repeats: the same draws from the same stream. (createPoissonSampler would also do this, but it brings the
|
|
1040
|
+
// portable-math functions into capture-only bundles.)
|
|
1041
|
+
if (darkMean !== f.darkMean) {
|
|
1042
|
+
f.darkMean = darkMean;
|
|
1043
|
+
f.darkThreshold = Math.exp(-darkMean);
|
|
1044
|
+
}
|
|
1045
|
+
const threshold = f.darkThreshold;
|
|
1046
|
+
let product = 1, count = 0;
|
|
1047
|
+
do {
|
|
1048
|
+
count += 1;
|
|
1049
|
+
product *= random.next();
|
|
1050
|
+
} while (product > threshold);
|
|
1051
|
+
darkRealized = count - 1;
|
|
1052
|
+
}
|
|
1053
|
+
else
|
|
1054
|
+
darkRealized = samplePoisson(darkMean, random);
|
|
1055
|
+
const realized = photoRealized + darkRealized + 0;
|
|
1056
|
+
if (!Number.isFinite(realized) || realized < 0)
|
|
1057
|
+
return false;
|
|
1058
|
+
const readRandom = readNoiseStream;
|
|
1059
|
+
readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
|
|
1060
|
+
const capacity = f.capacity;
|
|
1061
|
+
const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
|
|
1062
|
+
const rmsAll = f.rms;
|
|
1063
|
+
let totalReadNoise = 0;
|
|
1064
|
+
for (let k = 0; k < rmsAll.length; k++) {
|
|
1065
|
+
const rms = rmsAll[k];
|
|
1066
|
+
totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
|
|
1067
|
+
}
|
|
1068
|
+
const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
|
|
1069
|
+
const threshold = f.threshold;
|
|
1070
|
+
const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
|
|
1071
|
+
let quantized;
|
|
1072
|
+
if (f.inlDefined)
|
|
1073
|
+
quantized = adcTransfer(f.regime, f.adcMaximumCode, afterPreAdc).quantized;
|
|
1074
|
+
else {
|
|
1075
|
+
// adcTransfer without integral non-linearity: ideal = e/G + black, plus the zero INL offset, rounded half up (Math.round + 0),
|
|
1076
|
+
// then with a digital gain the ADC clamp and the gain's own rounding about the black level.
|
|
1077
|
+
const ideal = afterPreAdc / f.gain + f.black, quantizedAdc = Math.round(ideal + 0) + 0, digitalGain = f.digitalGain;
|
|
1078
|
+
if (digitalGain === undefined)
|
|
1079
|
+
quantized = quantizedAdc;
|
|
1080
|
+
else {
|
|
1081
|
+
const adcCode = Math.max(0, Math.min(quantizedAdc, f.adcMaximumCode));
|
|
1082
|
+
quantized = Math.round((adcCode - f.black) * digitalGain + f.black) + 0;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const digitalSaturationCode = f.digitalSaturationCode, out = f.out;
|
|
1086
|
+
out.rawCode = Math.max(0, Math.min(quantized, digitalSaturationCode));
|
|
1087
|
+
out.blackLevelCode = f.black;
|
|
1088
|
+
out.digitalSaturationCode = digitalSaturationCode;
|
|
1089
|
+
out.flags = Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2;
|
|
1090
|
+
out.whiteCode = f.whiteCode;
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1014
1093
|
/** The Kahan (compensated) total that composeSensorAccumulatedCharge computes, for photo and dark charge only. */
|
|
1015
1094
|
function chargeTotal(photoE, darkE) {
|
|
1016
1095
|
let sum = 0, correction = 0;
|
|
@@ -1039,17 +1118,22 @@ export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo
|
|
|
1039
1118
|
realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
|
|
1040
1119
|
out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
|
|
1041
1120
|
return undefined;
|
|
1121
|
+
// #449: the flat stage must reproduce the verification site too.
|
|
1122
|
+
const flat = flatSensorStage(t), fo = flat.out;
|
|
1123
|
+
if (!flatSensorSite(flat, photo.expectedGeneratedElectronCount, darkE, total, x, y, index, seed) || fo.rawCode !== out.rawCode || fo.blackLevelCode !== out.blackLevelCode ||
|
|
1124
|
+
fo.digitalSaturationCode !== out.digitalSaturationCode || fo.flags !== out.flags || fo.whiteCode !== out.whiteCode)
|
|
1125
|
+
return undefined;
|
|
1042
1126
|
return {
|
|
1043
1127
|
compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
|
|
1044
1128
|
// The parse-phase predicates of the full path (claim()): finite non-negative dark and total charge.
|
|
1045
|
-
const siteDark =
|
|
1129
|
+
const siteDark = flat.darkRate * duration;
|
|
1046
1130
|
if (!Number.isFinite(photoE) || photoE < 0 || !Number.isFinite(siteDark) || siteDark < 0)
|
|
1047
1131
|
return undefined;
|
|
1048
1132
|
const siteTotal = chargeTotal(photoE, siteDark);
|
|
1049
1133
|
if (!Number.isFinite(siteTotal) || siteTotal < 0)
|
|
1050
1134
|
return undefined;
|
|
1051
|
-
//
|
|
1052
|
-
return
|
|
1135
|
+
// One record for every site (#449): the caller copies the five RAW fields before its next call.
|
|
1136
|
+
return flatSensorSite(flat, photoE, siteDark, siteTotal, sx, sy, siteIndex, siteSeed) ? fo : undefined;
|
|
1053
1137
|
}
|
|
1054
1138
|
};
|
|
1055
1139
|
}
|