@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +23 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/dist/core/version.d.ts +1 -1
  6. package/dist/core/version.js +1 -1
  7. package/dist/core/version.js.map +1 -1
  8. package/dist/facade/simulate-capture.d.ts +1 -1
  9. package/dist/facade/simulate-capture.js +15 -4
  10. package/dist/facade/simulate-capture.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/optics/defocus-disc-kernel.d.ts +90 -1
  15. package/dist/optics/defocus-disc-kernel.js +58 -5
  16. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  17. package/dist/optics/diffraction-psf-profile.d.ts +9 -4
  18. package/dist/optics/diffraction-psf-profile.js +14 -6
  19. package/dist/optics/diffraction-psf-profile.js.map +1 -1
  20. package/dist/optics/lens-psf-kernels.d.ts +13 -2
  21. package/dist/optics/lens-psf-kernels.js +23 -6
  22. package/dist/optics/lens-psf-kernels.js.map +1 -1
  23. package/dist/optics/site-diffraction-kernel.d.ts +1 -1
  24. package/dist/optics/site-diffraction-kernel.js +37 -17
  25. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  26. package/docs/AGENT_GUIDE.md +1 -0
  27. package/docs/API_REFERENCE.md +12 -1
  28. package/docs/QUICK_START.md +20 -4
  29. package/docs/USAGE.md +17 -0
  30. package/docs/api/core-version.md +1 -1
  31. package/docs/api/exports.json +22 -2
  32. package/docs/api/facade-simulate-capture.md +1 -1
  33. package/docs/api/optics-defocus-disc-kernel.md +86 -0
  34. package/docs/api/optics-diffraction-psf-profile.md +6 -3
  35. package/docs/api/optics-lens-psf-kernels.md +3 -2
  36. package/llms.txt +1 -0
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,6 +1,27 @@
1
1
  # Changelog
2
2
 
3
- ## 2.14.1 — unreleased (prepared November 3, 2026)
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.14.1"
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.14.1 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_14_1.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
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.14.1
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.14.1` tag.
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.14.1` (release candidate prepared November 3, 2026; publication is separate)
367
- - Engine API contract: `2.14.1`
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.14.1; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
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.0**, published on November 1, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
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
 
@@ -6,4 +6,4 @@
6
6
  * own versions. Release checks confirm it matches package.json and the packed package.
7
7
  * @see docs/RELEASE_1_0_1.md
8
8
  */
9
- export declare const ENGINE_API_VERSION: "2.14.1";
9
+ export declare const ENGINE_API_VERSION: "2.15.0";
@@ -7,5 +7,5 @@
7
7
  * own versions. Release checks confirm it matches package.json and the packed package.
8
8
  * @see docs/RELEASE_1_0_1.md
9
9
  */
10
- export const ENGINE_API_VERSION = "2.14.1";
10
+ export const ENGINE_API_VERSION = "2.15.0";
11
11
  //# sourceMappingURL=version.js.map
@@ -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.14.1\" as const;\n"]}
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 8: 64 real pixels). */
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 that clone took about twice as long
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
- if (diffractionProfileCache.size > DIFFRACTION_PROFILE_CACHE_SIZE)
235
- diffractionProfileCache.delete(diffractionProfileCache.keys().next().value);
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
  }