@photivra/engine 2.20.0 → 2.22.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 (275) hide show
  1. package/CHANGELOG.md +295 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +18 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  13. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  14. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  15. package/dist/capture/dng-opcodes.d.ts +96 -0
  16. package/dist/capture/dng-opcodes.js +391 -0
  17. package/dist/capture/dng-opcodes.js.map +1 -0
  18. package/dist/capture/dng-profile-maps.d.ts +99 -0
  19. package/dist/capture/dng-profile-maps.js +381 -0
  20. package/dist/capture/dng-profile-maps.js.map +1 -0
  21. package/dist/capture/dng-stage-image.d.ts +23 -0
  22. package/dist/capture/dng-stage-image.js +65 -0
  23. package/dist/capture/dng-stage-image.js.map +1 -0
  24. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  25. package/dist/capture/exposure-build-up-stage.js +4 -0
  26. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  27. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  28. package/dist/capture/jpeg-xl-tiles.js +151 -0
  29. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  30. package/dist/capture/lossy-jpeg.d.ts +17 -0
  31. package/dist/capture/lossy-jpeg.js +369 -0
  32. package/dist/capture/lossy-jpeg.js.map +1 -0
  33. package/dist/capture/native-environment-lean.d.ts +2 -1
  34. package/dist/capture/native-environment-lean.js +41 -73
  35. package/dist/capture/native-environment-lean.js.map +1 -1
  36. package/dist/capture/native-environment-tabulated.js +4 -3
  37. package/dist/capture/native-environment-tabulated.js.map +1 -1
  38. package/dist/capture/native-raw-development.d.ts +2 -1
  39. package/dist/capture/native-raw-development.js.map +1 -1
  40. package/dist/capture/native-raw.d.ts +3 -2
  41. package/dist/capture/native-raw.js +5 -4
  42. package/dist/capture/native-raw.js.map +1 -1
  43. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  44. package/dist/capture/sensor-raw-producer.js +12 -3
  45. package/dist/capture/sensor-raw-producer.js.map +1 -1
  46. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  47. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  48. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  49. package/dist/color/sensor-color-development.d.ts +5 -0
  50. package/dist/color/sensor-color-development.js +7 -1
  51. package/dist/color/sensor-color-development.js.map +1 -1
  52. package/dist/color/spectral-spikiness.d.ts +11 -0
  53. package/dist/color/spectral-spikiness.js +23 -0
  54. package/dist/color/spectral-spikiness.js.map +1 -0
  55. package/dist/core/execution-error.d.ts +21 -0
  56. package/dist/core/execution-error.js +12 -0
  57. package/dist/core/execution-error.js.map +1 -0
  58. package/dist/core/version.d.ts +1 -1
  59. package/dist/core/version.js +1 -1
  60. package/dist/core/version.js.map +1 -1
  61. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  62. package/dist/equipment/generic-lens-catalogue.js +18 -5
  63. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  64. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  65. package/dist/equipment/generic-tier-assets.js +16 -11
  66. package/dist/equipment/generic-tier-assets.js.map +1 -1
  67. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  68. package/dist/equipment/sensor-profile-body.js +77 -0
  69. package/dist/equipment/sensor-profile-body.js.map +1 -0
  70. package/dist/equipment/sensor-profiles.d.ts +8 -0
  71. package/dist/equipment/sensor-profiles.js +82 -0
  72. package/dist/equipment/sensor-profiles.js.map +1 -0
  73. package/dist/facade/catalogue-lens-signal.d.ts +28 -3
  74. package/dist/facade/catalogue-lens-signal.js +83 -78
  75. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  76. package/dist/facade/chart-radiance-batch.js +2 -1
  77. package/dist/facade/chart-radiance-batch.js.map +1 -1
  78. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  79. package/dist/facade/defocus-image-space-provider.js +64 -76
  80. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  81. package/dist/facade/diffraction-provider.d.ts +1 -1
  82. package/dist/facade/diffraction-provider.js +4 -4
  83. package/dist/facade/diffraction-provider.js.map +1 -1
  84. package/dist/facade/display-view-gpu-session.js +39 -5
  85. package/dist/facade/display-view-gpu-session.js.map +1 -1
  86. package/dist/facade/display-view-gpu.d.ts +19 -6
  87. package/dist/facade/display-view-gpu.js.map +1 -1
  88. package/dist/facade/display-view-lens-signal.js +25 -23
  89. package/dist/facade/display-view-lens-signal.js.map +1 -1
  90. package/dist/facade/display-view-normal.d.ts +15 -0
  91. package/dist/facade/display-view-normal.js +44 -0
  92. package/dist/facade/display-view-normal.js.map +1 -0
  93. package/dist/facade/display-view-rendering.d.ts +97 -0
  94. package/dist/facade/display-view-rendering.js +598 -0
  95. package/dist/facade/display-view-rendering.js.map +1 -0
  96. package/dist/facade/display-view.d.ts +37 -65
  97. package/dist/facade/display-view.js +152 -499
  98. package/dist/facade/display-view.js.map +1 -1
  99. package/dist/facade/expected-signal-setup.d.ts +3 -1
  100. package/dist/facade/expected-signal-setup.js +3 -3
  101. package/dist/facade/expected-signal-setup.js.map +1 -1
  102. package/dist/facade/expected-signal.d.ts +47 -14
  103. package/dist/facade/expected-signal.js +117 -112
  104. package/dist/facade/expected-signal.js.map +1 -1
  105. package/dist/facade/exposure-build-up.d.ts +70 -0
  106. package/dist/facade/exposure-build-up.js +164 -0
  107. package/dist/facade/exposure-build-up.js.map +1 -0
  108. package/dist/facade/fast-methods.d.ts +1 -1
  109. package/dist/facade/fast-methods.js +46 -9
  110. package/dist/facade/fast-methods.js.map +1 -1
  111. package/dist/facade/generic-lens-capture.d.ts +3 -3
  112. package/dist/facade/generic-lens-capture.js +3 -3
  113. package/dist/facade/generic-lens-capture.js.map +1 -1
  114. package/dist/facade/image-plane-enable.d.ts +7 -0
  115. package/dist/facade/image-plane-enable.js +17 -0
  116. package/dist/facade/image-plane-enable.js.map +1 -0
  117. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  118. package/dist/facade/image-plane-lattice-data.js +10 -0
  119. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  120. package/dist/facade/image-plane-support.d.ts +33 -0
  121. package/dist/facade/image-plane-support.js +11 -0
  122. package/dist/facade/image-plane-support.js.map +1 -0
  123. package/dist/facade/image-plane.d.ts +36 -0
  124. package/dist/facade/image-plane.js +384 -0
  125. package/dist/facade/image-plane.js.map +1 -0
  126. package/dist/facade/mixed-illumination.d.ts +108 -0
  127. package/dist/facade/mixed-illumination.js +174 -0
  128. package/dist/facade/mixed-illumination.js.map +1 -0
  129. package/dist/facade/on-demand-kits.d.ts +14 -0
  130. package/dist/facade/on-demand-kits.js +36 -0
  131. package/dist/facade/on-demand-kits.js.map +1 -0
  132. package/dist/facade/opt-in-kit.d.ts +9 -0
  133. package/dist/facade/opt-in-kit.js +11 -0
  134. package/dist/facade/opt-in-kit.js.map +1 -0
  135. package/dist/facade/quality-kit.d.ts +21 -0
  136. package/dist/facade/quality-kit.js +23 -0
  137. package/dist/facade/quality-kit.js.map +1 -0
  138. package/dist/facade/quality-paths.d.ts +22 -0
  139. package/dist/facade/quality-paths.js +44 -0
  140. package/dist/facade/quality-paths.js.map +1 -0
  141. package/dist/facade/representative-light-sources.d.ts +72 -0
  142. package/dist/facade/representative-light-sources.js +124 -0
  143. package/dist/facade/representative-light-sources.js.map +1 -0
  144. package/dist/facade/scene-radiance.d.ts +145 -4
  145. package/dist/facade/scene-radiance.js +50 -13
  146. package/dist/facade/scene-radiance.js.map +1 -1
  147. package/dist/facade/simulate-capture.d.ts +68 -20
  148. package/dist/facade/simulate-capture.js +399 -146
  149. package/dist/facade/simulate-capture.js.map +1 -1
  150. package/dist/facade/simulate-display-view.d.ts +10 -12
  151. package/dist/facade/simulate-display-view.js +70 -53
  152. package/dist/facade/simulate-display-view.js.map +1 -1
  153. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  154. package/dist/gpu/chart-composite-gpu.js +48 -9
  155. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  156. package/dist/gpu/chart-radiance-gpu.js +4 -30
  157. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  158. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  159. package/dist/gpu/display-view-composite-material-module.js +39 -13
  160. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  161. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  162. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  163. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  164. package/dist/gpu/display-view-image-material-module.d.ts +87 -0
  165. package/dist/gpu/display-view-image-material-module.js +361 -0
  166. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  167. package/dist/gpu/display-view-image-quality.d.ts +87 -0
  168. package/dist/gpu/display-view-image-quality.js +625 -0
  169. package/dist/gpu/display-view-image-quality.js.map +1 -0
  170. package/dist/gpu/display-view-quality-gpu.d.ts +80 -65
  171. package/dist/gpu/display-view-quality-gpu.js +187 -445
  172. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  173. package/dist/gpu/display-view-speed-gpu.d.ts +67 -19
  174. package/dist/gpu/display-view-speed-gpu.js +181 -105
  175. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  176. package/dist/gpu/image-kernel-accelerator.js +3 -0
  177. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  178. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  179. package/dist/gpu/image-kernel-execution.js +2 -10
  180. package/dist/gpu/image-kernel-execution.js.map +1 -1
  181. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  182. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  183. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  184. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  185. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  186. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  187. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +33 -24
  188. package/dist/gpu/wgsl-display-view-quality-kernel.js +166 -146
  189. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  190. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +19 -44
  191. package/dist/gpu/wgsl-display-view-speed-kernel.js +116 -18
  192. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  193. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  194. package/dist/gpu/wgsl-exact-sum.js +42 -0
  195. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  196. package/dist/index.d.ts +22 -6
  197. package/dist/index.js +16 -2
  198. package/dist/index.js.map +1 -1
  199. package/dist/motion/subject-motion.d.ts +66 -0
  200. package/dist/motion/subject-motion.js +52 -0
  201. package/dist/motion/subject-motion.js.map +1 -0
  202. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  203. package/dist/optics/generic-lens-lateral-color.js +77 -0
  204. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  205. package/dist/optics/generic-lens-psf.js +54 -46
  206. package/dist/optics/generic-lens-psf.js.map +1 -1
  207. package/dist/optics/lens-psf-profile.d.ts +7 -0
  208. package/dist/optics/lens-psf-profile.js +2 -1
  209. package/dist/optics/lens-psf-profile.js.map +1 -1
  210. package/dist/output/camera-channel-response.d.ts +50 -0
  211. package/dist/output/camera-channel-response.js +69 -0
  212. package/dist/output/camera-channel-response.js.map +1 -0
  213. package/dist/output/developed-lens-corrections.d.ts +19 -5
  214. package/dist/output/developed-lens-corrections.js +28 -9
  215. package/dist/output/developed-lens-corrections.js.map +1 -1
  216. package/dist/output/generic-lens-corrections.d.ts +41 -5
  217. package/dist/output/generic-lens-corrections.js +52 -8
  218. package/dist/output/generic-lens-corrections.js.map +1 -1
  219. package/dist/output/native-capture-sdr.js +5 -2
  220. package/dist/output/native-capture-sdr.js.map +1 -1
  221. package/dist/output/print-plan.d.ts +41 -5
  222. package/dist/output/print-plan.js +59 -14
  223. package/dist/output/print-plan.js.map +1 -1
  224. package/dist/output/sdr-rendering.d.ts +18 -1
  225. package/dist/output/sdr-rendering.js +39 -2
  226. package/dist/output/sdr-rendering.js.map +1 -1
  227. package/dist/sensor/binomial-sample.d.ts +3 -0
  228. package/dist/sensor/binomial-sample.js +86 -0
  229. package/dist/sensor/binomial-sample.js.map +1 -0
  230. package/dist/sensor/native-sensor-report.d.ts +10 -0
  231. package/dist/sensor/native-sensor-report.js +16 -0
  232. package/dist/sensor/native-sensor-report.js.map +1 -0
  233. package/dist/sensor/sensor-characteristics.d.ts +91 -13
  234. package/dist/sensor/sensor-characteristics.js +30 -15
  235. package/dist/sensor/sensor-characteristics.js.map +1 -1
  236. package/docs/AGENT_GUIDE.md +1 -0
  237. package/docs/API_REFERENCE.md +214 -4
  238. package/docs/QUICK_START.md +162 -8
  239. package/docs/USAGE.md +195 -22
  240. package/docs/api/capture-c2pa-embed.md +177 -0
  241. package/docs/api/capture-dng-import.md +86 -10
  242. package/docs/api/capture-dng-opcodes.md +93 -0
  243. package/docs/api/capture-dng-profile-maps.md +141 -0
  244. package/docs/api/capture-dng-stage-image.md +43 -0
  245. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  246. package/docs/api/capture-lossy-jpeg.md +41 -0
  247. package/docs/api/capture-native-raw-development.md +2 -1
  248. package/docs/api/core-execution-error.md +43 -0
  249. package/docs/api/core-version.md +1 -1
  250. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  251. package/docs/api/equipment-sensor-profile-body.md +64 -0
  252. package/docs/api/equipment-sensor-profiles.md +86 -0
  253. package/docs/api/exports.json +460 -5
  254. package/docs/api/facade-display-view-gpu.md +5 -2
  255. package/docs/api/facade-display-view.md +11 -15
  256. package/docs/api/facade-exposure-build-up.md +105 -0
  257. package/docs/api/facade-fast-methods.md +47 -10
  258. package/docs/api/facade-image-plane-enable.md +16 -0
  259. package/docs/api/facade-mixed-illumination.md +67 -0
  260. package/docs/api/facade-quality-paths.md +24 -0
  261. package/docs/api/facade-representative-light-sources.md +131 -0
  262. package/docs/api/facade-scene-radiance.md +54 -3
  263. package/docs/api/facade-simulate-capture.md +48 -11
  264. package/docs/api/facade-simulate-display-view.md +10 -12
  265. package/docs/api/gpu-image-kernel-execution.md +0 -36
  266. package/docs/api/motion-subject-motion.md +23 -0
  267. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  268. package/docs/api/output-camera-channel-response.md +55 -0
  269. package/docs/api/output-developed-lens-corrections.md +10 -1
  270. package/docs/api/output-generic-lens-corrections.md +40 -4
  271. package/docs/api/output-print-plan.md +35 -1
  272. package/docs/api/output-sdr-rendering.md +6 -1
  273. package/docs/api/sensor-sensor-characteristics.md +54 -2
  274. package/llms.txt +3 -0
  275. package/package.json +6 -5
@@ -0,0 +1,78 @@
1
+ import { type GenericTierBodyAssets, type GenericTierPresetVersion } from "./generic-tier-assets.js";
2
+ /** How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value. */
3
+ export type SensorProfileValueKind = "published" | "derived" | "design";
4
+ /** One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind. */
5
+ export interface SensorProfile {
6
+ id: string;
7
+ /** Format, resolution, architecture and era; no maker or product name. */
8
+ name: string;
9
+ /** A market category, not a specification: it is a label and changes nothing. */
10
+ marketTag: "consumer" | "professional";
11
+ architecture: "front-illuminated" | "back-illuminated" | "stacked";
12
+ sensorWidthMm: number;
13
+ sensorHeightMm: number;
14
+ pixelsWide: number;
15
+ pixelsHigh: number;
16
+ pixelPitchMicrometers: number;
17
+ /** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */
18
+ peakQuantumEfficiency: number;
19
+ /** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */
20
+ fullWellElectrons: number;
21
+ /** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */
22
+ readNoise: {
23
+ lowGainElectrons: number;
24
+ highGainElectrons: number;
25
+ highGainFromIso: number | null;
26
+ };
27
+ /** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */
28
+ highGainFullWellElectrons: number | null;
29
+ adcBitDepth: 12 | 14 | 16;
30
+ /** Re-rated base ISO (see the module comment) and the highest ISO setting. */
31
+ baseIso: number;
32
+ maximumIso: number;
33
+ /** Largest analog gain relative to base ISO; above it the gain is digital. */
34
+ maximumAnalogGain: number;
35
+ /** Rolling readout, s: the top-to-bottom skew with `motion.shutter: "rolling"`. */
36
+ readoutSeconds: number;
37
+ /** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */
38
+ fixedPattern: {
39
+ prnu: number;
40
+ dsnu: number;
41
+ hotPixelFraction: number;
42
+ hotPixelMultiplier: number;
43
+ rowElectrons: number;
44
+ columnElectrons: number;
45
+ };
46
+ /** What the profile is good for (text only). */
47
+ goodFor: readonly string[];
48
+ description: string;
49
+ /** "phone": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */
50
+ fixedLenses: "phone" | null;
51
+ /**
52
+ * Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md "Sources"); a value not listed is a design value.
53
+ * For example `fullWellElectrons: "published S1"` or `baseIso: "derived"`.
54
+ */
55
+ provenance: Readonly<Partial<Record<keyof SensorProfile | "sensorSize" | "pixels" | "highGainFromIso", string>>>;
56
+ }
57
+ /** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */
58
+ export declare const SENSOR_PROFILE_ISO_MARKINGS: readonly number[];
59
+ /**
60
+ * The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than
61
+ * the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).
62
+ * Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.
63
+ */
64
+ export declare function parseSensorProfile(value: unknown, path?: string): SensorProfile;
65
+ /** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */
66
+ export declare const SENSOR_PROFILE_ISO_ANCHOR: Readonly<{
67
+ iso: 100;
68
+ fullWellElectrons: 60000;
69
+ pixelAreaSquareMicrometers: 36;
70
+ peakQuantumEfficiency: 0.5;
71
+ }>;
72
+ /** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */
73
+ export declare function sensorProfileSaturationIso(p: Pick<SensorProfile, "peakQuantumEfficiency" | "pixelPitchMicrometers" | "fullWellElectrons">): number;
74
+ /**
75
+ * The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both
76
+ * read noises: √N for a site that sums N photosites (a downsized view, #554).
77
+ */
78
+ export declare function sensorProfileBody(p: SensorProfile, body: GenericTierBodyAssets, version: GenericTierPresetVersion, readNoiseScale?: number): GenericTierBodyAssets;
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Sensor profiles (#554): the profile record, its strict parser and the camera-body sensor assets built from it. The
4
+ * catalogue's data are in `sensor-profiles.ts`, which nothing in a capture imports: a caller passes a profile record
5
+ * (`simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`), so a capture bundle never contains the catalogue.
6
+ *
7
+ * ISO rating (owner decision, 2026-11-09): each profile's base ISO is re-rated from its full well, QE and pixel area,
8
+ * S0 = 100 · (η / 0.5) · (A / 36 µm²) · (60 000 e⁻ / FW), rounded to the nearest third-stop marking, so the RAW white at base
9
+ * ISO is the full well and every ISO keeps the engine's anchor (the Professional tier's ISO 100: 60 000 e⁻ at the white,
10
+ * 6 µm pixels, QE 0.5; S = 10/H, CIPA DC-004-2020 §3.2). The rounding is reported as `sensitivityOffsetStops` in the
11
+ * `sensor.profile` assumption. The high gain's charge storage is the white charge at its switch ISO.
12
+ * @see docs/SENSOR_PROFILES.md for every value's source or design basis.
13
+ */
14
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
15
+ import { requireInputObject } from "../core/record-validation.js";
16
+ import { checkFieldRanges } from "../sensor/sensor-characteristics.js";
17
+ import { bodyAssets, GENERIC_THIRD_STOP_ISO_SETTINGS } from "./generic-tier-assets.js";
18
+ /** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */
19
+ export const SENSOR_PROFILE_ISO_MARKINGS = /* @__PURE__ */ Object.freeze([32, 40, 50, 64, 80, ...GENERIC_THIRD_STOP_ISO_SETTINGS, 32000, 40000, 51200, 64000, 80000, 102400]);
20
+ const ISO_MARKINGS = SENSOR_PROFILE_ISO_MARKINGS;
21
+ const fail = (message) => { throw new InvalidConfigurationError(message); };
22
+ const isoMarking = (v) => ISO_MARKINGS.includes(v);
23
+ const text = (v) => typeof v === "string" && v.length > 0 && v.length <= 400;
24
+ const FRACTION = [0, 1], ELECTRONS = [0, 100];
25
+ /** The record's fields, by path (#555's range checker: [minimum, maximum], the values allowed, 0 a nested record, or a predicate). */
26
+ const SCHEMA = { id: text, name: text, marketTag: ["consumer", "professional"], architecture: ["front-illuminated", "back-illuminated", "stacked"],
27
+ sensorWidthMm: [1, 100], sensorHeightMm: [1, 100], pixelsWide: [1, 1e5], pixelsHigh: [1, 1e5], pixelPitchMicrometers: [0.5, 100], peakQuantumEfficiency: [0.05, 0.9],
28
+ fullWellElectrons: [100, 1e10], readNoise: 0, "readNoise.lowGainElectrons": ELECTRONS, "readNoise.highGainElectrons": ELECTRONS, "readNoise.highGainFromIso": (v) => v === null || isoMarking(v),
29
+ highGainFullWellElectrons: (v) => v === null || typeof v === "number" && v >= 1 && v <= 1e10, adcBitDepth: [12, 14, 16], baseIso: isoMarking, maximumIso: isoMarking,
30
+ maximumAnalogGain: [1, 1024], readoutSeconds: [1e-6, 1], fixedPattern: 0, "fixedPattern.prnu": FRACTION, "fixedPattern.dsnu": [0, 100], "fixedPattern.hotPixelFraction": FRACTION,
31
+ "fixedPattern.hotPixelMultiplier": [1, 1e4], "fixedPattern.rowElectrons": ELECTRONS, "fixedPattern.columnElectrons": ELECTRONS,
32
+ goodFor: (v) => Array.isArray(v) && v.length <= 16 && v.every(text), description: text, fixedLenses: [null, "phone"],
33
+ provenance: (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.entries(v).every(([k, s]) => k.length <= 64 && text(s)) };
34
+ /**
35
+ * The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than
36
+ * the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).
37
+ * Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.
38
+ */
39
+ export function parseSensorProfile(value, path = "sensor.profile") {
40
+ requireInputObject(value, path);
41
+ checkFieldRanges(value, Object.keys(value), "", SCHEMA, path + ".", true);
42
+ const p = value, n = p.readNoise;
43
+ if (!Number.isInteger(p.pixelsWide) || !Number.isInteger(p.pixelsHigh))
44
+ fail(path + ".pixelsWide and pixelsHigh must be whole numbers.");
45
+ if (p.baseIso > p.maximumIso)
46
+ fail(path + ".baseIso must not exceed maximumIso.");
47
+ if (n.highGainFromIso === null ? p.highGainFullWellElectrons !== null || n.highGainElectrons !== n.lowGainElectrons :
48
+ p.highGainFullWellElectrons === null || n.highGainFromIso <= p.baseIso || p.highGainFullWellElectrons > p.fullWellElectrons)
49
+ fail(path + ": a dual-gain switch above baseIso needs highGainFullWellElectrons up to the full well; without one, equal read noises and a null storage.");
50
+ return p;
51
+ }
52
+ /** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */
53
+ export const SENSOR_PROFILE_ISO_ANCHOR = /* @__PURE__ */ Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 });
54
+ /** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */
55
+ export function sensorProfileSaturationIso(p) {
56
+ const a = SENSOR_PROFILE_ISO_ANCHOR;
57
+ return a.iso * p.peakQuantumEfficiency / a.peakQuantumEfficiency * p.pixelPitchMicrometers ** 2 / a.pixelAreaSquareMicrometers * a.fullWellElectrons / p.fullWellElectrons;
58
+ }
59
+ /**
60
+ * The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both
61
+ * read noises: √N for a site that sums N photosites (a downsized view, #554).
62
+ */
63
+ export function sensorProfileBody(p, body, version, readNoiseScale = 1) {
64
+ const evidence = [{ sourceOrigin: "manufacturer", sourceReference: "photivra:sensor-profiles-v1#" + p.id, reuseStatus: "factual-reference-only",
65
+ note: "Published specifications and open papers (docs/SENSOR_PROFILES.md); unpublished values are Photivra design values." }];
66
+ const black = { 12: 256, 14: 1024, 16: 4096 }[p.adcBitDepth], n = p.readNoise;
67
+ const built = bodyAssets(version, "photivra-sensor-profile-" + p.id, p.maximumIso, 1, 1, { bitDepth: p.adcBitDepth, blackLevelCode: black,
68
+ saturationElectrons: p.fullWellElectrons, maximumAnalogGain: p.maximumAnalogGain, lowGainReadNoise: n.lowGainElectrons * readNoiseScale, highGainReadNoise: n.highGainElectrons * readNoiseScale,
69
+ highGainFromIso: n.highGainFromIso ?? Infinity, highGainMaximumCharge: (p.highGainFullWellElectrons ?? p.fullWellElectrons) * (1 + 1e-12),
70
+ prnu: p.fixedPattern.prnu, dsnu: p.fixedPattern.dsnu, hotFraction: p.fixedPattern.hotPixelFraction, hotMultiplier: p.fixedPattern.hotPixelMultiplier,
71
+ rowRms: p.fixedPattern.rowElectrons, columnRms: p.fixedPattern.columnElectrons, instanceSeed: (0x9e3779b9 ^ (p.pixelsWide * 8191 + p.pixelsHigh)) >>> 0 }, p.readoutSeconds, .5, .01, false, false, { isoValues: ISO_MARKINGS.filter((iso) => iso >= p.baseIso && iso <= p.maximumIso), evidence,
72
+ limitations: ["Sensor profile " + p.name + " (#554): published, derived and design values (docs/SENSOR_PROFILES.md); not a calibration of any camera."] });
73
+ // Only the sensor assets are taken; the placeholders above (release, stabilization …) are the body's own.
74
+ return { ...body, exposure: built.exposure, iso: built.iso, signalChain: built.signalChain, readout: built.readout, readoutTiming: built.readoutTiming,
75
+ rollingReadoutTiming: built.rollingReadoutTiming, fixedPatternNoise: built.fixedPatternNoise, flashSync: built.flashSync };
76
+ }
77
+ //# sourceMappingURL=sensor-profile-body.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensor-profile-body.js","sourceRoot":"","sources":["../../src/equipment/sensor-profile-body.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor profiles (#554): the profile record, its strict parser and the camera-body sensor assets built from it. The\n * catalogue's data are in `sensor-profiles.ts`, which nothing in a capture imports: a caller passes a profile record\n * (`simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`), so a capture bundle never contains the catalogue.\n *\n * ISO rating (owner decision, 2026-11-09): each profile's base ISO is re-rated from its full well, QE and pixel area,\n * S0 = 100 · (η / 0.5) · (A / 36 µm²) · (60 000 e⁻ / FW), rounded to the nearest third-stop marking, so the RAW white at base\n * ISO is the full well and every ISO keeps the engine's anchor (the Professional tier's ISO 100: 60 000 e⁻ at the white,\n * 6 µm pixels, QE 0.5; S = 10/H, CIPA DC-004-2020 §3.2). The rounding is reported as `sensitivityOffsetStops` in the\n * `sensor.profile` assumption. The high gain's charge storage is the white charge at its switch ISO.\n * @see docs/SENSOR_PROFILES.md for every value's source or design basis.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { checkFieldRanges, type FieldRange } from \"../sensor/sensor-characteristics.js\";\nimport { bodyAssets, GENERIC_THIRD_STOP_ISO_SETTINGS, type GenericTierBodyAssets, type GenericTierPresetVersion } from \"./generic-tier-assets.js\";\n\n/** How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value. */\nexport type SensorProfileValueKind = \"published\" | \"derived\" | \"design\";\n\n/** One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind. */\nexport interface SensorProfile {\n id: string;\n /** Format, resolution, architecture and era; no maker or product name. */\n name: string;\n /** A market category, not a specification: it is a label and changes nothing. */\n marketTag: \"consumer\" | \"professional\";\n architecture: \"front-illuminated\" | \"back-illuminated\" | \"stacked\";\n sensorWidthMm: number; sensorHeightMm: number;\n pixelsWide: number; pixelsHigh: number;\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */\n highGainFullWellElectrons: number | null;\n adcBitDepth: 12 | 14 | 16;\n /** Re-rated base ISO (see the module comment) and the highest ISO setting. */\n baseIso: number; maximumIso: number;\n /** Largest analog gain relative to base ISO; above it the gain is digital. */\n maximumAnalogGain: number;\n /** Rolling readout, s: the top-to-bottom skew with `motion.shutter: \"rolling\"`. */\n readoutSeconds: number;\n /** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */\n fixedPattern: { prnu: number; dsnu: number; hotPixelFraction: number; hotPixelMultiplier: number; rowElectrons: number; columnElectrons: number };\n /** What the profile is good for (text only). */\n goodFor: readonly string[];\n description: string;\n /** \"phone\": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */\n fixedLenses: \"phone\" | null;\n /**\n * Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md \"Sources\"); a value not listed is a design value.\n * For example `fullWellElectrons: \"published S1\"` or `baseIso: \"derived\"`.\n */\n provenance: Readonly<Partial<Record<keyof SensorProfile | \"sensorSize\" | \"pixels\" | \"highGainFromIso\", string>>>;\n}\n\n/** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */\nexport const SENSOR_PROFILE_ISO_MARKINGS: readonly number[] = /* @__PURE__ */ Object.freeze([32, 40, 50, 64, 80, ...GENERIC_THIRD_STOP_ISO_SETTINGS, 32000, 40000, 51200, 64000, 80000, 102400]);\nconst ISO_MARKINGS = SENSOR_PROFILE_ISO_MARKINGS;\n\nconst fail = (message: string): never => { throw new InvalidConfigurationError(message); };\nconst isoMarking = (v: unknown): boolean => ISO_MARKINGS.includes(v as number);\nconst text = (v: unknown): boolean => typeof v === \"string\" && v.length > 0 && v.length <= 400;\nconst FRACTION = [0, 1] as const, ELECTRONS = [0, 100] as const;\n/** The record's fields, by path (#555's range checker: [minimum, maximum], the values allowed, 0 a nested record, or a predicate). */\nconst SCHEMA: Readonly<Record<string, FieldRange>> = { id: text, name: text, marketTag: [\"consumer\", \"professional\"], architecture: [\"front-illuminated\", \"back-illuminated\", \"stacked\"],\n sensorWidthMm: [1, 100], sensorHeightMm: [1, 100], pixelsWide: [1, 1e5], pixelsHigh: [1, 1e5], pixelPitchMicrometers: [0.5, 100], peakQuantumEfficiency: [0.05, 0.9],\n fullWellElectrons: [100, 1e10], readNoise: 0, \"readNoise.lowGainElectrons\": ELECTRONS, \"readNoise.highGainElectrons\": ELECTRONS, \"readNoise.highGainFromIso\": (v) => v === null || isoMarking(v),\n highGainFullWellElectrons: (v) => v === null || typeof v === \"number\" && v >= 1 && v <= 1e10, adcBitDepth: [12, 14, 16], baseIso: isoMarking, maximumIso: isoMarking,\n maximumAnalogGain: [1, 1024], readoutSeconds: [1e-6, 1], fixedPattern: 0, \"fixedPattern.prnu\": FRACTION, \"fixedPattern.dsnu\": [0, 100], \"fixedPattern.hotPixelFraction\": FRACTION,\n \"fixedPattern.hotPixelMultiplier\": [1, 1e4], \"fixedPattern.rowElectrons\": ELECTRONS, \"fixedPattern.columnElectrons\": ELECTRONS,\n goodFor: (v) => Array.isArray(v) && v.length <= 16 && v.every(text), description: text, fixedLenses: [null, \"phone\"],\n provenance: (v) => typeof v === \"object\" && v !== null && !Array.isArray(v) && Object.entries(v).every(([k, s]) => k.length <= 64 && text(s)) };\n/**\n * The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than\n * the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).\n * Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.\n */\nexport function parseSensorProfile(value: unknown, path = \"sensor.profile\"): SensorProfile {\n requireInputObject(value, path);\n checkFieldRanges(value as Record<string, unknown>, Object.keys(value as object), \"\", SCHEMA, path + \".\", true);\n const p = value as SensorProfile, n = p.readNoise;\n if (!Number.isInteger(p.pixelsWide) || !Number.isInteger(p.pixelsHigh)) fail(path + \".pixelsWide and pixelsHigh must be whole numbers.\");\n if (p.baseIso > p.maximumIso) fail(path + \".baseIso must not exceed maximumIso.\");\n if (n.highGainFromIso === null ? p.highGainFullWellElectrons !== null || n.highGainElectrons !== n.lowGainElectrons :\n p.highGainFullWellElectrons === null || n.highGainFromIso <= p.baseIso || p.highGainFullWellElectrons > p.fullWellElectrons)\n fail(path + \": a dual-gain switch above baseIso needs highGainFullWellElectrons up to the full well; without one, equal read noises and a null storage.\");\n return p;\n}\n\n/** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */\nexport const SENSOR_PROFILE_ISO_ANCHOR = /* @__PURE__ */ Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 });\n/** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */\nexport function sensorProfileSaturationIso(p: Pick<SensorProfile, \"peakQuantumEfficiency\" | \"pixelPitchMicrometers\" | \"fullWellElectrons\">): number {\n const a = SENSOR_PROFILE_ISO_ANCHOR;\n return a.iso * p.peakQuantumEfficiency / a.peakQuantumEfficiency * p.pixelPitchMicrometers ** 2 / a.pixelAreaSquareMicrometers * a.fullWellElectrons / p.fullWellElectrons;\n}\n\n\n/**\n * The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both\n * read noises: √N for a site that sums N photosites (a downsized view, #554).\n */\nexport function sensorProfileBody(p: SensorProfile, body: GenericTierBodyAssets, version: GenericTierPresetVersion, readNoiseScale = 1): GenericTierBodyAssets {\n const evidence: readonly EvidenceProvenance[] = [{ sourceOrigin: \"manufacturer\", sourceReference: \"photivra:sensor-profiles-v1#\" + p.id, reuseStatus: \"factual-reference-only\",\n note: \"Published specifications and open papers (docs/SENSOR_PROFILES.md); unpublished values are Photivra design values.\" }];\n const black = { 12: 256, 14: 1024, 16: 4096 }[p.adcBitDepth], n = p.readNoise;\n const built = bodyAssets(version, \"photivra-sensor-profile-\" + p.id, p.maximumIso, 1, 1, { bitDepth: p.adcBitDepth, blackLevelCode: black,\n saturationElectrons: p.fullWellElectrons, maximumAnalogGain: p.maximumAnalogGain, lowGainReadNoise: n.lowGainElectrons * readNoiseScale, highGainReadNoise: n.highGainElectrons * readNoiseScale,\n highGainFromIso: n.highGainFromIso ?? Infinity, highGainMaximumCharge: (p.highGainFullWellElectrons ?? p.fullWellElectrons) * (1 + 1e-12),\n prnu: p.fixedPattern.prnu, dsnu: p.fixedPattern.dsnu, hotFraction: p.fixedPattern.hotPixelFraction, hotMultiplier: p.fixedPattern.hotPixelMultiplier,\n rowRms: p.fixedPattern.rowElectrons, columnRms: p.fixedPattern.columnElectrons, instanceSeed: (0x9e3779b9 ^ (p.pixelsWide * 8191 + p.pixelsHigh)) >>> 0 },\n p.readoutSeconds, .5, .01, false, false, { isoValues: ISO_MARKINGS.filter((iso) => iso >= p.baseIso && iso <= p.maximumIso), evidence,\n limitations: [\"Sensor profile \" + p.name + \" (#554): published, derived and design values (docs/SENSOR_PROFILES.md); not a calibration of any camera.\"] });\n // Only the sensor assets are taken; the placeholders above (release, stabilization …) are the body's own.\n return { ...body, exposure: built.exposure, iso: built.iso, signalChain: built.signalChain, readout: built.readout, readoutTiming: built.readoutTiming,\n rollingReadoutTiming: built.rollingReadoutTiming, fixedPatternNoise: built.fixedPatternNoise, flashSync: built.flashSync };\n}\n"]}
@@ -0,0 +1,8 @@
1
+ import type { SensorProfile } from "./sensor-profile-body.js";
2
+ export type { SensorProfile, SensorProfileValueKind } from "./sensor-profile-body.js";
3
+ /** The catalogue, in the issue's order. Frozen. */
4
+ export declare const SENSOR_PROFILES: readonly SensorProfile[];
5
+ /** The profile ids, in catalogue order. */
6
+ export declare const SENSOR_PROFILE_IDS: readonly string[];
7
+ /** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */
8
+ export declare function resolveSensorProfile(id: unknown): SensorProfile;
@@ -0,0 +1,82 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Sensor profile catalogue (#554): eight representative, unnamed sensors whose values come from manufacturer-published
4
+ * specifications and openly published papers, or are labelled design values inside the range those sources support.
5
+ * Profiles are named by format, resolution, architecture and era; the market tag is a label only and enters no equation.
6
+ *
7
+ * A capture takes a profile record, `simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`; this module is data only
8
+ * and is never imported by the capture, so an application's capture worker does not bundle it (the record is checked by
9
+ * `parseSensorProfile` in `sensor-profile-body.ts`).
10
+ * @see docs/SENSOR_PROFILES.md for every value's source or design basis.
11
+ */
12
+ import { InvalidConfigurationError } from "../core/configuration-error.js";
13
+ const BSI = { prnu: .004, dsnu: .15, hotPixelFraction: 5e-5, hotPixelMultiplier: 30, rowElectrons: .15, columnElectrons: .1 };
14
+ const FSI = { prnu: .006, dsnu: .2, hotPixelFraction: 1e-4, hotPixelMultiplier: 40, rowElectrons: .3, columnElectrons: .2 };
15
+ const sourced = (key, ...fields) => Object.fromEntries(fields.map((f) => [f, "published " + key]));
16
+ /** The catalogue, in the issue's order. Frozen. */
17
+ export const SENSOR_PROFILES = /* @__PURE__ */ Object.freeze([
18
+ { id: "full-frame-24mp-bsi-2020s", name: "Full frame 24 MP BSI, 2020s", marketTag: "consumer", architecture: "back-illuminated",
19
+ sensorWidthMm: 36.07, sensorHeightMm: 24.01, pixelsWide: 6072, pixelsHigh: 4042, pixelPitchMicrometers: 5.94, peakQuantumEfficiency: .8,
20
+ fullWellElectrons: 100000, readNoise: { lowGainElectrons: 6.4, highGainElectrons: 1.1, highGainFromIso: 500 }, highGainFullWellElectrons: 20000,
21
+ adcBitDepth: 14, baseIso: 100, maximumIso: 51200, maximumAnalogGain: 64, readoutSeconds: .05, fixedPattern: BSI,
22
+ goodFor: ["landscape", "portrait", "travel"], description: "Large pixels and a deep full well for wide dynamic range at base ISO; a moderate readout shows some rolling-shutter skew.",
23
+ fixedLenses: null, provenance: { ...sourced("S1", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise", "adcBitDepth"),
24
+ sensorSize: "derived", architecture: "published S1", highGainFromIso: "derived S1", highGainFullWellElectrons: "derived", baseIso: "derived" } },
25
+ { id: "full-frame-45mp-stacked-2020s", name: "Full frame 45 MP stacked, 2020s", marketTag: "professional", architecture: "stacked",
26
+ sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 8256, pixelsHigh: 5504, pixelPitchMicrometers: 4.36, peakQuantumEfficiency: .8,
27
+ fullWellElectrons: 69000, readNoise: { lowGainElectrons: 3.5, highGainElectrons: 1.5, highGainFromIso: 250 }, highGainFullWellElectrons: 22080,
28
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .005, fixedPattern: BSI,
29
+ goodFor: ["landscape", "studio", "wildlife"], description: "High resolution with a fast stacked readout: fine detail for landscape and studio work, little rolling-shutter skew for action.",
30
+ fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", fullWellElectrons: "design (S2 scaled by pixel area)", readNoise: "design (S2)", highGainFromIso: "design (S2)",
31
+ highGainFullWellElectrons: "derived", baseIso: "derived" } },
32
+ { id: "full-frame-24mp-stacked-sports-2020s", name: "Full frame 24 MP stacked (sports), 2020s", marketTag: "professional", architecture: "stacked",
33
+ sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 6000, pixelsHigh: 4000, pixelPitchMicrometers: 6, peakQuantumEfficiency: .8,
34
+ fullWellElectrons: 80000, readNoise: { lowGainElectrons: 3, highGainElectrons: 1, highGainFromIso: 400 }, highGainFullWellElectrons: 25000,
35
+ adcBitDepth: 14, baseIso: 125, maximumIso: 102400, maximumAnalogGain: 64, readoutSeconds: .0025, fixedPattern: BSI,
36
+ goodFor: ["sports", "wildlife", "journalism"], description: "The fastest readout and low high-gain read noise: almost no rolling-shutter skew and clean high ISO, at a moderate resolution.",
37
+ fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", highGainFullWellElectrons: "derived", baseIso: "derived" } },
38
+ { id: "full-frame-24mp-fsi-2010s", name: "Full frame 24 MP front-illuminated, 2010s", marketTag: "professional", architecture: "front-illuminated",
39
+ sensorWidthMm: 36.01, sensorHeightMm: 24.07, pixelsWide: 6032, pixelsHigh: 4032, pixelPitchMicrometers: 5.97, peakQuantumEfficiency: .53,
40
+ fullWellElectrons: 76000, readNoise: { lowGainElectrons: 4, highGainElectrons: 4, highGainFromIso: null }, highGainFullWellElectrons: null,
41
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .08, fixedPattern: FSI,
42
+ goodFor: ["portrait", "landscape", "journalism"], description: "An older professional sensor: lower QE and one conversion gain, so a modern consumer BSI sensor beats it at high ISO; slow readout.",
43
+ fixedLenses: null, provenance: { ...sourced("S3", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "adcBitDepth"),
44
+ sensorSize: "derived", architecture: "derived S4", baseIso: "derived" } },
45
+ { id: "aps-c-24mp-fsi-2020s", name: "APS-C 24 MP front-illuminated, 2020s", marketTag: "consumer", architecture: "front-illuminated",
46
+ sensorWidthMm: 23.55, sensorHeightMm: 15.73, pixelsWide: 6024, pixelsHigh: 4024, pixelPitchMicrometers: 3.91, peakQuantumEfficiency: .55,
47
+ fullWellElectrons: 36000, readNoise: { lowGainElectrons: 2.7, highGainElectrons: 2.7, highGainFromIso: null }, highGainFullWellElectrons: null,
48
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .06, fixedPattern: FSI,
49
+ goodFor: ["travel", "family", "learning"], description: "A smaller, front-illuminated sensor: less light per pixel, a slower readout and more rolling-shutter skew.",
50
+ fixedLenses: null, provenance: { ...sourced("S5", "pixels", "pixelPitchMicrometers", "architecture", "fullWellElectrons", "readNoise", "adcBitDepth"), sensorSize: "derived", baseIso: "derived" } },
51
+ { id: "aps-c-26mp-bsi-2020s", name: "APS-C 26 MP BSI, 2020s", marketTag: "professional", architecture: "back-illuminated",
52
+ sensorWidthMm: 23.49, sensorHeightMm: 15.7, pixelsWide: 6248, pixelsHigh: 4176, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
53
+ fullWellElectrons: 50000, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16000,
54
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .025, fixedPattern: BSI,
55
+ goodFor: ["wildlife", "sports", "travel"], description: "A BSI APS-C sensor with dual conversion gain whose readout beats the consumer full-frame profile's.",
56
+ fixedLenses: null, provenance: { ...sourced("S6", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise"), sensorSize: "derived",
57
+ architecture: "published S6", highGainFromIso: "design (S7)", highGainFullWellElectrons: "derived", baseIso: "derived" } },
58
+ { id: "medium-format-100mp-bsi-16bit", name: "Medium format 100 MP BSI, 16-bit", marketTag: "professional", architecture: "back-illuminated",
59
+ sensorWidthMm: 43.856, sensorHeightMm: 32.9, pixelsWide: 11656, pixelsHigh: 8750, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
60
+ fullWellElectrons: 50300, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16096,
61
+ adcBitDepth: 16, baseIso: 80, maximumIso: 12800, maximumAnalogGain: 64, readoutSeconds: .25, fixedPattern: BSI,
62
+ goodFor: ["studio", "landscape", "portrait"], description: "A large sensor with 100 MP and a 16-bit RAW; a slow readout makes its electronic shutter unsuited to motion.",
63
+ fixedLenses: null, provenance: { ...sourced("S7", "pixels", "pixelPitchMicrometers", "sensorSize", "fullWellElectrons", "readNoise", "adcBitDepth"), architecture: "published S8",
64
+ highGainFromIso: "derived S7", highGainFullWellElectrons: "derived", baseIso: "derived" } },
65
+ { id: "phone-50mp-stacked", name: "Phone, large, 50 MP stacked", marketTag: "consumer", architecture: "stacked",
66
+ sensorWidthMm: 9.844, sensorHeightMm: 7.431, pixelsWide: 8192, pixelsHigh: 6144, pixelPitchMicrometers: 1.197, peakQuantumEfficiency: .75,
67
+ fullWellElectrons: 8600, readNoise: { lowGainElectrons: 3, highGainElectrons: 1.4, highGainFromIso: 160 }, highGainFullWellElectrons: 2150,
68
+ adcBitDepth: 12, baseIso: 40, maximumIso: 6400, maximumAnalogGain: 64, readoutSeconds: 1 / 30,
69
+ fixedPattern: { prnu: .008, dsnu: .3, hotPixelFraction: 2e-4, hotPixelMultiplier: 50, rowElectrons: .2, columnElectrons: .2 },
70
+ goodFor: ["everyday", "travel", "family"], description: "A large phone sensor: tiny pixels with a small full well, so less dynamic range and more noise per pixel; simulated as a single frame (no multi-frame stacking).",
71
+ fixedLenses: "phone", provenance: { ...sourced("S9", "pixels", "pixelPitchMicrometers", "sensorSize", "architecture", "adcBitDepth"), fullWellElectrons: "design (S10 scaled by pixel area)",
72
+ readNoise: "high gain published S10, low gain design", highGainFullWellElectrons: "derived", readoutSeconds: "derived S9 (1 / full-resolution frame rate)",
73
+ fixedPattern: "PRNU, row and column published S10; the rest design", baseIso: "derived" } }
74
+ ].map((p) => Object.freeze(p)));
75
+ /** The profile ids, in catalogue order. */
76
+ export const SENSOR_PROFILE_IDS = /* @__PURE__ */ Object.freeze(SENSOR_PROFILES.map((p) => p.id));
77
+ /** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */
78
+ export function resolveSensorProfile(id) {
79
+ return SENSOR_PROFILES.find((p) => p.id === id) ?? fail("resolveSensorProfile: the id must be one of " + SENSOR_PROFILE_IDS.join(", ") + ".");
80
+ }
81
+ function fail(message) { throw new InvalidConfigurationError(message); }
82
+ //# sourceMappingURL=sensor-profiles.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sensor-profiles.js","sourceRoot":"","sources":["../../src/equipment/sensor-profiles.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor profile catalogue (#554): eight representative, unnamed sensors whose values come from manufacturer-published\n * specifications and openly published papers, or are labelled design values inside the range those sources support.\n * Profiles are named by format, resolution, architecture and era; the market tag is a label only and enters no equation.\n *\n * A capture takes a profile record, `simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`; this module is data only\n * and is never imported by the capture, so an application's capture worker does not bundle it (the record is checked by\n * `parseSensorProfile` in `sensor-profile-body.ts`).\n * @see docs/SENSOR_PROFILES.md for every value's source or design basis.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { SensorProfile } from \"./sensor-profile-body.js\";\n\nexport type { SensorProfile, SensorProfileValueKind } from \"./sensor-profile-body.js\";\n\nconst BSI = { prnu: .004, dsnu: .15, hotPixelFraction: 5e-5, hotPixelMultiplier: 30, rowElectrons: .15, columnElectrons: .1 };\nconst FSI = { prnu: .006, dsnu: .2, hotPixelFraction: 1e-4, hotPixelMultiplier: 40, rowElectrons: .3, columnElectrons: .2 };\nconst sourced = (key: string, ...fields: string[]): Record<string, string> => Object.fromEntries(fields.map((f) => [f, \"published \" + key]));\n\n/** The catalogue, in the issue's order. Frozen. */\nexport const SENSOR_PROFILES: readonly SensorProfile[] = /* @__PURE__ */ Object.freeze(([\n { id: \"full-frame-24mp-bsi-2020s\", name: \"Full frame 24 MP BSI, 2020s\", marketTag: \"consumer\", architecture: \"back-illuminated\",\n sensorWidthMm: 36.07, sensorHeightMm: 24.01, pixelsWide: 6072, pixelsHigh: 4042, pixelPitchMicrometers: 5.94, peakQuantumEfficiency: .8,\n fullWellElectrons: 100000, readNoise: { lowGainElectrons: 6.4, highGainElectrons: 1.1, highGainFromIso: 500 }, highGainFullWellElectrons: 20000,\n adcBitDepth: 14, baseIso: 100, maximumIso: 51200, maximumAnalogGain: 64, readoutSeconds: .05, fixedPattern: BSI,\n goodFor: [\"landscape\", \"portrait\", \"travel\"], description: \"Large pixels and a deep full well for wide dynamic range at base ISO; a moderate readout shows some rolling-shutter skew.\",\n fixedLenses: null, provenance: { ...sourced(\"S1\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"),\n sensorSize: \"derived\", architecture: \"published S1\", highGainFromIso: \"derived S1\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-45mp-stacked-2020s\", name: \"Full frame 45 MP stacked, 2020s\", marketTag: \"professional\", architecture: \"stacked\",\n sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 8256, pixelsHigh: 5504, pixelPitchMicrometers: 4.36, peakQuantumEfficiency: .8,\n fullWellElectrons: 69000, readNoise: { lowGainElectrons: 3.5, highGainElectrons: 1.5, highGainFromIso: 250 }, highGainFullWellElectrons: 22080,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .005, fixedPattern: BSI,\n goodFor: [\"landscape\", \"studio\", \"wildlife\"], description: \"High resolution with a fast stacked readout: fine detail for landscape and studio work, little rolling-shutter skew for action.\",\n fixedLenses: null, provenance: { pixelPitchMicrometers: \"derived\", fullWellElectrons: \"design (S2 scaled by pixel area)\", readNoise: \"design (S2)\", highGainFromIso: \"design (S2)\",\n highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-24mp-stacked-sports-2020s\", name: \"Full frame 24 MP stacked (sports), 2020s\", marketTag: \"professional\", architecture: \"stacked\",\n sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 6000, pixelsHigh: 4000, pixelPitchMicrometers: 6, peakQuantumEfficiency: .8,\n fullWellElectrons: 80000, readNoise: { lowGainElectrons: 3, highGainElectrons: 1, highGainFromIso: 400 }, highGainFullWellElectrons: 25000,\n adcBitDepth: 14, baseIso: 125, maximumIso: 102400, maximumAnalogGain: 64, readoutSeconds: .0025, fixedPattern: BSI,\n goodFor: [\"sports\", \"wildlife\", \"journalism\"], description: \"The fastest readout and low high-gain read noise: almost no rolling-shutter skew and clean high ISO, at a moderate resolution.\",\n fixedLenses: null, provenance: { pixelPitchMicrometers: \"derived\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-24mp-fsi-2010s\", name: \"Full frame 24 MP front-illuminated, 2010s\", marketTag: \"professional\", architecture: \"front-illuminated\",\n sensorWidthMm: 36.01, sensorHeightMm: 24.07, pixelsWide: 6032, pixelsHigh: 4032, pixelPitchMicrometers: 5.97, peakQuantumEfficiency: .53,\n fullWellElectrons: 76000, readNoise: { lowGainElectrons: 4, highGainElectrons: 4, highGainFromIso: null }, highGainFullWellElectrons: null,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .08, fixedPattern: FSI,\n goodFor: [\"portrait\", \"landscape\", \"journalism\"], description: \"An older professional sensor: lower QE and one conversion gain, so a modern consumer BSI sensor beats it at high ISO; slow readout.\",\n fixedLenses: null, provenance: { ...sourced(\"S3\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"adcBitDepth\"),\n sensorSize: \"derived\", architecture: \"derived S4\", baseIso: \"derived\" } },\n { id: \"aps-c-24mp-fsi-2020s\", name: \"APS-C 24 MP front-illuminated, 2020s\", marketTag: \"consumer\", architecture: \"front-illuminated\",\n sensorWidthMm: 23.55, sensorHeightMm: 15.73, pixelsWide: 6024, pixelsHigh: 4024, pixelPitchMicrometers: 3.91, peakQuantumEfficiency: .55,\n fullWellElectrons: 36000, readNoise: { lowGainElectrons: 2.7, highGainElectrons: 2.7, highGainFromIso: null }, highGainFullWellElectrons: null,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .06, fixedPattern: FSI,\n goodFor: [\"travel\", \"family\", \"learning\"], description: \"A smaller, front-illuminated sensor: less light per pixel, a slower readout and more rolling-shutter skew.\",\n fixedLenses: null, provenance: { ...sourced(\"S5\", \"pixels\", \"pixelPitchMicrometers\", \"architecture\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"), sensorSize: \"derived\", baseIso: \"derived\" } },\n { id: \"aps-c-26mp-bsi-2020s\", name: \"APS-C 26 MP BSI, 2020s\", marketTag: \"professional\", architecture: \"back-illuminated\",\n sensorWidthMm: 23.49, sensorHeightMm: 15.7, pixelsWide: 6248, pixelsHigh: 4176, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,\n fullWellElectrons: 50000, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16000,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .025, fixedPattern: BSI,\n goodFor: [\"wildlife\", \"sports\", \"travel\"], description: \"A BSI APS-C sensor with dual conversion gain whose readout beats the consumer full-frame profile's.\",\n fixedLenses: null, provenance: { ...sourced(\"S6\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"readNoise\"), sensorSize: \"derived\",\n architecture: \"published S6\", highGainFromIso: \"design (S7)\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"medium-format-100mp-bsi-16bit\", name: \"Medium format 100 MP BSI, 16-bit\", marketTag: \"professional\", architecture: \"back-illuminated\",\n sensorWidthMm: 43.856, sensorHeightMm: 32.9, pixelsWide: 11656, pixelsHigh: 8750, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,\n fullWellElectrons: 50300, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16096,\n adcBitDepth: 16, baseIso: 80, maximumIso: 12800, maximumAnalogGain: 64, readoutSeconds: .25, fixedPattern: BSI,\n goodFor: [\"studio\", \"landscape\", \"portrait\"], description: \"A large sensor with 100 MP and a 16-bit RAW; a slow readout makes its electronic shutter unsuited to motion.\",\n fixedLenses: null, provenance: { ...sourced(\"S7\", \"pixels\", \"pixelPitchMicrometers\", \"sensorSize\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"), architecture: \"published S8\",\n highGainFromIso: \"derived S7\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"phone-50mp-stacked\", name: \"Phone, large, 50 MP stacked\", marketTag: \"consumer\", architecture: \"stacked\",\n sensorWidthMm: 9.844, sensorHeightMm: 7.431, pixelsWide: 8192, pixelsHigh: 6144, pixelPitchMicrometers: 1.197, peakQuantumEfficiency: .75,\n fullWellElectrons: 8600, readNoise: { lowGainElectrons: 3, highGainElectrons: 1.4, highGainFromIso: 160 }, highGainFullWellElectrons: 2150,\n adcBitDepth: 12, baseIso: 40, maximumIso: 6400, maximumAnalogGain: 64, readoutSeconds: 1 / 30,\n fixedPattern: { prnu: .008, dsnu: .3, hotPixelFraction: 2e-4, hotPixelMultiplier: 50, rowElectrons: .2, columnElectrons: .2 },\n goodFor: [\"everyday\", \"travel\", \"family\"], description: \"A large phone sensor: tiny pixels with a small full well, so less dynamic range and more noise per pixel; simulated as a single frame (no multi-frame stacking).\",\n fixedLenses: \"phone\", provenance: { ...sourced(\"S9\", \"pixels\", \"pixelPitchMicrometers\", \"sensorSize\", \"architecture\", \"adcBitDepth\"), fullWellElectrons: \"design (S10 scaled by pixel area)\",\n readNoise: \"high gain published S10, low gain design\", highGainFullWellElectrons: \"derived\", readoutSeconds: \"derived S9 (1 / full-resolution frame rate)\",\n fixedPattern: \"PRNU, row and column published S10; the rest design\", baseIso: \"derived\" } }\n] satisfies SensorProfile[]).map((p) => Object.freeze(p)));\n\n/** The profile ids, in catalogue order. */\nexport const SENSOR_PROFILE_IDS: readonly string[] = /* @__PURE__ */ Object.freeze(SENSOR_PROFILES.map((p) => p.id));\n\n/** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */\nexport function resolveSensorProfile(id: unknown): SensorProfile {\n return SENSOR_PROFILES.find((p) => p.id === id) ?? fail(\"resolveSensorProfile: the id must be one of \" + SENSOR_PROFILE_IDS.join(\", \") + \".\");\n}\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\n"]}
@@ -1,8 +1,30 @@
1
1
  import { type IlluminationVignettingProfile } from "../optics/illumination-vignetting.js";
2
2
  import { type ResolveLensSampledPsfInput } from "../optics/lens-psf-profile.js";
3
3
  import type { DisplayViewScene } from "./expected-signal.js";
4
- /** Fewest grid cells between neighboring profile field nodes. */
5
- export declare const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 4;
4
+ /** Fewest grid cells between neighboring profile field nodes (10 since #589: with 4, 0.4 % of quick frames failed the run-time check). */
5
+ export declare const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 10;
6
+ /**
7
+ * #603 (2.22.0): the fewest cells on a fast request (on screen: 2.20.0's grid, its run-time fallbacks to the exact method included);
8
+ * the full grid, {@link CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS}, on an exact one (the exports and the calculator).
9
+ */
10
+ export declare const CATALOGUE_FAST_GRID_SPEED_SUBDIVISIONS = 4;
11
+ /**
12
+ * #603 (owner decision 2026-11-11): the lenses that keep the full grid on fast requests too, because the 4-cell grid's run-time
13
+ * fallbacks to the exact method made their fast frames slower on average. Measured November 11, 2026 (arm64, Node.js 24; every
14
+ * catalogue lens but the phone lenses, wide open and at max(f/8, widest), Siemens star and color patches, whole frames 192, 384 and
15
+ * 768 pixels wide, two runs each; docs/LENS_CATALOGUE.md, "Fast method"): only this lens fell back, 4 of its 8 frames at 192 pixels
16
+ * (f/2.8), 11.6 s each, against 0.17 s for the 10-cell grid.
17
+ */
18
+ export declare const CATALOGUE_FAST_GRID_FULL_ON_SCREEN: readonly {
19
+ lensId: string;
20
+ fallbacksAtFourCells: {
21
+ frames: number;
22
+ fellBack: number;
23
+ widthPixels: number;
24
+ };
25
+ }[];
26
+ /** #603: the fewest grid cells for a lens on a fast or an exact request. */
27
+ export declare function catalogueFastGridSubdivisions(lensId: string, fast: boolean): number;
6
28
  /** Largest move of a node kernel's intensity centroid across one grid cell, in sites (sets finer grids where lateral color moves the PSF). */
7
29
  export declare const CATALOGUE_FAST_GRID_CENTROID_STEP_SITES = 0.05;
8
30
  /**
@@ -44,6 +66,8 @@ export interface CatalogueLensSignalInput {
44
66
  /** The planar provider's plane distance and frame-unit scale. */
45
67
  distanceM: number;
46
68
  metresPerFrameUnit: number;
69
+ /** #603: the fewest grid cells between neighbouring field nodes (default {@link CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS}). */
70
+ gridSubdivisions?: number;
47
71
  /** Internal test hook: resolve every site's kernels exactly (the exact method's sum, slowly) instead of blending the grid. */
48
72
  exactKernels?: boolean;
49
73
  }
@@ -66,7 +90,7 @@ export interface CatalogueLensSignal {
66
90
  * ⌈Δc / GRID_CENTROID_STEP_SITES⌉) equal cells, Δc the largest centroid move between the interval's two nodes over the other
67
91
  * axis's nodes and the wavelength nodes.
68
92
  */
69
- export declare function catalogueFastGridAxes(psf: CatalogueLensSignalInput["psf"]): {
93
+ export declare function catalogueFastGridAxes(psf: CatalogueLensSignalInput["psf"], minimum?: number): {
70
94
  x: number[];
71
95
  y: number[];
72
96
  };
@@ -87,6 +111,7 @@ export declare function catalogueFastAssumptions(run: {
87
111
  rows: number;
88
112
  resolved: number;
89
113
  verificationRelativeDifference: number;
114
+ subdivisions: number;
90
115
  }): {
91
116
  id: string;
92
117
  value: Record<string, unknown>;