@photivra/engine 2.4.1 → 2.5.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 CHANGED
@@ -1,6 +1,21 @@
1
1
  # Changelog
2
2
 
3
- ## 2.4.1 — unreleased (prepared October 18, 2026)
3
+ ## 2.5.0 — unreleased (prepared October 18, 2026)
4
+
5
+ A minor release for whole-frame views and chart framing: `simulateCapture` accepts pixel pitches up to 1000 µm, so a whole sensor frame can be rendered at a small output size with the real sensor's prime, minimum focus and framing, and `calculateChartFraming` reports how much of the frame a chart covers at any distance. Both are additive: every request that 2.4.1 accepted gives byte-identical results (the engine version recorded in captures aside). Root exports grow from 1,714 to 1,717 symbols; none is removed.
6
+
7
+ ### Added
8
+
9
+ - **Whole-frame views at large pixel pitches.** `simulateCapture` `sensor.pixelPitchMicrometers` now accepts 0.5–1000 µm (0.5–100 before). Pitch = sensor width ÷ output width renders the whole frame at a small output size (36 mm at 48 pixels is 750 µm, 44 mm at 48 pixels 917 µm), so `lens.prime` (crop factor, minimum focus), `calculateSubjectDistanceForFraming` and `chartSize` match the real sensor. Above 100 µm the new assumption `sensor.wholeFrameView` records the frame size and states that the noise is that of a binned, downsized view: each pixel's shot noise equals the real photosites in its square summed, while read noise and dark current are applied once per output pixel. Tested at 100–1000 µm: brightness against 6 µm at area ratios up to 27 778 (each CFA channel's RAW mean within half a code, sRGB mean within one, identical saturation), photon transfer at ISO 12 800 within 12 %, diffraction kernels far below a pixel, the four-spot filter, AHD, 2 × 2 and 4 × 4 box apertures, rotation blur and a rolling shutter (each matching the same framing at 1/100 of the pitch), byte-identical region tiles, and a clean `unsupported-float32-domain` from the `webgpu-f32` sensor stage above its 2^24 e⁻ capacity (see the quick start's "Whole-frame views (2.5)").
10
+ - **`calculateChartFraming`: chart coverage at any subject distance.** `calculateChartFraming({ sensor, focalLengthMm | equivalentFocalLengthMm, chart, subjectDistanceM, focusDistanceM? })` returns the thin-lens magnification v/d (v at the focus distance, default the subject distance), the framed plane size, the chart's coverage of the frame width and height (above 1 when cropped), its image size in millimetres, the generic prime of that equivalent focal length and the minimum-focus check (`belowMinimumFocus`), with assumptions. It is the framing `simulateCapture` uses: tested equal to `result.scene.chart` with `chartSize` (within 1e-12 relative, including a separate focus distance) and to coverage 1 on the fitted side at `calculateSubjectDistanceForFraming`'s distance. New `CalculateChartFramingInput` and `ChartFraming`. Root exports grow from 1,714 to 1,717 symbols; none is removed.
11
+
12
+ ### Changed
13
+
14
+ - **Diffraction cells far larger than the Airy pattern are integrated in real space.** `createDiffractionLimitedPsfProfile` (and so `lens.diffraction`) integrates cells with β = pitch/(λ·N_w) ≥ 192 from the Airy encircled energy 1 − J₀² − J₁² (Hankel expansion, DLMF 10.17.3) instead of the frequency-domain quadrature, whose work grows as β²: a 48 × 32 view of a 44 mm frame at f/1.4 with diffraction takes 1.0 s instead of 296 s. Same model; the two methods agree within 2e-12 at β = 192–400. `quadrature.method` gains `"real-space-encircled-energy-gauss-legendre"` and `"frequency-domain-and-real-space"`, with `quadrature.realSpaceWavelengthsNm` listing the real-space wavelengths; profiles below the threshold are unchanged. Every `simulateCapture` pitch up to 100 µm stays below it.
15
+
16
+ Requests at 0.5–100 µm are byte-identical to 2.4.1 (the hash locks and the facade fingerprints of 15 default-path results).
17
+
18
+ ## 2.4.1 — published October 18, 2026
4
19
 
5
20
  A republication of 2.4.0. 2.4.0 was tagged and released on GitHub on October 18, 2026 but never reached npm: the Publish npm workflow's verify job hit its 10-minute limit during the coverage gate on both attempts and was cancelled before the publish job ran. 2.4.1 carries the 2.4.0 contents unchanged (caller text metadata in JPEG, PNG and DNG outputs, `output.metadata`, `serializeNativeRawDng` `textMetadata` and `OutputTextMetadata`; see the 2.4.0 entry below) plus the publish-workflow fix. Apart from the engine version that captures record, every request, plan, RAW, image, encoded file and hash lock is byte-identical to 2.4.0, and without `output.metadata` to 2.3.0. Root exports stay at 1,714 symbols; none is added or removed.
6
21
 
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.4.1"
12
+ version: "2.5.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.4.1 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_4_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.5.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_5_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.4.1
38
+ npm install @photivra/engine@2.5.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.4.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.5.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.4.1` (release candidate prepared October 18, 2026; publication is separate)
367
- - Engine API contract: `2.4.1`
366
+ - Repository package version: `2.5.0` (release candidate prepared October 18, 2026; publication is separate)
367
+ - Engine API contract: `2.5.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.4.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.5.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.3.0**, published on October 18, 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.4.1**, published on October 18, 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.4.1";
9
+ export declare const ENGINE_API_VERSION: "2.5.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.4.1";
10
+ export const ENGINE_API_VERSION = "2.5.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,OAAgB,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.4.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,OAAgB,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.5.0\" as const;\n"]}
@@ -239,7 +239,11 @@ export interface SimulateCaptureOptions {
239
239
  /** Camera motion and shutter type. Default: still camera, global electronic shutter. */
240
240
  motion?: SimulateCaptureMotion;
241
241
  sensor?: {
242
- /** Pixel pitch in micrometres. Default 6. */
242
+ /**
243
+ * Pixel pitch in micrometres, 0.5–1000. Default 6. The simulated sensor is output width × pitch wide. 2.5: above 100 µm the
244
+ * capture is a downsized view of a whole frame (for example 36 mm / 48 pixels = 750 µm), recorded in the assumption
245
+ * `sensor.wholeFrameView`: brightness is unchanged and the noise is that of a binned view, not of the real sensor's pixels.
246
+ */
243
247
  pixelPitchMicrometers?: number;
244
248
  /** Maximum spectral quadrature subinterval in nm. Default 20. */
245
249
  spectralStepNanometers?: number;
@@ -120,6 +120,10 @@ export const SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX = Math.PI * sosReferenceLu
120
120
  * aperture with fill factor 1. At other areas the facade scales the electron-domain readout values by
121
121
  * area / this reference, so the same focal-plane exposure H gives the same output code (see `isoAreaScaledReadout`).
122
122
  */
123
+ /** 2.5: the largest accepted pixel pitch, µm (44 mm ÷ 48 pixels ≈ 917 µm, a whole full-frame or larger sensor at a small output). */
124
+ const SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS = 1000;
125
+ /** Pitches above this, µm, are larger than any camera photosite: the capture is a downsized whole-frame view (assumption sensor.wholeFrameView). */
126
+ const SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS = 100;
123
127
  const ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS = DEFAULTS.pixelPitchMicrometers * DEFAULTS.pixelPitchMicrometers;
124
128
  /** Demosaic border, native sites per side. */
125
129
  const MARGIN = 1;
@@ -687,8 +691,8 @@ async function runCapture(options, idealReference) {
687
691
  fail("output.jpegQuantizationStep applies only to output.format \"jpeg\".");
688
692
  const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing", "pixelAperture"]);
689
693
  const pitch = positive(pick(sensorOptions, "pixelPitchMicrometers", "sensor.pixelPitchMicrometers", DEFAULTS.pixelPitchMicrometers, "6 µm pixels (about a 24 MP full-frame sensor). The simulated sensor is width × pitch wide, so small images are a central crop of a large sensor, not a downscaled full frame."), "sensor.pixelPitchMicrometers");
690
- if (pitch < 0.5 || pitch > 100)
691
- fail("sensor.pixelPitchMicrometers must be 0.5-100.");
694
+ if (pitch < 0.5 || pitch > SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS)
695
+ fail("sensor.pixelPitchMicrometers must be 0.5-" + SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS + ".");
692
696
  const spectralStep = positive(pick(sensorOptions, "spectralStepNanometers", "sensor.spectralStepNanometers", DEFAULTS.spectralStepNanometers, "Spectral midpoint quadrature over 380-780 nm with subintervals of at most 20 nm."), "sensor.spectralStepNanometers");
693
697
  if (spectralStep < 1 || spectralStep > 100)
694
698
  fail("sensor.spectralStepNanometers must be 1-100.");
@@ -947,6 +951,15 @@ async function runCapture(options, idealReference) {
947
951
  "the aperture area: a design consequence of holding the tier's ISO rating and highlight headroom fixed, not a measured full-well model (published sensor data grow less than " +
948
952
  "proportionally with area across technologies).");
949
953
  }
954
+ // Whole-frame views (2.5): a pitch above 100 µm is larger than any camera photosite, so the capture is a downsized view of a frame width × pitch wide.
955
+ if (pitch > SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS) {
956
+ assume("sensor.wholeFrameView", "model", { pixelPitchMicrometers: pitch, frameWidthMm: width * pitch / 1000, frameHeightMm: height * pitch / 1000 }, "A pixel pitch above 100 µm is larger than any camera photosite, so each output pixel stands for a " + pitch + " µm square of a " + width * pitch / 1000 + " × " + height * pitch / 1000 +
957
+ " mm frame: a downsized view of the whole frame, with the framing, crop factor, generic prime and minimum focus of that frame. Its signal is the charge of the whole square " +
958
+ "(sensor.isoAreaNormalization), so its shot noise is that of all the real photosites in the square summed or averaged (Poisson counts add), and the image brightness is the " +
959
+ "same as at any other pitch. Read noise and dark current are applied once per output pixel, not once per real photosite, so in deep shadows the noise is lower than a real " +
960
+ "sensor's binned output. This is the noise of a binned, downsized view, not the real sensor's per-pixel noise. A point pixel aperture samples one point of each square; " +
961
+ "use sensor.pixelAperture box (or sensor.antiAliasing) to average scene detail finer than the pitch.");
962
+ }
950
963
  assume("sensor.darkCurrent", "model", { electronsPerSecond: DARK_CURRENT_ELECTRONS_PER_SECOND, temperatureC: OPERATING_TEMPERATURE_C }, "Synthetic uniform dark current of 1 e⁻/s per site at 20 °C (the tier presets declare none); dark shot noise follows from it.");
951
964
  assume("sensor.spectralResponse", "model", { kind: "colorimetric-cmf-eqe", peakScale: EQE_SCALE }, "Idealized colorimetric sensor: channel EQE = a·{x̄, ȳ, z̄}(λ)·555/λ (the engine's analytic CIE 1931 fit), so RAW channels are proportional to CIE XYZ (Luther–Ives) and the camera matrix is diagonal. Not a real camera's response; CMF air wavelengths are used as vacuum wavelengths (difference about 0.03 %).");
952
965
  if (boxAperture) {