@photivra/engine 2.14.1 → 2.15.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 +23 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +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/simulate-capture.d.ts +1 -1
- package/dist/facade/simulate-capture.js +15 -4
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +90 -1
- package/dist/optics/defocus-disc-kernel.js +58 -5
- package/dist/optics/defocus-disc-kernel.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +9 -4
- package/dist/optics/diffraction-psf-profile.js +14 -6
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/lens-psf-kernels.d.ts +13 -2
- package/dist/optics/lens-psf-kernels.js +23 -6
- package/dist/optics/lens-psf-kernels.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +1 -1
- package/dist/optics/site-diffraction-kernel.js +37 -17
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +12 -1
- package/docs/QUICK_START.md +20 -4
- package/docs/USAGE.md +17 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +22 -2
- package/docs/api/facade-simulate-capture.md +1 -1
- package/docs/api/optics-defocus-disc-kernel.md +86 -0
- package/docs/api/optics-diffraction-psf-profile.md +6 -3
- package/docs/api/optics-lens-psf-kernels.md +3 -2
- package/llms.txt +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.15.0 — unreleased (prepared November 7, 2026)
|
|
4
|
+
|
|
5
|
+
A minor release: `lens.diffraction` at every spectral step (a fix for the app's color chart), and the defocus size limit with a pre-check as root exports (#532, #560). Captures, RAW codes, files and assumptions are the same as 2.14.1 for every request that 2.14.1 renders.
|
|
6
|
+
- **Fixed:** `lens.diffraction` with `sensor.spectralStepNanometers` under 6.25 nm, for example the color chart at the recommended 5 nm.
|
|
7
|
+
- **New:** `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES` and `calculateDefocusDiscKernelRadius` (#532).
|
|
8
|
+
- **Why minor:** 4 new root exports, and two public limits that accept more input (64 → 400 wavelengths). Nothing is removed and no default changes.
|
|
9
|
+
|
|
10
|
+
- **Fixed: `lens.diffraction` at every spectral step (owner report from the app, 2026-11-06).** The color chart with diffraction failed with "wavelengthsNm must list 1 to 64 wavelengths." The app renders the `color-patches` chart at `sensor.spectralStepNanometers: 5`, as docs/COLOR_PATCHES.md recommends for color work. That step gives the sensor quadrature 80 spectral nodes over 380–780 nm, and `lens.diffraction` builds one Airy kernel per node with `createDiffractionLimitedPsfProfile`, which took at most 64. So every step under 6.25 nm failed with diffraction, on every scene, in `simulateCapture`, `simulateDisplayView`, `prepareDisplayView` and `simulateIdealReference`.
|
|
11
|
+
- **Fix, exact:** `DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS.maximumWavelengths` and `LENS_PSF_CONVOLUTION_KERNELS_MAXIMUM_WAVELENGTHS` are now 400, the node count of the finest allowed step (1 nm). Each node keeps its own kernel at its own wavelength: no kernel is shared, banded or interpolated, and each equals a one-wavelength profile's kernel bit for bit. The refusal messages now state the limit ("… 1 to 400 wavelengths.").
|
|
12
|
+
- **Cost made linear in the node count:** every node's site kernel used to clone and parse the whole profile, O(N²). Now one private clone, parsed once, serves a request's nodes, with the same kernels. At radius 32 that took 80 nodes from 5.1 s to 0.12 s and 400 from 124 s to 1.4 s. The Airy cell-table cache holds 800 tables (was 256), and the capture's profile cache is also limited to the kernel values it could hold before.
|
|
13
|
+
- **Measured** (Apple M5 Pro, Node.js 24.19.0): a 4000 × 2666 color chart capture at 6 µm and f/16 takes 3.9 s without diffraction. With diffraction it takes 4.4 s at 20 nm, 4.3 s at 5 nm (heap 146 MiB against 128 MiB) and 4.5 s at 1 nm (218 MiB). The worst case, `kernelRadiusSamples: 32` at 1 nm on 1200 × 800, takes 35.9 s and 675 MiB. The full table is in docs/PSF_FOUNDATION.md, "Spectral nodes (2.15.0)"; also see docs/QUICK_START.md ("Optics") and docs/COLOR_PATCHES.md.
|
|
14
|
+
- **Tests** (`test/diffraction-spectral-grid.test.ts`): the reproduction through all four entry points; node counts from 1 nm to 100 nm; kernels against one-wavelength profiles; every chart preset and both caller scene kinds at 1 nm and 5 nm on the tabulated and per-site paths; every option that changes how diffraction runs; the display view's GPU Quality kernels (float32 emulation) at 80 nodes; the limits.
|
|
15
|
+
- **Compatibility:** no new export. Two public limits accept more input (64 → 400). Requests that 2.14.1 renders give the same values, because the kernels are bit-identical and only the cache sizes changed.
|
|
16
|
+
|
|
17
|
+
- **The defocus size limit and a pre-check are root exports (#532, for Photivra/apps#154 and Photivra/apps#157).** `lens.defocus: { model: "geometric-disc" }` refuses a disc whose kernel radius is over 64 sites (129 sites across), for example 50 mm f/1.4 focused at 1 m on a plane at 2 m with 6 µm pixels. A caller can now check a request before rendering it, instead of reading the refusal afterwards.
|
|
18
|
+
- **API:** `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES` (64) and `calculateDefocusDiscKernelRadius({ focalLengthMm, aperture, focusDistanceM, subjectDistanceM, pixelPitchMicrometers })`, which returns a `CalculationResult` (model `defocus-disc-kernel-radius` 1.0.0) with `diameterMicrometers`, `diameterSites`, `kernelRadiusSites` (0 in focus), `maximumKernelRadiusSites`, `maximumDiameterSites` (129), `withinLimit` and `excessKernelRadiusSites`. Pure: no kernel is built and nothing is rendered.
|
|
19
|
+
- **Same rule as the renderer:** it calls the capture's own disc resolution (`resolveDefocusDisc`, from `calculateDefocusCircle` 1.1.0) and the same limit test as `requireDefocusDiscKernelRadius`, which `simulateCapture`, `simulateDisplayView`, `prepareDisplayView` and `simulateIdealReference` run. Pass the f-number the capture uses (in an automatic mode, `exposure.settings.fNumber`). The limit is on the disc alone; with `lens.diffraction` the combined kernel has no further limit. The other refusals of `lens.defocus` (rotation, projection, distortion, vignetting, pupil sampling, a caller PSF) are not checked.
|
|
20
|
+
- **Validation:** unknown fields fail with `InvalidConfigurationError`; a missing, non-finite or non-positive input, or a distance inside the focal length, with `InvalidScientificInputError`.
|
|
21
|
+
- **Tests** (`test/defocus-disc-limit.test.ts`): the limit is 64 and a disc of 129 sites is the largest within it; the closed form and the capture's resolved disc; radius 0 in focus and for a disc inside one site; at two adjacent focus distances (bisected to neighbouring doubles), the inside one renders with radius 64 and the outside one (radius 65, 1 over) is refused by `simulateCapture`, `prepareDisplayView` and `simulateIdealReference`; the same disc as a program-mode capture at its resolved f-number; input validation. `test/root-export-input-errors.test.ts` lists it as a strict calculator.
|
|
22
|
+
- **Compatibility:** 4 new root exports (1,805; runtime exports 611 → 613): `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES`, `calculateDefocusDiscKernelRadius` and the types `CalculateDefocusDiscKernelRadiusInput` and `DefocusDiscKernelRadius`. None removed or changed; captures, RAW codes, files, assumptions and error messages are unchanged. Docs: `docs/QUICK_START.md` ("Optics", `lens.defocus`), `docs/USAGE.md` ("Depth of field and defocus"), `docs/AGENT_GUIDE.md`, `llms.txt`; API reference regenerated.
|
|
23
|
+
|
|
24
|
+
## 2.14.1 — published November 3, 2026
|
|
4
25
|
|
|
5
26
|
A patch release of docs and legal files only. **No API, behavior or value change from 2.14.0:** `src/` differs only in `ENGINE_API_VERSION` and one source comment, root exports are unchanged (1,801), and captures, RAW codes, files and assumptions are the same.
|
|
6
27
|
- **Copyright holder and trademark owner (#543, #545):** `NOTICE` names Shackelford Creative, LLC as the copyright holder (`Copyright 2026 Shackelford Creative, LLC`) and as the owner of the Photivra trade names, trademarks, logos and branding; `README.md` ("Licensing") says the same and gains a **Trademarks** paragraph for the third-party names it mentions (ColorChecker, RAL and others). `CONTRIBUTING.md` and the licence scan's allowed holder follow.
|
|
@@ -398,6 +419,7 @@ A minor release: new functions for writing a whole-frame DNG from region parts a
|
|
|
398
419
|
- **Faster preview:** in the app's Quality preview (Chromium, four workers), the quick frame takes 0.7 s instead of 4.0 s, and the fully sharp preview 65 s instead of 170 s.
|
|
399
420
|
- **Faster captures:** a 24 MP chart capture takes 16.4 s instead of 18.5 s, and with AHD 28.5 s instead of 33.6 s (Node, one thread), with the same bytes.
|
|
400
421
|
- **Compatibility:** five new root exports (1,734); no option, default or result field changes. `createDisplayViewExpectedSignal`, which is internal, takes an optional span hint.
|
|
422
|
+
- **Known limit** (line added after release, #400): the Quality noise limit of 2.6.0 remains (#379), unchanged from 2.8.1; 2.9.0 did not fix it, and no release is scheduled for the fix.
|
|
401
423
|
|
|
402
424
|
### Added
|
|
403
425
|
|
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.15.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.15.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_15_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.15.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.15.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.15.0` (release candidate prepared November 7, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.15.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.15.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.14.
|
|
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.14.1**, published on November 3, 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/dist/core/version.d.ts
CHANGED
package/dist/core/version.js
CHANGED
package/dist/core/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.15.0\" as const;\n"]}
|
|
@@ -828,7 +828,7 @@ export interface SimulateDisplayViewOptions {
|
|
|
828
828
|
/** Display pixels, 1 to the capture's output width and height. */
|
|
829
829
|
width: number;
|
|
830
830
|
height: number;
|
|
831
|
-
/** Stratified Monte Carlo samples per axis for each display pixel's expected value (1–32, default
|
|
831
|
+
/** Stratified Monte Carlo samples per axis for each display pixel's expected value (1–32, default 16: 256 real pixels where the display pixel has detail). */
|
|
832
832
|
samplesPerAxis?: number;
|
|
833
833
|
/** "srgb-8" (default) or "srgb-16". */
|
|
834
834
|
format?: "srgb-8" | "srgb-16";
|
|
@@ -204,10 +204,15 @@ const channelResponseCache = new Map();
|
|
|
204
204
|
* DIFFRACTION_PROFILE_CACHE_SIZE, most recently used last). Every region part and detail tile of one request builds the same
|
|
205
205
|
* input. The kept record is private: each call gets its own copy, as mutable as a newly built one, so a caller's change
|
|
206
206
|
* never reaches another call. The copy is made of plain records and arrays (plainDataCopy): the kernel set-up structured-clones
|
|
207
|
-
* the profile once per spectral node, and in a CPU profile of the 96 × 64 Quality tile
|
|
208
|
-
* (24 instead of 12 ms per call) on a deep-frozen record or on a structuredClone copy as on a newly built record or this copy.
|
|
207
|
+
* the profile (once per spectral node before 2.15.0, once per request since), and in a CPU profile of the 96 × 64 Quality tile
|
|
208
|
+
* that clone took about twice as long (24 instead of 12 ms per call) on a deep-frozen record or on a structuredClone copy as on a newly built record or this copy.
|
|
209
|
+
* 2.15.0: profiles may hold up to 400 wavelengths (a 1 nm spectral step), so the cache also keeps at most
|
|
210
|
+
* DIFFRACTION_PROFILE_CACHE_KERNEL_VALUES kernel values in all (wavelengths × (2R + 1)², summed), the most it could hold before
|
|
211
|
+
* (8 profiles of 64 wavelengths at radius 32); the newest profile is always kept.
|
|
209
212
|
*/
|
|
210
213
|
const DIFFRACTION_PROFILE_CACHE_SIZE = 8;
|
|
214
|
+
const DIFFRACTION_PROFILE_CACHE_KERNEL_VALUES = 8 * 64 * 65 * 65;
|
|
215
|
+
const kernelValuesOf = (profile) => profile.wavelengths.length * (2 * profile.kernelRadiusSamples + 1) ** 2;
|
|
211
216
|
const diffractionProfileCache = new Map();
|
|
212
217
|
/** A copy of plain data (records, arrays, primitives; typed arrays are sliced), built from ordinary literals. */
|
|
213
218
|
function plainDataCopy(value) {
|
|
@@ -231,8 +236,14 @@ function cachedDiffractionProfile(input) {
|
|
|
231
236
|
else
|
|
232
237
|
kept = createDiffractionLimitedPsfProfile(input).value;
|
|
233
238
|
diffractionProfileCache.set(key, kept);
|
|
234
|
-
|
|
235
|
-
|
|
239
|
+
let kernelValues = 0;
|
|
240
|
+
for (const profile of diffractionProfileCache.values())
|
|
241
|
+
kernelValues += kernelValuesOf(profile);
|
|
242
|
+
while (diffractionProfileCache.size > 1 && (diffractionProfileCache.size > DIFFRACTION_PROFILE_CACHE_SIZE || kernelValues > DIFFRACTION_PROFILE_CACHE_KERNEL_VALUES)) {
|
|
243
|
+
const oldest = diffractionProfileCache.keys().next().value;
|
|
244
|
+
kernelValues -= kernelValuesOf(diffractionProfileCache.get(oldest));
|
|
245
|
+
diffractionProfileCache.delete(oldest);
|
|
246
|
+
}
|
|
236
247
|
// The same fields in the same order as the built record, frozen at the top level like it; `model` is the frozen constant.
|
|
237
248
|
return Object.freeze({ ...kept, profile: plainDataCopy(kept.profile), wavelengths: plainDataCopy(kept.wavelengths), quadrature: plainDataCopy(kept.quadrature) });
|
|
238
249
|
}
|