@photivra/engine 2.15.0 → 2.17.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 (119) hide show
  1. package/CHANGELOG.md +102 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +38 -0
  6. package/dist/camera/lens-projection.d.ts +5 -0
  7. package/dist/camera/lens-projection.js +10 -0
  8. package/dist/camera/lens-projection.js.map +1 -1
  9. package/dist/capture/native-environment-lean.js +1 -1
  10. package/dist/capture/native-environment-lean.js.map +1 -1
  11. package/dist/capture/native-raw-development.d.ts +15 -0
  12. package/dist/capture/native-raw-development.js +12 -6
  13. package/dist/capture/native-raw-development.js.map +1 -1
  14. package/dist/capture/raw-development-kernel.d.ts +10 -3
  15. package/dist/capture/raw-development-kernel.js +25 -6
  16. package/dist/capture/raw-development-kernel.js.map +1 -1
  17. package/dist/core/internal-radiance-evaluator.d.ts +5 -2
  18. package/dist/core/internal-radiance-evaluator.js.map +1 -1
  19. package/dist/core/version.d.ts +1 -1
  20. package/dist/core/version.js +1 -1
  21. package/dist/core/version.js.map +1 -1
  22. package/dist/equipment/generic-lens-catalogue.d.ts +227 -0
  23. package/dist/equipment/generic-lens-catalogue.js +243 -0
  24. package/dist/equipment/generic-lens-catalogue.js.map +1 -0
  25. package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
  26. package/dist/facade/defocus-bokeh-zones.js +53 -0
  27. package/dist/facade/defocus-bokeh-zones.js.map +1 -0
  28. package/dist/facade/defocus-image-space-provider.d.ts +118 -0
  29. package/dist/facade/defocus-image-space-provider.js +448 -0
  30. package/dist/facade/defocus-image-space-provider.js.map +1 -0
  31. package/dist/facade/diffraction-provider.d.ts +45 -0
  32. package/dist/facade/diffraction-provider.js +120 -14
  33. package/dist/facade/diffraction-provider.js.map +1 -1
  34. package/dist/facade/display-view-gpu-session.js +4 -0
  35. package/dist/facade/display-view-gpu-session.js.map +1 -1
  36. package/dist/facade/display-view-gpu.d.ts +10 -0
  37. package/dist/facade/display-view-gpu.js.map +1 -1
  38. package/dist/facade/display-view-lens-signal.d.ts +30 -0
  39. package/dist/facade/display-view-lens-signal.js +427 -0
  40. package/dist/facade/display-view-lens-signal.js.map +1 -0
  41. package/dist/facade/display-view.d.ts +21 -0
  42. package/dist/facade/display-view.js +66 -8
  43. package/dist/facade/display-view.js.map +1 -1
  44. package/dist/facade/expected-signal-setup.d.ts +7 -0
  45. package/dist/facade/expected-signal-setup.js +12 -2
  46. package/dist/facade/expected-signal-setup.js.map +1 -1
  47. package/dist/facade/expected-signal.d.ts +8 -0
  48. package/dist/facade/expected-signal.js +150 -0
  49. package/dist/facade/expected-signal.js.map +1 -1
  50. package/dist/facade/generic-lens-capture.d.ts +6 -0
  51. package/dist/facade/generic-lens-capture.js +8 -0
  52. package/dist/facade/generic-lens-capture.js.map +1 -0
  53. package/dist/facade/large-defocus-lattice.d.ts +39 -0
  54. package/dist/facade/large-defocus-lattice.js +64 -0
  55. package/dist/facade/large-defocus-lattice.js.map +1 -0
  56. package/dist/facade/scene-radiance.d.ts +5 -0
  57. package/dist/facade/scene-radiance.js +5 -2
  58. package/dist/facade/scene-radiance.js.map +1 -1
  59. package/dist/facade/simulate-capture.d.ts +148 -6
  60. package/dist/facade/simulate-capture.js +521 -88
  61. package/dist/facade/simulate-capture.js.map +1 -1
  62. package/dist/facade/simulate-display-view.js +29 -19
  63. package/dist/facade/simulate-display-view.js.map +1 -1
  64. package/dist/gpu/display-view-quality-gpu.d.ts +2 -9
  65. package/dist/gpu/display-view-quality-gpu.js +63 -13
  66. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  67. package/dist/gpu/display-view-speed-gpu.d.ts +2 -0
  68. package/dist/gpu/display-view-speed-gpu.js +47 -16
  69. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  70. package/dist/gpu/image-kernel-accelerator.js +3 -0
  71. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  72. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -0
  73. package/dist/gpu/wgsl-display-view-quality-kernel.js +53 -2
  74. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  75. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +15 -0
  76. package/dist/gpu/wgsl-display-view-speed-kernel.js +42 -1
  77. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  78. package/dist/index.d.ts +5 -2
  79. package/dist/index.js +4 -1
  80. package/dist/index.js.map +1 -1
  81. package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
  82. package/dist/optics/defocus-bokeh-kernel.js +380 -0
  83. package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
  84. package/dist/optics/defocus-disc-kernel.d.ts +44 -21
  85. package/dist/optics/defocus-disc-kernel.js +47 -25
  86. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  87. package/dist/optics/generic-lens-psf.d.ts +68 -0
  88. package/dist/optics/generic-lens-psf.js +337 -0
  89. package/dist/optics/generic-lens-psf.js.map +1 -0
  90. package/dist/optics/illumination-vignetting.d.ts +6 -0
  91. package/dist/optics/illumination-vignetting.js +24 -7
  92. package/dist/optics/illumination-vignetting.js.map +1 -1
  93. package/dist/optics/sensor-environment-query.d.ts +37 -0
  94. package/dist/optics/sensor-environment-query.js +58 -19
  95. package/dist/optics/sensor-environment-query.js.map +1 -1
  96. package/dist/optics/site-kernel-runs.d.ts +77 -0
  97. package/dist/optics/site-kernel-runs.js +200 -0
  98. package/dist/optics/site-kernel-runs.js.map +1 -0
  99. package/dist/output/generic-lens-corrections.d.ts +24 -0
  100. package/dist/output/generic-lens-corrections.js +51 -0
  101. package/dist/output/generic-lens-corrections.js.map +1 -0
  102. package/dist/processing/development-detail.d.ts +132 -0
  103. package/dist/processing/development-detail.js +325 -0
  104. package/dist/processing/development-detail.js.map +1 -0
  105. package/docs/AGENT_GUIDE.md +1 -0
  106. package/docs/API_REFERENCE.md +54 -1
  107. package/docs/QUICK_START.md +444 -26
  108. package/docs/USAGE.md +71 -8
  109. package/docs/api/capture-native-raw-development.md +16 -0
  110. package/docs/api/core-version.md +1 -1
  111. package/docs/api/equipment-generic-lens-catalogue.md +281 -0
  112. package/docs/api/exports.json +162 -2
  113. package/docs/api/facade-simulate-capture.md +151 -5
  114. package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
  115. package/docs/api/optics-defocus-disc-kernel.md +25 -17
  116. package/docs/api/optics-sensor-environment-query.md +6 -0
  117. package/docs/api/output-generic-lens-corrections.md +39 -0
  118. package/llms.txt +1 -0
  119. package/package.json +1 -1
package/docs/USAGE.md CHANGED
@@ -1455,7 +1455,7 @@ console.log(blur.value.diameterMm);
1455
1455
 
1456
1456
  You supply the circle-of-confusion criterion used for depth of field (DOF). These are ideal geometric thin-lens models. They do not include diffraction or real-lens aberrations in the DOF criterion.
1457
1457
 
1458
- `simulateCapture` and the display view render the defocus blur disc with `lens.defocus: { model: "geometric-disc" }`, up to a kernel radius of `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES` (64 sites, a disc 129 sites across) and refuse a larger one. Use `calculateDefocusDiscKernelRadius()` to check a request before rendering it. It runs the capture's own resolution and limit test, so it agrees with the capture's refusal. Pass the f-number the capture uses (in an automatic exposure mode, the resolved one) and the sensor's pixel pitch:
1458
+ `simulateCapture` and the display view render the defocus blur disc with `lens.defocus: { model: "geometric-disc" }`, up to a kernel radius of `DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES` (512 sites, a disc 1025 sites across, 6.15 mm at 6 µm; 64 sites before 2.17.0) and refuse a larger one. Up to 64 sites the disc is convolved directly, as before; above it, by runs of equal kernel weights (#564), the same sums at a cost that grows with the radius instead of its square ([Quick start, Large defocus discs](QUICK_START.md#large-defocus-discs-564)). Use `calculateDefocusDiscKernelRadius()` to check a request before rendering it. It runs the capture's own resolution and limit test, so it agrees with the capture's refusal, and `convolution` says which method renders it. Pass the f-number the capture uses (in an automatic exposure mode, the resolved one) and the sensor's pixel pitch:
1459
1459
 
1460
1460
  ```ts
1461
1461
  import { calculateDefocusDiscKernelRadius } from "@photivra/engine";
@@ -1463,13 +1463,14 @@ import { calculateDefocusDiscKernelRadius } from "@photivra/engine";
1463
1463
  const check = calculateDefocusDiscKernelRadius({
1464
1464
  focalLengthMm: 50,
1465
1465
  aperture: 1.4,
1466
- focusDistanceM: 1,
1466
+ focusDistanceM: 0.25,
1467
1467
  subjectDistanceM: 2,
1468
1468
  pixelPitchMicrometers: 6
1469
1469
  });
1470
1470
 
1471
- console.log(check.value.withinLimit); // false: lens.defocus refuses this request
1472
- console.log(check.value.kernelRadiusSites, check.value.excessKernelRadiusSites); // 78, 14
1471
+ console.log(check.value.withinLimit); // false: lens.defocus refuses this request (a 7.81 mm blur, 1,303 sites across)
1472
+ console.log(check.value.kernelRadiusSites, check.value.excessKernelRadiusSites); // 651, 139
1473
+ // Focused at 1 m instead: 157 sites across, radius 78, withinLimit true, convolution "runs".
1473
1474
  ```
1474
1475
 
1475
1476
  See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#thin-lens-focus-and-depth-of-field).
@@ -3293,6 +3294,9 @@ void develop;
3293
3294
 
3294
3295
  - `highlightMode: "reconstruct"` blends pixels above 0.8 of white towards neutral, instead of clipping
3295
3296
  each WB-scaled channel. It is an approximation, not a recovery of the clipped signal.
3297
+ - `exposureCompensationStops` (#556, −2 to +5, default 0) multiplies each site's black-subtracted, white-normalized
3298
+ value by 2^stops before demosaic and white balance. A site pushed to white clips and counts as saturated; the RAW
3299
+ codes are not changed. 0 develops byte for byte as before. The WebGPU accelerator rejects a non-zero push.
3296
3300
  - `extended-reinhard-luminance` takes `{ whiteLuminance }`, the luminance mapped to output white.
3297
3301
  - `createPhotographicExportPair(input, { signal })` accepts an optional `AbortSignal`.
3298
3302
 
@@ -7467,6 +7471,28 @@ console.log(calculateGeometricFocusBreathing({
7467
7471
  }).value.deltaDegrees);
7468
7472
  ```
7469
7473
 
7474
+ ## Generic lens catalogue (#549)
7475
+
7476
+ `GENERIC_LENS_CATALOGUE` lists the engine's 30 generic primes (17 professional, 13 consumer, "PV 50 1.2 Pro", "PV 50 1.8"; Photivra's
7477
+ own designs, not real products). `resolveGenericLens` resolves one for a sensor, an f-number and a focus distance: its focal length
7478
+ (scaled by the crop factor), aperture range, close focus, projection and its character, Seidel aberrations, axial and lateral
7479
+ colour, distortion and vignetting profiles, flare, breathing and focus scatter, each a labelled approximation.
7480
+ `simulateGenericLensFocusError` draws a seeded autofocus error; `createGenericLensCorrectionProfile` builds the lens's generic
7481
+ correction profile for `resolveLensCorrectionPlan`. `simulateCapture` renders a catalogue lens with `lens.catalogue`
7482
+ ([quick start](QUICK_START.md#generic-lens-catalogue-549)). Equations, tables and the chart measurements:
7483
+ [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7484
+
7485
+ ```ts
7486
+ import { GENERIC_LENS_CATALOGUE, resolveGenericLens, simulateGenericLensFocusError } from "@photivra/engine";
7487
+
7488
+ const frame = { widthMm: 36, heightMm: 24 };
7489
+ const pro = resolveGenericLens({ lensId: "pv-50-1.2-pro", activeImagingArea: frame, fNumber: 2 });
7490
+ const consumer = resolveGenericLens({ lensId: "pv-50-1.8", activeImagingArea: frame, fNumber: 2 });
7491
+ console.log(GENERIC_LENS_CATALOGUE.length, pro.character.aberrations.comaWaves < consumer.character.aberrations.comaWaves); // 30 true
7492
+ const shot = simulateGenericLensFocusError({ lensId: "pv-50-1.8", activeImagingArea: frame, fNumber: 1.8, focusDistanceM: 2, seed: 1, shotIndex: 0 });
7493
+ console.log(shot.focusDistanceM, shot.imageSideErrorMicrometers);
7494
+ ```
7495
+
7470
7496
  ## Parallel native environment RAW (row-band partitions, 2.0)
7471
7497
 
7472
7498
  `createNativeEnvironmentRawTask` runs on one thread. For several cores, split the capture into row bands with
@@ -7802,6 +7828,27 @@ import { simulateDisplayView } from "@photivra/engine";
7802
7828
  const view = await simulateDisplayView({ scene: { preset: "slanted-edge" }, sensor: { pixelPitchMicrometers: 3 }, output: { width: 600, height: 400 } },
7803
7829
  { width: 60, height: 40 });
7804
7830
  console.log(view.kind, view.view.scaleX, view.assumptions.find((a) => a.id === "view.definition")?.value);
7831
+ // #533: the real capture's clipped-highlight pixels in the rendered region, as the expected value of simulateCapture's count.
7832
+ console.log(view.diagnostics.highlight.saturatedPixelCount, view.diagnostics.highlight.statistic); // a real number, "expected-value"
7833
+ ```
7834
+
7835
+ `diagnostics.highlight.saturatedPixelCount` is the expected number of the real capture's output pixels in the rendered
7836
+ region that have a clipped contributing site. `simulateCapture` counts the same pixels on its one noise sample, so the two
7837
+ are equal where every site is far from white. Counts of regions that tile a view add up to the whole view's count. See
7838
+ [Highlight count](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#highlight-count-533).
7839
+
7840
+ From #563 the view renders `lens.projection`, `lens.distortion`, `lens.vignetting` and camera rotation (global
7841
+ shutter), in focus and with `lens.defocus` out of focus, with each real site's expected electrons as the capture's. These
7842
+ requests run on the CPU (`gpu: "auto"` falls back labelled `unsupported-on-backend`); rotation with a rolling shutter is
7843
+ refused. See [Lens projection, distortion, vignetting and rotation](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#lens-projection-distortion-vignetting-and-rotation).
7844
+
7845
+ ```ts
7846
+ import { simulateDisplayView } from "@photivra/engine";
7847
+
7848
+ const fisheye = await simulateDisplayView({ scene: { preset: "siemens-star" }, sensor: { pixelPitchMicrometers: 3 }, output: { width: 120, height: 80 },
7849
+ lens: { projection: "fisheye-equisolid-angle", vignetting: { normalizationRadiusMm: 0.25, maximumNormalizedRadius: 1, coefficients: { r2: -0.4, r4: 0, r6: 0 } } } },
7850
+ { width: 30, height: 20 });
7851
+ console.log(fisheye.kind, fisheye.assumptions.find((a) => a.id === "lens.vignetting")?.kind); // "display-view" "model"
7805
7852
  ```
7806
7853
 
7807
7854
  #### Rendering a prepared view on the GPU (2.11, #481)
@@ -7809,9 +7856,10 @@ console.log(view.kind, view.view.scaleX, view.assumptions.find((a) => a.id === "
7809
7856
  `prepareDisplayView(request, { gpu })` chooses the renderer: `"off"` (the default) is the CPU reference, `"auto"` the GPU
7810
7857
  when a conforming device is available and the request is supported (else the CPU, labelled), `"require"` fails instead of
7811
7858
  falling back. `gpuDevice` hands the view a worker's `navigator.gpu` or an `ImageKernelAccelerator` whose device it shares.
7812
- Every render says which path ran in `diagnostics.fast.execution`, in the accelerator's words. No GPU display-view renderer
7813
- exists yet (#478), so today `"auto"` runs the CPU reference labelled `unsupported-on-backend` without requesting a device;
7814
- see [Rendering on the GPU](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#rendering-on-the-gpu).
7859
+ Every render says which path ran in `diagnostics.fast.execution`, in the accelerator's words. The engine's renderer runs the
7860
+ Speed (#478) and Quality (#479) kernels on a conforming device for the requests it covers. Elsewhere `"auto"` runs the CPU
7861
+ reference and gives the reason (in Node without a WebGPU entry point, for example); see
7862
+ [Rendering on the GPU](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#rendering-on-the-gpu).
7815
7863
 
7816
7864
  ```ts
7817
7865
  import { prepareDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
@@ -7912,7 +7960,7 @@ console.log(barrel.request.siteInput(0).environment.lensFieldMapping?.distortion
7912
7960
  lattice, and the capture convolves the scene radiance with it, which is a geometric approximation. With
7913
7961
  `lens.diffraction`, each spectral node's diffraction kernel is convolved with the disc, which is a further
7914
7962
  approximation. The `lens.defocus` assumption records the diameter in µm and in sites, the kernel radius and the
7915
- execution. `simulateDisplayView` does not render it yet. See [Optics](QUICK_START.md#optics).
7963
+ execution. `simulateDisplayView` renders it too, also with projection, distortion, vignetting and rotation (#563). See [Optics](QUICK_START.md#optics).
7916
7964
 
7917
7965
  ```ts
7918
7966
  import { simulateCapture } from "@photivra/engine";
@@ -7926,6 +7974,21 @@ const blurred = await simulateCapture({
7926
7974
  console.log(blurred.assumptions.find((a) => a.id === "lens.defocus")?.value); // diameterSites ≈ 5.95, kernelRadiusSites 3
7927
7975
  ```
7928
7976
 
7977
+ Bokeh character (#553): `lens.defocus` also takes `blades` (a polygonal or rounded-blade opening), `sphericalAberration`
7978
+ (primary W040: a soft-edged blur on one side of the focus plane and a bright-rimmed one on the other) and `catsEye`
7979
+ (mechanical vignetting by field position; `simulateCapture` only). Each is off by default. `calculateDefocusBokehKernel`
7980
+ returns the kernel without rendering. See [Bokeh character](QUICK_START.md#bokeh-character-553).
7981
+
7982
+ ```ts
7983
+ import { calculateDefocusBokehKernel } from "@photivra/engine";
7984
+
7985
+ const kernel = calculateDefocusBokehKernel({
7986
+ focalLengthMm: 50, aperture: 4, focusDistanceM: 1.4, subjectDistanceM: 2, pixelPitchMicrometers: 6,
7987
+ bokeh: { blades: { count: 5 }, sphericalAberration: { w040Micrometers: 0.5, atFNumber: 4 } }
7988
+ });
7989
+ console.log(kernel.value.side, kernel.value.kernelRadiusSites, kernel.value.method); // "behind-focus", 17, "pupil-ray-binning"
7990
+ ```
7991
+
7929
7992
  `motion.angularVelocityRadPerSec` rotates the camera during the exposure (8 time midpoints by
7930
7993
  default). `motion.shutter: "rolling"` derives each site's exposure window from the body's rolling
7931
7994
  readout, with `resolveElectronicRollingShutterExposureSchedule`. See [Motion](QUICK_START.md#motion).
@@ -31,6 +31,14 @@ export function createNativeRawDevelopmentTask(input: NativeRawDevelopmentInput,
31
31
 
32
32
  This call is synchronous; any supplied provider must follow its explicit synchronous contract.
33
33
 
34
+ ## NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS
35
+
36
+ Accepted range of `NativeRawDevelopmentInput.exposureCompensationStops` and `simulateCapture`'s `output.exposureCompensationStops` (#556).
37
+
38
+ ```ts
39
+ NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS = Object.freeze({ minimum: -2, maximum: 5 } as const)
40
+ ```
41
+
34
42
  ## NativeRawDevelopmentInput
35
43
 
36
44
  The exact contract is declared below; use the domain guide for assumptions and staged integration.
@@ -56,6 +64,14 @@ export interface NativeRawDevelopmentInput {
56
64
  * always true for a 2×2 Bayer pattern. Interior pixels are unchanged.
57
65
  */
58
66
  borderHandling?: RawReconstructionBorderHandling;
67
+ /**
68
+ * Development exposure push in stops (#556), from {@link NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS} −2 to +5; default 0.
69
+ * Each site's black-subtracted, white-normalized value is multiplied by 2^stops before demosaic, white balance, the
70
+ * highlight mode, colour and the tone curve, like a raw editor's exposure slider. A site whose pushed value reaches the
71
+ * white level is clipped there and counted as saturated (so `highlightMode: "reconstruct"` rolls it off); values below
72
+ * black stay signed. The RAW codes are not changed. 0 (or omitted) develops exactly as before, byte for byte.
73
+ */
74
+ exposureCompensationStops?: number;
59
75
  /** Counts the kept packed RAW plus the integer output. It does not count caller buffers, process heap or GPU memory. */
60
76
  maximumRetainedPayloadBytes: number;
61
77
  }
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.15.0" as const
14
+ ENGINE_API_VERSION = "2.17.0" as const
15
15
  ```
@@ -0,0 +1,281 @@
1
+ # equipment/generic-lens-catalogue.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## GENERIC_LENS_CATALOGUE
6
+
7
+ The 30 lenses: 17 professional, then 13 consumer, each set by focal length.
8
+
9
+ ```ts
10
+ GENERIC_LENS_CATALOGUE: readonly GenericLensCatalogueEntry[] = Object.freeze([...PRO.map((row) => entry("professional", row)),
11
+ ...CONSUMER.map((row) => entry("consumer", row))])
12
+ ```
13
+
14
+ ## GENERIC_LENS_IDS
15
+
16
+ The catalogue ids, in catalogue order.
17
+
18
+ ```ts
19
+ GENERIC_LENS_IDS: readonly string[] = Object.freeze(GENERIC_LENS_CATALOGUE.map((lens) => lens.lensId))
20
+ ```
21
+
22
+ ## GENERIC_LENS_SET_BASELINES
23
+
24
+ The set baselines (owner's table, #549), each lens's starting point before its adjustments.
25
+
26
+ ```ts
27
+ GENERIC_LENS_SET_BASELINES: Readonly<Record<GenericLensSet, Readonly<GenericLensCharacterValues>>> = Object.freeze({
28
+ professional: Object.freeze({ sphericalWaves: 0.25, comaWaves: 0.35, astigmatismWaves: 0.3, fieldCurvatureWaves: 0.15, axialColorWaves: 0.15,
29
+ lateralColorFraction: 0.00008, distortionMagnitude: 0.01, naturalVignettingStops: 0.4, opticalVignettingStops: 0.6, veilingGlareFraction: 0.005, ghostFraction: 0.002,
30
+ breathingFraction: -0.03, focusScatterDepthOfFocusFraction: 0.1, closeFocusFactor: 0.15 }),
31
+ consumer: Object.freeze({ sphericalWaves: 0.5, comaWaves: 1.0, astigmatismWaves: 0.8, fieldCurvatureWaves: 0.45, axialColorWaves: 0.4,
32
+ lateralColorFraction: 0.0003, distortionMagnitude: 0.03, naturalVignettingStops: 0.7, opticalVignettingStops: 1.1, veilingGlareFraction: 0.02, ghostFraction: 0.008,
33
+ breathingFraction: -0.08, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 })
34
+ })
35
+ ```
36
+
37
+ ## GenericLensAdjustments
38
+
39
+ Per-lens multipliers of the set baseline (missing: 1). `distortion` is signed and required: −1 barrel, +1 pincushion at the baseline.
40
+
41
+ ```ts
42
+ export type GenericLensAdjustments = { distortion: number } & Partial<Record<"spherical" | "coma" | "astigmatism" | "fieldCurvature" | "axialColor" | "lateralColor" |
43
+ "vignetting" | "strayLight" | "breathing" | "focusScatter" | "closeFocus", number>>;
44
+ ```
45
+
46
+ ## GenericLensCatalogueEntry
47
+
48
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
49
+
50
+ ```ts
51
+ export interface GenericLensCatalogueEntry {
52
+ /** Catalogue id, for example "pv-50-1.2-pro". */
53
+ lensId: string;
54
+ /** Display name, for example "PV 50 1.2 Pro". */
55
+ name: string;
56
+ set: GenericLensSet;
57
+ kind: GenericLensKind;
58
+ /** Full-frame-equivalent focal length (36 × 24 mm reference). */
59
+ equivalentFocalLengthMm: number;
60
+ widestFNumber: number;
61
+ narrowestFNumber: number;
62
+ /** Largest magnification: 0.15 (the generic rule) or 1 for the professional macro. */
63
+ maximumMagnification: number;
64
+ projection: LensProjectionModel;
65
+ /** Fisheyes only: a circular image inside the frame, or one that fills it. */
66
+ fisheyeImage?: "circular" | "full-frame";
67
+ adjustments: Readonly<GenericLensAdjustments>;
68
+ /** Why this lens departs from its set's baseline. */
69
+ note: string;
70
+ /**
71
+ * A short plain-English summary of the lens's character (at most 40 characters, US spelling), written from its adjustments and
72
+ * `note`, for lists and pickers. It describes only what the catalogue models (bokeh shape is not modeled: the defocus is a disc).
73
+ */
74
+ summary: string;
75
+ }
76
+ ```
77
+
78
+ ## GenericLensCharacter
79
+
80
+ A catalogue lens's character resolved for a sensor, an f-number and a focus distance. Aberrations in waves at 550 nm.
81
+
82
+ ```ts
83
+ export interface GenericLensCharacter {
84
+ fNumber: number;
85
+ focusDistanceM: number | null;
86
+ /** Thin-lens magnification at the focus distance (0 at infinity). */
87
+ magnification: number;
88
+ /** 1 + c·m/m_max. */
89
+ closeFocusGain: number;
90
+ /** Pupil-radius ratio N0/N. */
91
+ pupilScale: number;
92
+ /** Resolved Seidel coefficients at the frame corner (H = 1), waves at 550 nm; `balancingDefocusWaves` = −spherical. */
93
+ aberrations: { sphericalWaves: number; balancingDefocusWaves: number; comaWaves: number; astigmatismWaves: number; fieldCurvatureWaves: number; axialColorWaves: number };
94
+ /** Lateral colour: F-to-C image-radius difference at the corner, fraction of the half-diagonal and micrometres on this sensor. */
95
+ lateralColor: { fraction: number; cornerMicrometers: number };
96
+ /** Radial distortion profile (normalised to the half-diagonal, valid to 1.25 of it, beyond the corner) and its corner value (fraction; negative = barrel). */
97
+ distortion: { profile: RadialDistortionProfile; corner: number };
98
+ /** Relative illumination profile (normalised to the half-diagonal, valid to 1.25 of it) and the corner fall-off in stops. */
99
+ vignetting: { profile: IlluminationVignettingProfile; cornerStops: number };
100
+ strayLight: { veilingGlareFraction: number; ghostFraction: number };
101
+ /** Field-of-view change at this focus and at minimum focus, beyond thin-lens unit focusing (fraction, negative = narrower). */
102
+ breathing: { fieldOfViewChange: number; atMinimumFocus: number };
103
+ /** Standard deviation of the autofocus error as a fraction of the depth of focus 2·N·c. */
104
+ focusScatterDepthOfFocusFraction: number;
105
+ }
106
+ ```
107
+
108
+ ## GenericLensCharacterValues
109
+
110
+ The character of a set, at each lens's widest aperture. Aberrations are in waves at 550 nm at the frame corner.
111
+
112
+ ```ts
113
+ export interface GenericLensCharacterValues {
114
+ /** Spherical aberration W040 (balanced by W020 = −W040): centre sharpness. */
115
+ sphericalWaves: number;
116
+ /** Coma W131 at H = 1: the comet-shaped point images of night photography. */
117
+ comaWaves: number;
118
+ /** Astigmatism W222 at H = 1: edge and corner sharpness. */
119
+ astigmatismWaves: number;
120
+ /** Field curvature W220 at H = 1: edge and corner sharpness. */
121
+ fieldCurvatureWaves: number;
122
+ /** Axial colour: the F-to-C difference of the defocus W020, waves at 550 nm. */
123
+ axialColorWaves: number;
124
+ /** Lateral colour: the F-to-C difference of the image radius at the corner, as a fraction of the half-diagonal. */
125
+ lateralColorFraction: number;
126
+ /** Corner distortion magnitude |k1| (fraction); each lens gives the sign (barrel −, pincushion +). */
127
+ distortionMagnitude: number;
128
+ /** Corner fall-off in stops that stays when stopped down (natural vignetting). */
129
+ naturalVignettingStops: number;
130
+ /** Extra corner fall-off in stops at the widest aperture (optical vignetting); gone two stops down. */
131
+ opticalVignettingStops: number;
132
+ /** Veiling glare: the fraction of the image's mean irradiance spread as a uniform veil. */
133
+ veilingGlareFraction: number;
134
+ /** Peak ghost power as a fraction of a bright source's power on the axis. */
135
+ ghostFraction: number;
136
+ /** Field-of-view change between infinity and minimum focus as a fraction (negative: narrower), beyond thin-lens unit focusing. */
137
+ breathingFraction: number;
138
+ /** Standard deviation of the autofocus error as a fraction of the depth of focus 2·N·c. */
139
+ focusScatterDepthOfFocusFraction: number;
140
+ /** c in the close-focus factor 1 + c·m/m_max. */
141
+ closeFocusFactor: number;
142
+ }
143
+ ```
144
+
145
+ ## GenericLensFocusError
146
+
147
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
148
+
149
+ ```ts
150
+ export interface GenericLensFocusError {
151
+ lensId: string;
152
+ /** The intended and the sampled focus distance, m (the sampled one held within minimum focus and a far limit). */
153
+ intendedFocusDistanceM: number;
154
+ focusDistanceM: number;
155
+ /** The standard normal draw z and the image-side focus error z·σ·2Nc, µm (positive: a longer image distance, so focused nearer). */
156
+ standardNormal: number;
157
+ imageSideErrorMicrometers: number;
158
+ /** σ (fraction of the depth of focus) and the depth of focus 2·N·c, µm. */
159
+ depthOfFocusFraction: number;
160
+ depthOfFocusMicrometers: number;
161
+ /** True when the sampled image distance was held at the minimum-focus or the far (1000 m) limit. */
162
+ held: "minimum-focus" | "far-limit" | null;
163
+ scientificStatus: "approximation";
164
+ }
165
+ ```
166
+
167
+ ## GenericLensKind
168
+
169
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
170
+
171
+ ```ts
172
+ export type GenericLensKind = "standard" | "macro" | "fisheye";
173
+ ```
174
+
175
+ ## GenericLensSet
176
+
177
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
178
+
179
+ ```ts
180
+ export type GenericLensSet = "professional" | "consumer";
181
+ ```
182
+
183
+ ## ResolvedGenericLens
184
+
185
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
186
+
187
+ ```ts
188
+ export interface ResolvedGenericLens {
189
+ lensId: string;
190
+ name: string;
191
+ set: GenericLensSet;
192
+ kind: GenericLensKind;
193
+ equivalentFocalLengthMm: number;
194
+ /** Physical focal length on this sensor: equivalent ÷ crop factor. */
195
+ focalLengthMm: number;
196
+ cropFactor35Mm: number;
197
+ /** Half-diagonal of the sensor, mm: field height H = 1 and ρ = 1 of the profiles. */
198
+ halfDiagonalMm: number;
199
+ widestFNumber: number;
200
+ narrowestFNumber: number;
201
+ /** Marked third-stop f-numbers within the range. */
202
+ fNumbers: readonly number[];
203
+ /** max(0.10 m, f·(1 + 1/m_max)), metres from the lens (thin lens). */
204
+ minimumFocusDistanceM: number;
205
+ maximumMagnification: number;
206
+ projection: LensProjectionModel;
207
+ fisheyeImage: "circular" | "full-frame" | null;
208
+ /** Fisheyes: the radius of the 180° image circle on this sensor, 2·f·sin 45° (equisolid), mm. Null otherwise. */
209
+ imageCircleRadiusMm: number | null;
210
+ character: GenericLensCharacter;
211
+ note: string;
212
+ /** The catalogue entry's `summary`: a short plain-English description of the lens's character (at most 40 characters). */
213
+ summary: string;
214
+ evidence: readonly EvidenceProvenance[];
215
+ scientificStatus: "approximation";
216
+ limitations: readonly string[];
217
+ }
218
+ ```
219
+
220
+ ## resolveGenericLens
221
+
222
+ Resolves a catalogue lens for a sensor: its focal length (equivalent ÷ the diagonal crop factor), f-number range, close focus
223
+ and projection, and its character (aberrations, colour, distortion, vignetting, flare, breathing and focus scatter) at the
224
+ given f-number and focus distance. Every value is a labelled approximation of a generic lens, not a measured product.
225
+
226
+ ```ts
227
+ export function resolveGenericLens(input: ResolveGenericLensInput): ResolvedGenericLens;
228
+ ```
229
+
230
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
231
+
232
+ ## ResolveGenericLensInput
233
+
234
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
235
+
236
+ ```ts
237
+ export interface ResolveGenericLensInput {
238
+ /** A catalogue id (`GENERIC_LENS_IDS`). */
239
+ lensId: string;
240
+ /** The sensor's active capture area, mm. */
241
+ activeImagingArea: { widthMm: number; heightMm: number };
242
+ /** Default: the widest aperture. Must lie in the lens's range. */
243
+ fNumber?: number;
244
+ /** Default: infinity. At least the minimum focus distance. */
245
+ focusDistanceM?: number;
246
+ }
247
+ ```
248
+
249
+ ## simulateGenericLensFocusError
250
+
251
+ Focus consistency (#549): a seeded, reproducible autofocus error for one shot. The image-side error is z·σ·2Nc, with z a
252
+ standard normal draw from (seed, shotIndex) (MurmurHash3 stream seed, xorshift32 and Box–Muller, the generators of the
253
+ capture's sensor noise), σ the lens's focus scatter as a fraction of the depth of focus 2·N·c, and
254
+ c the circle of confusion. The thin-lens image distance of the intended focus moves by that error and the focus distance is
255
+ read back from it. An approximation of autofocus variation; off unless called (or `simulateCapture`'s `lens.focusScatter`).
256
+
257
+ ```ts
258
+ export function simulateGenericLensFocusError(input: SimulateGenericLensFocusErrorInput): GenericLensFocusError;
259
+ ```
260
+
261
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
262
+
263
+ ## SimulateGenericLensFocusErrorInput
264
+
265
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
266
+
267
+ ```ts
268
+ export interface SimulateGenericLensFocusErrorInput {
269
+ lensId: string;
270
+ activeImagingArea: { widthMm: number; heightMm: number };
271
+ fNumber: number;
272
+ /** The intended focus distance, m. */
273
+ focusDistanceM: number;
274
+ /** uint32 seed of the shooting session. */
275
+ seed: number;
276
+ /** uint32 index of the shot in the session. Default 0. */
277
+ shotIndex?: number;
278
+ /** Circle of confusion for the depth of focus, mm. Default: the sensor diagonal / 1500. */
279
+ circleOfConfusionMm?: number;
280
+ }
281
+ ```