@photivra/engine 2.16.0 → 2.18.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 (107) hide show
  1. package/CHANGELOG.md +104 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +24 -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/camera/sensor-equivalence.d.ts +156 -0
  10. package/dist/camera/sensor-equivalence.js +208 -0
  11. package/dist/camera/sensor-equivalence.js.map +1 -0
  12. package/dist/capture/native-environment-tabulated.d.ts +5 -0
  13. package/dist/capture/native-environment-tabulated.js +1 -1
  14. package/dist/capture/native-environment-tabulated.js.map +1 -1
  15. package/dist/core/version.d.ts +1 -1
  16. package/dist/core/version.js +1 -1
  17. package/dist/core/version.js.map +1 -1
  18. package/dist/equipment/generic-lens-catalogue.d.ts +227 -0
  19. package/dist/equipment/generic-lens-catalogue.js +243 -0
  20. package/dist/equipment/generic-lens-catalogue.js.map +1 -0
  21. package/dist/equipment/ideal-lens.d.ts +25 -0
  22. package/dist/equipment/ideal-lens.js +21 -0
  23. package/dist/equipment/ideal-lens.js.map +1 -0
  24. package/dist/facade/catalogue-lens-signal.d.ts +94 -0
  25. package/dist/facade/catalogue-lens-signal.js +362 -0
  26. package/dist/facade/catalogue-lens-signal.js.map +1 -0
  27. package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
  28. package/dist/facade/defocus-bokeh-zones.js +53 -0
  29. package/dist/facade/defocus-bokeh-zones.js.map +1 -0
  30. package/dist/facade/defocus-image-space-provider.d.ts +9 -0
  31. package/dist/facade/defocus-image-space-provider.js +101 -9
  32. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  33. package/dist/facade/diffraction-provider.d.ts +29 -0
  34. package/dist/facade/diffraction-provider.js +117 -11
  35. package/dist/facade/diffraction-provider.js.map +1 -1
  36. package/dist/facade/display-view-gpu-session.js +4 -0
  37. package/dist/facade/display-view-gpu-session.js.map +1 -1
  38. package/dist/facade/display-view-gpu.d.ts +10 -0
  39. package/dist/facade/display-view-gpu.js.map +1 -1
  40. package/dist/facade/display-view-lens-signal.d.ts +61 -0
  41. package/dist/facade/display-view-lens-signal.js +502 -0
  42. package/dist/facade/display-view-lens-signal.js.map +1 -0
  43. package/dist/facade/expected-signal-setup.d.ts +15 -0
  44. package/dist/facade/expected-signal-setup.js +19 -2
  45. package/dist/facade/expected-signal-setup.js.map +1 -1
  46. package/dist/facade/expected-signal.d.ts +13 -0
  47. package/dist/facade/expected-signal.js +155 -1
  48. package/dist/facade/expected-signal.js.map +1 -1
  49. package/dist/facade/generic-lens-capture.d.ts +7 -0
  50. package/dist/facade/generic-lens-capture.js +9 -0
  51. package/dist/facade/generic-lens-capture.js.map +1 -0
  52. package/dist/facade/large-defocus-lattice.d.ts +39 -0
  53. package/dist/facade/large-defocus-lattice.js +64 -0
  54. package/dist/facade/large-defocus-lattice.js.map +1 -0
  55. package/dist/facade/scene-radiance.d.ts +5 -0
  56. package/dist/facade/scene-radiance.js +5 -2
  57. package/dist/facade/scene-radiance.js.map +1 -1
  58. package/dist/facade/simulate-capture.d.ts +67 -7
  59. package/dist/facade/simulate-capture.js +400 -64
  60. package/dist/facade/simulate-capture.js.map +1 -1
  61. package/dist/facade/simulate-display-view.js +23 -17
  62. package/dist/facade/simulate-display-view.js.map +1 -1
  63. package/dist/gpu/display-view-quality-gpu.d.ts +0 -9
  64. package/dist/gpu/display-view-quality-gpu.js +6 -0
  65. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  66. package/dist/gpu/display-view-speed-gpu.js +2 -0
  67. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  68. package/dist/index.d.ts +6 -1
  69. package/dist/index.js +5 -0
  70. package/dist/index.js.map +1 -1
  71. package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
  72. package/dist/optics/defocus-bokeh-kernel.js +380 -0
  73. package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
  74. package/dist/optics/defocus-disc-kernel.d.ts +41 -19
  75. package/dist/optics/defocus-disc-kernel.js +44 -23
  76. package/dist/optics/defocus-disc-kernel.js.map +1 -1
  77. package/dist/optics/generic-lens-psf.d.ts +68 -0
  78. package/dist/optics/generic-lens-psf.js +337 -0
  79. package/dist/optics/generic-lens-psf.js.map +1 -0
  80. package/dist/optics/illumination-vignetting.d.ts +6 -0
  81. package/dist/optics/illumination-vignetting.js +24 -7
  82. package/dist/optics/illumination-vignetting.js.map +1 -1
  83. package/dist/optics/sensor-environment-query.d.ts +6 -0
  84. package/dist/optics/sensor-environment-query.js +13 -6
  85. package/dist/optics/sensor-environment-query.js.map +1 -1
  86. package/dist/optics/site-kernel-runs.d.ts +77 -0
  87. package/dist/optics/site-kernel-runs.js +200 -0
  88. package/dist/optics/site-kernel-runs.js.map +1 -0
  89. package/dist/output/generic-lens-corrections.d.ts +24 -0
  90. package/dist/output/generic-lens-corrections.js +51 -0
  91. package/dist/output/generic-lens-corrections.js.map +1 -0
  92. package/docs/AGENT_GUIDE.md +4 -0
  93. package/docs/API_REFERENCE.md +77 -1
  94. package/docs/QUICK_START.md +305 -17
  95. package/docs/USAGE.md +98 -5
  96. package/docs/api/camera-sensor-equivalence.md +209 -0
  97. package/docs/api/core-version.md +1 -1
  98. package/docs/api/equipment-generic-lens-catalogue.md +281 -0
  99. package/docs/api/equipment-ideal-lens.md +26 -0
  100. package/docs/api/exports.json +202 -2
  101. package/docs/api/facade-simulate-capture.md +48 -4
  102. package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
  103. package/docs/api/optics-defocus-disc-kernel.md +22 -15
  104. package/docs/api/optics-sensor-environment-query.md +6 -0
  105. package/docs/api/output-generic-lens-corrections.md +39 -0
  106. package/llms.txt +3 -0
  107. package/package.json +1 -1
package/docs/USAGE.md CHANGED
@@ -374,6 +374,8 @@ The actual focal length stays the physical optical quantity used by projection a
374
374
  - Focus distance is not part of conventional 35 mm-equivalent focal length. A later digital/output crop and the output resolution are also excluded.
375
375
  - If a product wants to describe the final digital framing, it should label it as output framing. It should not silently replace the optical focal length or the capture-equivalent focal length.
376
376
 
377
+ To render a lens by its physical focal length on any sensor, give `simulateCapture()` or the display view `lens.ideal: { focalLengthMm }` with any `lens.fNumber` within `IDEAL_LENS_LIMITS` (f/0.5–f/128, 1–4000 mm; optional `widestFNumber` and `narrowestFNumber` narrow the f-number range for the automatic exposure modes): an aberration-free thin lens that is not scaled by the crop factor, unlike `lens.prime` (see docs/QUICK_START.md, "Ideal lens at any focal length and f-number").
378
+
377
379
  `calculateImagingAreaMetrics()` provides the same canonical diagonal crop-factor calculation, independent of raster density, so other engine modules do not need to repeat that formula.
378
380
 
379
381
  ## Focus-breathing projection
@@ -1455,7 +1457,7 @@ console.log(blur.value.diameterMm);
1455
1457
 
1456
1458
  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
1459
 
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:
1460
+ `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
1461
 
1460
1462
  ```ts
1461
1463
  import { calculateDefocusDiscKernelRadius } from "@photivra/engine";
@@ -1463,13 +1465,14 @@ import { calculateDefocusDiscKernelRadius } from "@photivra/engine";
1463
1465
  const check = calculateDefocusDiscKernelRadius({
1464
1466
  focalLengthMm: 50,
1465
1467
  aperture: 1.4,
1466
- focusDistanceM: 1,
1468
+ focusDistanceM: 0.25,
1467
1469
  subjectDistanceM: 2,
1468
1470
  pixelPitchMicrometers: 6
1469
1471
  });
1470
1472
 
1471
- console.log(check.value.withinLimit); // false: lens.defocus refuses this request
1472
- console.log(check.value.kernelRadiusSites, check.value.excessKernelRadiusSites); // 78, 14
1473
+ console.log(check.value.withinLimit); // false: lens.defocus refuses this request (a 7.81 mm blur, 1,303 sites across)
1474
+ console.log(check.value.kernelRadiusSites, check.value.excessKernelRadiusSites); // 651, 139
1475
+ // Focused at 1 m instead: 157 sites across, radius 78, withinLimit true, convolution "runs".
1473
1476
  ```
1474
1477
 
1475
1478
  See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#thin-lens-focus-and-depth-of-field).
@@ -7470,6 +7473,29 @@ console.log(calculateGeometricFocusBreathing({
7470
7473
  }).value.deltaDegrees);
7471
7474
  ```
7472
7475
 
7476
+ ## Generic lens catalogue (#549)
7477
+
7478
+ `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
7479
+ own designs, not real products). `resolveGenericLens` resolves one for a sensor, an f-number and a focus distance: its focal length
7480
+ (scaled by the crop factor), aperture range, close focus, projection and its character, Seidel aberrations, axial and lateral
7481
+ colour, distortion and vignetting profiles, flare, breathing and focus scatter, each a labelled approximation.
7482
+ `simulateGenericLensFocusError` draws a seeded autofocus error; `createGenericLensCorrectionProfile` builds the lens's generic
7483
+ correction profile for `resolveLensCorrectionPlan`. `simulateCapture` renders a catalogue lens with `lens.catalogue`
7484
+ ([quick start](QUICK_START.md#generic-lens-catalogue-549)); `fast: true` (or `lens: { catalogue: { kind, fast: true } }`) takes its
7485
+ fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures. Equations, tables and the
7486
+ chart measurements: [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7487
+
7488
+ ```ts
7489
+ import { GENERIC_LENS_CATALOGUE, resolveGenericLens, simulateGenericLensFocusError } from "@photivra/engine";
7490
+
7491
+ const frame = { widthMm: 36, heightMm: 24 };
7492
+ const pro = resolveGenericLens({ lensId: "pv-50-1.2-pro", activeImagingArea: frame, fNumber: 2 });
7493
+ const consumer = resolveGenericLens({ lensId: "pv-50-1.8", activeImagingArea: frame, fNumber: 2 });
7494
+ console.log(GENERIC_LENS_CATALOGUE.length, pro.character.aberrations.comaWaves < consumer.character.aberrations.comaWaves); // 30 true
7495
+ const shot = simulateGenericLensFocusError({ lensId: "pv-50-1.8", activeImagingArea: frame, fNumber: 1.8, focusDistanceM: 2, seed: 1, shotIndex: 0 });
7496
+ console.log(shot.focusDistanceM, shot.imageSideErrorMicrometers);
7497
+ ```
7498
+
7473
7499
  ## Parallel native environment RAW (row-band partitions, 2.0)
7474
7500
 
7475
7501
  `createNativeEnvironmentRawTask` runs on one thread. For several cores, split the capture into row bands with
@@ -7814,6 +7840,20 @@ region that have a clipped contributing site. `simulateCapture` counts the same
7814
7840
  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
7815
7841
  [Highlight count](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#highlight-count-533).
7816
7842
 
7843
+ From #563 the view renders `lens.projection`, `lens.distortion`, `lens.vignetting` and camera rotation (global
7844
+ shutter), in focus and with `lens.defocus` out of focus, with each real site's expected electrons as the capture's. These
7845
+ requests run on the CPU (`gpu: "auto"` falls back labelled `unsupported-on-backend`); rotation with a rolling shutter is
7846
+ 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).
7847
+
7848
+ ```ts
7849
+ import { simulateDisplayView } from "@photivra/engine";
7850
+
7851
+ const fisheye = await simulateDisplayView({ scene: { preset: "siemens-star" }, sensor: { pixelPitchMicrometers: 3 }, output: { width: 120, height: 80 },
7852
+ lens: { projection: "fisheye-equisolid-angle", vignetting: { normalizationRadiusMm: 0.25, maximumNormalizedRadius: 1, coefficients: { r2: -0.4, r4: 0, r6: 0 } } } },
7853
+ { width: 30, height: 20 });
7854
+ console.log(fisheye.kind, fisheye.assumptions.find((a) => a.id === "lens.vignetting")?.kind); // "display-view" "model"
7855
+ ```
7856
+
7817
7857
  #### Rendering a prepared view on the GPU (2.11, #481)
7818
7858
 
7819
7859
  `prepareDisplayView(request, { gpu })` chooses the renderer: `"off"` (the default) is the CPU reference, `"auto"` the GPU
@@ -7923,7 +7963,7 @@ console.log(barrel.request.siteInput(0).environment.lensFieldMapping?.distortion
7923
7963
  lattice, and the capture convolves the scene radiance with it, which is a geometric approximation. With
7924
7964
  `lens.diffraction`, each spectral node's diffraction kernel is convolved with the disc, which is a further
7925
7965
  approximation. The `lens.defocus` assumption records the diameter in µm and in sites, the kernel radius and the
7926
- execution. `simulateDisplayView` does not render it yet. See [Optics](QUICK_START.md#optics).
7966
+ execution. `simulateDisplayView` renders it too, also with projection, distortion, vignetting and rotation (#563). See [Optics](QUICK_START.md#optics).
7927
7967
 
7928
7968
  ```ts
7929
7969
  import { simulateCapture } from "@photivra/engine";
@@ -7937,6 +7977,21 @@ const blurred = await simulateCapture({
7937
7977
  console.log(blurred.assumptions.find((a) => a.id === "lens.defocus")?.value); // diameterSites ≈ 5.95, kernelRadiusSites 3
7938
7978
  ```
7939
7979
 
7980
+ Bokeh character (#553): `lens.defocus` also takes `blades` (a polygonal or rounded-blade opening), `sphericalAberration`
7981
+ (primary W040: a soft-edged blur on one side of the focus plane and a bright-rimmed one on the other) and `catsEye`
7982
+ (mechanical vignetting by field position; `simulateCapture` only). Each is off by default. `calculateDefocusBokehKernel`
7983
+ returns the kernel without rendering. See [Bokeh character](QUICK_START.md#bokeh-character-553).
7984
+
7985
+ ```ts
7986
+ import { calculateDefocusBokehKernel } from "@photivra/engine";
7987
+
7988
+ const kernel = calculateDefocusBokehKernel({
7989
+ focalLengthMm: 50, aperture: 4, focusDistanceM: 1.4, subjectDistanceM: 2, pixelPitchMicrometers: 6,
7990
+ bokeh: { blades: { count: 5 }, sphericalAberration: { w040Micrometers: 0.5, atFNumber: 4 } }
7991
+ });
7992
+ console.log(kernel.value.side, kernel.value.kernelRadiusSites, kernel.value.method); // "behind-focus", 17, "pupil-ray-binning"
7993
+ ```
7994
+
7940
7995
  `motion.angularVelocityRadPerSec` rotates the camera during the exposure (8 time midpoints by
7941
7996
  default). `motion.shutter: "rolling"` derives each site's exposure window from the body's rolling
7942
7997
  readout, with `resolveElectronicRollingShutterExposureSchedule`. See [Motion](QUICK_START.md#motion).
@@ -7947,3 +8002,41 @@ import { simulateCapture } from "@photivra/engine";
7947
8002
  const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motion: { angularVelocityRadPerSec: { pitch: 0.02 }, shutter: "rolling" }, output: { width: 6, height: 6 } });
7948
8003
  console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
7949
8004
  ```
8005
+
8006
+ ## Equivalent f-number, sensor area and blur as a share of the frame
8007
+
8008
+ `calculateSensorEquivalence()` says what a lens on one sensor corresponds to on a 36 × 24 mm frame (or on a `reference` you pass). Call it once per sensor to fill a lens table:
8009
+
8010
+ ```ts
8011
+ import { calculateBlurShareOfFrame, calculateSensorEquivalence } from "@photivra/engine";
8012
+
8013
+ const apsC = calculateSensorEquivalence({
8014
+ sensor: { widthMm: 23.5, heightMm: 15.6 },
8015
+ focalLengthMm: 50,
8016
+ fNumber: 2
8017
+ }).value;
8018
+
8019
+ console.log(apsC.cropFactor); // ≈ 1.53
8020
+ console.log(apsC.equivalentFocalLengthMm); // ≈ 76.7 (framing)
8021
+ console.log(apsC.equivalentFNumber); // ≈ 3.07 (depth of field, blur and total light; not exposure)
8022
+ console.log(apsC.exposureFNumber); // 2 (exposure does not change with sensor size)
8023
+ console.log(apsC.areaRatio); // ≈ 0.42 of the full-frame area
8024
+ console.log(apsC.totalLightDifferenceStops); // ≈ -1.24 stops at the same f-number and shutter
8025
+ console.log(apsC.basis.equivalentFNumber.kind); // "approximation"
8026
+
8027
+ const share = calculateBlurShareOfFrame({
8028
+ blurCircleMm: 0.03,
8029
+ sensor: { widthMm: 36, heightMm: 24 },
8030
+ print: { widthMm: 300, heightMm: 200 }
8031
+ }).value;
8032
+
8033
+ console.log(share.shareOfDiagonal); // ≈ 0.00069 of the frame diagonal
8034
+ console.log(share.print?.blurOnPrintMm); // 0.25 mm on the print
8035
+ ```
8036
+
8037
+ - `basis` gives, for every number, `kind: "exact"` or `"approximation"` and the conditions. Crop factor, equivalent focal length, area ratio, total-light difference, entrance pupil and angles of view are exact under their stated conditions.
8038
+ - The equivalent f-number (crop factor × f-number) assumes the same framing, subject distance and output size. It is a thin-lens approximation that is exact only when the focus distance is much greater than the focal length; at close focus the blur shares match at `k·N·(s − f)/(s − k·f)`.
8039
+ - Exposure uses the f-number on the lens. Equivalence never changes shutter, ISO or exposure.
8040
+ - Equal shares of the frame diagonal look the same when images are shown at the same size. `calculateBlurShareOfFrame()` does not judge whether a blur is visible.
8041
+
8042
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-equivalence-f-number-area-and-blur-share).
@@ -0,0 +1,209 @@
1
+ # camera/sensor-equivalence.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
+ ## BlurShareOfFrame
6
+
7
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
8
+
9
+ ```ts
10
+ export interface BlurShareOfFrame {
11
+ /** Blur-circle diameter on the sensor in millimetres. */
12
+ blurCircleMm: number;
13
+ /** Sensor diagonal in millimetres. */
14
+ frameDiagonalMm: number;
15
+ /** Blur diameter / frame diagonal. Equal shares look the same at the same output size. */
16
+ shareOfDiagonal: number;
17
+ /** Blur diameter / frame width. */
18
+ shareOfWidth: number;
19
+ /** Blur diameter / frame height. */
20
+ shareOfHeight: number;
21
+ /** Blur on the print; present only when a print size was given. */
22
+ print?: BlurShareOfPrint;
23
+ }
24
+ ```
25
+
26
+ ## BlurShareOfPrint
27
+
28
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
29
+
30
+ ```ts
31
+ export interface BlurShareOfPrint {
32
+ /** Linear enlargement from sensor to print: the smaller of the long-side and short-side ratios. */
33
+ enlargement: number;
34
+ /** Blur-circle diameter on the print in millimetres: blurCircleMm × enlargement. */
35
+ blurOnPrintMm: number;
36
+ /** Size of the printed image in millimetres (equal to the print when the aspect ratios match). */
37
+ imageLongSideMm: number;
38
+ imageShortSideMm: number;
39
+ }
40
+ ```
41
+
42
+ ## calculateBlurShareOfFrame
43
+
44
+ A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a
45
+ print size is given, its diameter on that print.
46
+
47
+ Equal shares of the diagonal look the same when the images are shown at the same size, which
48
+ is how "same framing" comparisons across sensor sizes are stated. Every value is an exact ratio
49
+ or scaling; whether a blur is visible depends on viewing distance and acuity, which this
50
+ function does not judge.
51
+
52
+ ```ts
53
+ export function calculateBlurShareOfFrame(
54
+ input: CalculateBlurShareOfFrameInput
55
+ ): CalculationResult<BlurShareOfFrame>;
56
+ ```
57
+
58
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
59
+
60
+ ## CalculateBlurShareOfFrameInput
61
+
62
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
63
+
64
+ ```ts
65
+ export interface CalculateBlurShareOfFrameInput {
66
+ /** Blur-circle diameter on the sensor in millimetres (for example from calculateDefocusCircle). Zero is allowed. */
67
+ blurCircleMm: number;
68
+ /** Physical active imaging area of the sensor. */
69
+ sensor: SensorImagingArea;
70
+ /**
71
+ * Optional print size. The whole frame is enlarged to fit inside it without cropping, long side
72
+ * to long side.
73
+ */
74
+ print?: { widthMm: number; heightMm: number };
75
+ }
76
+ ```
77
+
78
+ ## calculateSensorEquivalence
79
+
80
+ Equivalence of a lens (focal length, f-number) on one sensor against a reference format,
81
+ 36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of
82
+ field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light
83
+ difference in stops and the angles of view.
84
+
85
+ Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it
86
+ is exact under its stated conditions or an approximation. The equivalent f-number is an
87
+ approximation (exact when the focus distance is much greater than the focal length) and never an
88
+ exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.
89
+
90
+ Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.
91
+
92
+ ```ts
93
+ export function calculateSensorEquivalence(
94
+ input: CalculateSensorEquivalenceInput
95
+ ): CalculationResult<SensorEquivalence>;
96
+ ```
97
+
98
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
99
+
100
+ ## CalculateSensorEquivalenceInput
101
+
102
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
103
+
104
+ ```ts
105
+ export interface CalculateSensorEquivalenceInput {
106
+ /** Physical active imaging area of the sensor the lens is used on. */
107
+ sensor: SensorImagingArea;
108
+ /** Physical focal length of the lens in millimetres. */
109
+ focalLengthMm: number;
110
+ /** f-number set on the lens (N = focal length / entrance-pupil diameter). */
111
+ fNumber: number;
112
+ /** Format to compare against. Defaults to the 36 × 24 mm frame. */
113
+ reference?: SensorImagingArea;
114
+ }
115
+ ```
116
+
117
+ ## EquivalenceExactness
118
+
119
+ Whether a value is exact under its stated conditions or an approximation.
120
+
121
+ ```ts
122
+ export type EquivalenceExactness = "exact" | "approximation";
123
+ ```
124
+
125
+ ## EquivalenceValueBasis
126
+
127
+ How one equivalence quantity was obtained, and under which conditions it holds.
128
+
129
+ ```ts
130
+ export interface EquivalenceValueBasis {
131
+ /** `"exact"` under the stated conditions, or `"approximation"`. */
132
+ kind: EquivalenceExactness;
133
+ /** Plain statement of the definition and its conditions. */
134
+ statement: string;
135
+ }
136
+ ```
137
+
138
+ ## SensorEquivalence
139
+
140
+ The exact contract is declared below; use the domain guide for assumptions and staged integration.
141
+
142
+ ```ts
143
+ export interface SensorEquivalence {
144
+ /** Physical focal length supplied by the caller, in millimetres. */
145
+ focalLengthMm: number;
146
+ /** f-number supplied by the caller. */
147
+ fNumber: number;
148
+ /** Sensor active imaging area used. */
149
+ sensor: SensorImagingArea;
150
+ /** Reference format used (36 × 24 mm unless the caller passed one). */
151
+ reference: SensorImagingArea;
152
+ /** Sensor diagonal in millimetres. */
153
+ sensorDiagonalMm: number;
154
+ /** Reference diagonal in millimetres. */
155
+ referenceDiagonalMm: number;
156
+ /** Diagonal crop factor: reference diagonal / sensor diagonal. Greater than 1 for a smaller sensor. */
157
+ cropFactor: number;
158
+ /** Focal length on the reference format with the same diagonal angle of view: cropFactor × focalLengthMm. */
159
+ equivalentFocalLengthMm: number;
160
+ /**
161
+ * f-number on the reference format that gives the same depth of field, background blur and total
162
+ * light for the same framing and output size: cropFactor × fNumber. Not an exposure setting.
163
+ */
164
+ equivalentFNumber: number;
165
+ /** f-number that sets exposure (image brightness): the f-number on the lens, unchanged by sensor size. */
166
+ exposureFNumber: number;
167
+ /** Entrance-pupil diameter f/N in millimetres; the equivalent lens has the same diameter. */
168
+ entrancePupilDiameterMm: number;
169
+ /** Sensor area / reference area. Less than 1 for a smaller sensor. */
170
+ areaRatio: number;
171
+ /**
172
+ * log2(areaRatio): total light collected by the sensor relative to the reference at the same
173
+ * f-number, shutter time and scene, in stops. Negative for a smaller sensor.
174
+ */
175
+ totalLightDifferenceStops: number;
176
+ /** Angles of view of this lens on the sensor (the equivalent lens gives the same diagonal angle). */
177
+ fieldOfView: SensorEquivalenceFieldOfView;
178
+ /** Exact or approximated, with the conditions, for every quantity above. */
179
+ basis: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>>;
180
+ }
181
+ ```
182
+
183
+ ## SensorEquivalenceFieldOfView
184
+
185
+ Angles of view of an ideal rectilinear lens focused at infinity, in degrees.
186
+
187
+ ```ts
188
+ export interface SensorEquivalenceFieldOfView {
189
+ horizontalDegrees: number;
190
+ verticalDegrees: number;
191
+ diagonalDegrees: number;
192
+ }
193
+ ```
194
+
195
+ ## SensorEquivalenceQuantity
196
+
197
+ Quantities of a sensor equivalence result that carry an exactness statement.
198
+
199
+ ```ts
200
+ export type SensorEquivalenceQuantity =
201
+ | "cropFactor"
202
+ | "equivalentFocalLengthMm"
203
+ | "equivalentFNumber"
204
+ | "exposureFNumber"
205
+ | "entrancePupilDiameterMm"
206
+ | "areaRatio"
207
+ | "totalLightDifferenceStops"
208
+ | "fieldOfView";
209
+ ```
@@ -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.16.0" as const
14
+ ENGINE_API_VERSION = "2.18.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
+ ```