@photivra/engine 2.20.0 → 2.21.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 (174) hide show
  1. package/CHANGELOG.md +178 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +7 -0
  6. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  7. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  8. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  9. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  10. package/dist/capture/exposure-build-up-stage.js +4 -0
  11. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  12. package/dist/capture/native-environment-lean.d.ts +2 -1
  13. package/dist/capture/native-environment-lean.js +41 -73
  14. package/dist/capture/native-environment-lean.js.map +1 -1
  15. package/dist/capture/native-environment-tabulated.js +4 -3
  16. package/dist/capture/native-environment-tabulated.js.map +1 -1
  17. package/dist/capture/native-raw.d.ts +3 -2
  18. package/dist/capture/native-raw.js +5 -4
  19. package/dist/capture/native-raw.js.map +1 -1
  20. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  21. package/dist/capture/sensor-raw-producer.js +12 -3
  22. package/dist/capture/sensor-raw-producer.js.map +1 -1
  23. package/dist/core/version.d.ts +1 -1
  24. package/dist/core/version.js +1 -1
  25. package/dist/core/version.js.map +1 -1
  26. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  27. package/dist/equipment/generic-lens-catalogue.js +18 -5
  28. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  29. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  30. package/dist/equipment/generic-tier-assets.js +16 -11
  31. package/dist/equipment/generic-tier-assets.js.map +1 -1
  32. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  33. package/dist/equipment/sensor-profile-body.js +77 -0
  34. package/dist/equipment/sensor-profile-body.js.map +1 -0
  35. package/dist/equipment/sensor-profiles.d.ts +8 -0
  36. package/dist/equipment/sensor-profiles.js +82 -0
  37. package/dist/equipment/sensor-profiles.js.map +1 -0
  38. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  39. package/dist/facade/catalogue-lens-signal.js +58 -71
  40. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  41. package/dist/facade/chart-radiance-batch.js +2 -1
  42. package/dist/facade/chart-radiance-batch.js.map +1 -1
  43. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  44. package/dist/facade/defocus-image-space-provider.js +64 -76
  45. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  46. package/dist/facade/display-view-gpu-session.js +2 -0
  47. package/dist/facade/display-view-gpu-session.js.map +1 -1
  48. package/dist/facade/display-view-gpu.d.ts +6 -1
  49. package/dist/facade/display-view-gpu.js.map +1 -1
  50. package/dist/facade/display-view-lens-signal.js +25 -23
  51. package/dist/facade/display-view-lens-signal.js.map +1 -1
  52. package/dist/facade/display-view.js +5 -23
  53. package/dist/facade/display-view.js.map +1 -1
  54. package/dist/facade/expected-signal-setup.d.ts +3 -1
  55. package/dist/facade/expected-signal-setup.js +1 -1
  56. package/dist/facade/expected-signal-setup.js.map +1 -1
  57. package/dist/facade/expected-signal.d.ts +47 -14
  58. package/dist/facade/expected-signal.js +117 -112
  59. package/dist/facade/expected-signal.js.map +1 -1
  60. package/dist/facade/exposure-build-up.d.ts +70 -0
  61. package/dist/facade/exposure-build-up.js +164 -0
  62. package/dist/facade/exposure-build-up.js.map +1 -0
  63. package/dist/facade/fast-methods.js +9 -1
  64. package/dist/facade/fast-methods.js.map +1 -1
  65. package/dist/facade/generic-lens-capture.d.ts +2 -2
  66. package/dist/facade/generic-lens-capture.js +2 -2
  67. package/dist/facade/generic-lens-capture.js.map +1 -1
  68. package/dist/facade/image-plane-enable.d.ts +7 -0
  69. package/dist/facade/image-plane-enable.js +17 -0
  70. package/dist/facade/image-plane-enable.js.map +1 -0
  71. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  72. package/dist/facade/image-plane-lattice-data.js +10 -0
  73. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  74. package/dist/facade/image-plane-support.d.ts +33 -0
  75. package/dist/facade/image-plane-support.js +11 -0
  76. package/dist/facade/image-plane-support.js.map +1 -0
  77. package/dist/facade/image-plane.d.ts +36 -0
  78. package/dist/facade/image-plane.js +353 -0
  79. package/dist/facade/image-plane.js.map +1 -0
  80. package/dist/facade/mixed-illumination.d.ts +108 -0
  81. package/dist/facade/mixed-illumination.js +174 -0
  82. package/dist/facade/mixed-illumination.js.map +1 -0
  83. package/dist/facade/representative-light-sources.d.ts +72 -0
  84. package/dist/facade/representative-light-sources.js +124 -0
  85. package/dist/facade/representative-light-sources.js.map +1 -0
  86. package/dist/facade/scene-radiance.d.ts +129 -4
  87. package/dist/facade/scene-radiance.js +48 -13
  88. package/dist/facade/scene-radiance.js.map +1 -1
  89. package/dist/facade/simulate-capture.d.ts +39 -10
  90. package/dist/facade/simulate-capture.js +252 -92
  91. package/dist/facade/simulate-capture.js.map +1 -1
  92. package/dist/facade/simulate-display-view.js +14 -6
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  95. package/dist/gpu/chart-composite-gpu.js +48 -9
  96. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  97. package/dist/gpu/chart-radiance-gpu.js +4 -30
  98. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  99. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  100. package/dist/gpu/display-view-composite-material-module.js +39 -13
  101. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  102. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  103. package/dist/gpu/display-view-image-material-module.js +339 -0
  104. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  105. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  106. package/dist/gpu/display-view-image-quality.js +380 -0
  107. package/dist/gpu/display-view-image-quality.js.map +1 -0
  108. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  109. package/dist/gpu/display-view-quality-gpu.js +136 -205
  110. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  111. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  112. package/dist/gpu/display-view-speed-gpu.js +131 -93
  113. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  115. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  116. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  117. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  118. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  119. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  120. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  121. package/dist/gpu/wgsl-exact-sum.js +42 -0
  122. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  123. package/dist/index.d.ts +11 -3
  124. package/dist/index.js +7 -1
  125. package/dist/index.js.map +1 -1
  126. package/dist/motion/subject-motion.d.ts +66 -0
  127. package/dist/motion/subject-motion.js +52 -0
  128. package/dist/motion/subject-motion.js.map +1 -0
  129. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  130. package/dist/optics/generic-lens-lateral-color.js +77 -0
  131. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  132. package/dist/optics/generic-lens-psf.js +54 -46
  133. package/dist/optics/generic-lens-psf.js.map +1 -1
  134. package/dist/optics/lens-psf-profile.d.ts +7 -0
  135. package/dist/optics/lens-psf-profile.js +2 -1
  136. package/dist/optics/lens-psf-profile.js.map +1 -1
  137. package/dist/output/developed-lens-corrections.d.ts +19 -5
  138. package/dist/output/developed-lens-corrections.js +28 -9
  139. package/dist/output/developed-lens-corrections.js.map +1 -1
  140. package/dist/output/generic-lens-corrections.d.ts +41 -5
  141. package/dist/output/generic-lens-corrections.js +52 -8
  142. package/dist/output/generic-lens-corrections.js.map +1 -1
  143. package/dist/output/print-plan.d.ts +41 -5
  144. package/dist/output/print-plan.js +59 -14
  145. package/dist/output/print-plan.js.map +1 -1
  146. package/dist/sensor/binomial-sample.d.ts +3 -0
  147. package/dist/sensor/binomial-sample.js +86 -0
  148. package/dist/sensor/binomial-sample.js.map +1 -0
  149. package/dist/sensor/sensor-characteristics.d.ts +10 -0
  150. package/dist/sensor/sensor-characteristics.js +20 -9
  151. package/dist/sensor/sensor-characteristics.js.map +1 -1
  152. package/docs/AGENT_GUIDE.md +1 -0
  153. package/docs/API_REFERENCE.md +98 -1
  154. package/docs/QUICK_START.md +125 -0
  155. package/docs/USAGE.md +92 -1
  156. package/docs/api/core-version.md +1 -1
  157. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  158. package/docs/api/equipment-sensor-profile-body.md +64 -0
  159. package/docs/api/equipment-sensor-profiles.md +86 -0
  160. package/docs/api/exports.json +207 -2
  161. package/docs/api/facade-exposure-build-up.md +105 -0
  162. package/docs/api/facade-fast-methods.md +9 -1
  163. package/docs/api/facade-image-plane-enable.md +16 -0
  164. package/docs/api/facade-mixed-illumination.md +67 -0
  165. package/docs/api/facade-representative-light-sources.md +131 -0
  166. package/docs/api/facade-scene-radiance.md +54 -3
  167. package/docs/api/facade-simulate-capture.md +26 -6
  168. package/docs/api/motion-subject-motion.md +23 -0
  169. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  170. package/docs/api/output-developed-lens-corrections.md +10 -1
  171. package/docs/api/output-generic-lens-corrections.md +40 -4
  172. package/docs/api/output-print-plan.md +35 -1
  173. package/llms.txt +2 -0
  174. package/package.json +1 -1
@@ -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":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAmB,MAAM,qCAAqC,CAAC;AACxF,OAAO,EAAE,UAAU,EAAE,+BAA+B,EAA6D,MAAM,0BAA0B,CAAC;AA6ClJ,0IAA0I;AAC1I,MAAM,CAAC,MAAM,2BAA2B,GAAsB,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,+BAA+B,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC;AACjM,MAAM,YAAY,GAAG,2BAA2B,CAAC;AAEjD,MAAM,IAAI,GAAG,CAAC,OAAe,EAAS,EAAE,GAAG,MAAM,IAAI,yBAAyB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3F,MAAM,UAAU,GAAG,CAAC,CAAU,EAAW,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAW,CAAC,CAAC;AAC/E,MAAM,IAAI,GAAG,CAAC,CAAU,EAAW,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,GAAG,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG,CAAC;AAC/F,MAAM,QAAQ,GAAG,CAAC,CAAC,EAAE,CAAC,CAAU,EAAE,SAAS,GAAG,CAAC,CAAC,EAAE,GAAG,CAAU,CAAC;AAChE,sIAAsI;AACtI,MAAM,MAAM,GAAyC,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,UAAU,EAAE,cAAc,CAAC,EAAE,YAAY,EAAE,CAAC,mBAAmB,EAAE,kBAAkB,EAAE,SAAS,CAAC;IACtL,aAAa,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,qBAAqB,EAAE,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,qBAAqB,EAAE,CAAC,IAAI,EAAE,GAAG,CAAC;IACpK,iBAAiB,EAAE,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,4BAA4B,EAAE,SAAS,EAAE,6BAA6B,EAAE,SAAS,EAAE,2BAA2B,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,UAAU,CAAC,CAAC,CAAC;IAChM,yBAAyB,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,IAAI,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,WAAW,EAAE,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,UAAU;IACpK,iBAAiB,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE,cAAc,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,YAAY,EAAE,CAAC,EAAE,mBAAmB,EAAE,QAAQ,EAAE,mBAAmB,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,+BAA+B,EAAE,QAAQ;IACjL,iCAAiC,EAAE,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,2BAA2B,EAAE,SAAS,EAAE,8BAA8B,EAAE,SAAS;IAC9H,OAAO,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,IAAI,EAAE,IAAI,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC;IACpH,UAAU,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAClJ;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAAc,EAAE,IAAI,GAAG,gBAAgB;IACxE,kBAAkB,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IAChC,gBAAgB,CAAC,KAAgC,EAAE,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,GAAG,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/G,MAAM,CAAC,GAAG,KAAsB,EAAE,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC;IAClD,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,UAAU,CAAC;QAAE,IAAI,CAAC,IAAI,GAAG,mDAAmD,CAAC,CAAC;IACzI,IAAI,CAAC,CAAC,OAAO,GAAG,CAAC,CAAC,UAAU;QAAE,IAAI,CAAC,IAAI,GAAG,sCAAsC,CAAC,CAAC;IAClF,IAAI,CAAC,CAAC,eAAe,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,yBAAyB,KAAK,IAAI,IAAI,CAAC,CAAC,iBAAiB,KAAK,CAAC,CAAC,gBAAgB,CAAC,CAAC;QACnH,CAAC,CAAC,yBAAyB,KAAK,IAAI,IAAI,CAAC,CAAC,eAAe,IAAI,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,yBAAyB,GAAG,CAAC,CAAC,iBAAiB;QAC3H,IAAI,CAAC,IAAI,GAAG,4IAA4I,CAAC,CAAC;IAC5J,OAAO,CAAC,CAAC;AACX,CAAC;AAED,2GAA2G;AAC3G,MAAM,CAAC,MAAM,yBAAyB,GAAG,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,iBAAiB,EAAE,KAAK,EAAE,0BAA0B,EAAE,EAAE,EAAE,qBAAqB,EAAE,EAAE,EAAE,CAAC,CAAC;AAC1K,0FAA0F;AAC1F,MAAM,UAAU,0BAA0B,CAAC,CAA+F;IACxI,MAAM,CAAC,GAAG,yBAAyB,CAAC;IACpC,OAAO,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,qBAAqB,GAAG,CAAC,CAAC,qBAAqB,GAAG,CAAC,CAAC,qBAAqB,IAAI,CAAC,GAAG,CAAC,CAAC,0BAA0B,GAAG,CAAC,CAAC,iBAAiB,GAAG,CAAC,CAAC,iBAAiB,CAAC;AAC7K,CAAC;AAGD;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAAC,CAAgB,EAAE,IAA2B,EAAE,OAAiC,EAAE,cAAc,GAAG,CAAC;IACpI,MAAM,QAAQ,GAAkC,CAAC,EAAE,YAAY,EAAE,cAAc,EAAE,eAAe,EAAE,8BAA8B,GAAG,CAAC,CAAC,EAAE,EAAE,WAAW,EAAE,wBAAwB;YAC5K,IAAI,EAAE,oHAAoH,EAAE,CAAC,CAAC;IAChI,MAAM,KAAK,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,WAAW,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,SAAS,CAAC;IAC9E,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,0BAA0B,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC,WAAW,EAAE,cAAc,EAAE,KAAK;QACvI,mBAAmB,EAAE,CAAC,CAAC,iBAAiB,EAAE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,EAAE,gBAAgB,EAAE,CAAC,CAAC,gBAAgB,GAAG,cAAc,EAAE,iBAAiB,EAAE,CAAC,CAAC,iBAAiB,GAAG,cAAc;QAChM,eAAe,EAAE,CAAC,CAAC,eAAe,IAAI,QAAQ,EAAE,qBAAqB,EAAE,CAAC,CAAC,CAAC,yBAAyB,IAAI,CAAC,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAC;QACzI,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,YAAY,CAAC,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,YAAY,CAAC,gBAAgB,EAAE,aAAa,EAAE,CAAC,CAAC,YAAY,CAAC,kBAAkB;QACpJ,MAAM,EAAE,CAAC,CAAC,YAAY,CAAC,YAAY,EAAE,SAAS,EAAE,CAAC,CAAC,YAAY,CAAC,eAAe,EAAE,YAAY,EAAE,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,UAAU,GAAG,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,EAAE,EAC3J,CAAC,CAAC,cAAc,EAAE,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,SAAS,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC,OAAO,IAAI,GAAG,IAAI,CAAC,CAAC,UAAU,CAAC,EAAE,QAAQ;QACnI,WAAW,EAAE,CAAC,iBAAiB,GAAG,CAAC,CAAC,IAAI,GAAG,2GAA2G,CAAC,EAAE,CAAC,CAAC;IAC7J,0GAA0G;IAC1G,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,GAAG,EAAE,KAAK,CAAC,GAAG,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;QACpJ,oBAAoB,EAAE,KAAK,CAAC,oBAAoB,EAAE,iBAAiB,EAAE,KAAK,CAAC,iBAAiB,EAAE,SAAS,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC;AAC/H,CAAC","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,8 @@
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
6
  /** 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
7
  export declare const CATALOGUE_FAST_GRID_CENTROID_STEP_SITES = 0.05;
8
8
  /**
@@ -18,9 +18,11 @@
18
18
  * The grid subdivides each interval between the profile's field nodes into at least GRID_MINIMUM_SUBDIVISIONS cells, and into
19
19
  * more where the node kernels' intensity centroids move by more than GRID_CENTROID_STEP_SITES sites across the interval (lateral
20
20
  * colour moves the PSF across the frame; a blend of two shifted kernels is not the shifted kernel). Since the blend is linear,
21
- * the channel response and the material radiance fold into the grid kernels once per grid point:
22
- * μ̃_c(q) = Σ_k F(s_k)·Σ_g b_g(q)·W_{g,c,m(s_k)}(k), W_{g,c,m}(k) = Σ_λ A_c(λ)·L_m(λ)·K_{g,λ}(k),
23
- * so a site costs (2R + 1)² taps instead of (2R + 1)² scene queries per spectral node. Its error against the exact method is
21
+ * the channel response folds into the grid kernels once per grid point and node, and each tap's signal is integrated from its
22
+ * material's spectrum when the site is read (2.21.0; no material is folded into a kernel):
23
+ * μ̃_c(q) = Σ_k F(s_k)·Σ_λ L_{m(s_k)}(λ)·Σ_g b_g(q)·C_{g,c}(k, λ), C_{g,c}(k, λ) = A_c(λ)·K_{g,λ}(k),
24
+ * the blend taken along y once per grid column and native row, so a site costs (2R + 1)² taps of N nodes each instead of
25
+ * (2R + 1)² scene queries per spectral node. Its error against the exact method is
24
26
  * bounded by ‖K̃ − K‖₁ times the brightest tap's signal; tests measure both (test/simulate-capture-lens-catalogue-fast.test.ts).
25
27
  *
26
28
  * The scene points. The mapping is the environment's (`undistortedImagePoint`, `lensProjectionDirectionValue`, the planar
@@ -32,9 +34,9 @@ import { InvalidConfigurationError } from "../core/configuration-error.js";
32
34
  import { insideRadialImageCircle, lensProjectionDirectionValue } from "../camera/lens-projection.js";
33
35
  import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
34
36
  import { prepareIlluminationVignettingThroughput } from "../optics/illumination-vignetting.js";
35
- import { resolveLensSampledPsfKernel } from "../optics/lens-psf-profile.js";
36
- /** Fewest grid cells between neighboring profile field nodes. */
37
- export const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 4;
37
+ import { kernelCentroid, resolveLensSampledPsfKernel } from "../optics/lens-psf-profile.js";
38
+ /** Fewest grid cells between neighboring profile field nodes (10 since #589: with 4, 0.4 % of quick frames failed the run-time check). */
39
+ export const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 10;
38
40
  /** Largest move of a node kernel's intensity centroid across one grid cell, in sites (sets finer grids where lateral color moves the PSF). */
39
41
  export const CATALOGUE_FAST_GRID_CENTROID_STEP_SITES = 0.05;
40
42
  /**
@@ -45,17 +47,6 @@ export const CATALOGUE_FAST_VERIFICATION_TOLERANCE = 0.005;
45
47
  /** Largest cache window, in lattice points; a larger window is evaluated without caching. */
46
48
  const WINDOW_LIMIT = 1 << 22;
47
49
  const NEWTON_ITERATIONS = 60;
48
- /** A kernel's intensity centroid, in samples. */
49
- function centroid(values, width) {
50
- let sum = 0, x = 0, y = 0;
51
- for (let i = 0; i < values.length; i++) {
52
- const v = values[i];
53
- sum += v;
54
- x += v * (i % width);
55
- y += v * Math.floor(i / width);
56
- }
57
- return { x: x / sum, y: y / sum };
58
- }
59
50
  /**
60
51
  * The grid axes: each interval between neighboring profile field nodes split into max(GRID_MINIMUM_SUBDIVISIONS,
61
52
  * ⌈Δc / GRID_CENTROID_STEP_SITES⌉) equal cells, Δc the largest centroid move between the interval's two nodes over the other
@@ -66,7 +57,7 @@ export function catalogueFastGridAxes(psf) {
66
57
  const centroids = new Map();
67
58
  for (const node of profile.nodes) {
68
59
  const c = node.coordinate;
69
- centroids.set(c.fieldXmm + ":" + c.fieldYmm + ":" + c.wavelengthNm, centroid(node.kernel.normalizedIntensity, node.kernel.widthSamples));
60
+ centroids.set(c.fieldXmm + ":" + c.fieldYmm + ":" + c.wavelengthNm, kernelCentroid(node.kernel));
70
61
  }
71
62
  const at = (x, y, nm) => {
72
63
  const c = centroids.get(x + ":" + y + ":" + nm);
@@ -122,18 +113,12 @@ export function createCatalogueLensSignal(given) {
122
113
  throw new InvalidConfigurationError("lens.catalogue fast: the PSF profile's kernels differ in size.");
123
114
  // Lattice reach of the taps around a site: x from −cx … kw − 1 − cx (as c_x − k_x), y from −cy … kh − 1 − cy.
124
115
  const reachLeft = kw - 1 - cx, reachRight = cx, reachUp = cy, reachDown = kh - 1 - cy;
125
- // Materials: neutral ones by reflectance (class 0, the white radiance), spectral ones by class 1 + index.
126
- const M = scene.materials.length, reflectance = new Float64Array(M), materialClass = new Int32Array(M);
127
- const spectral = [];
128
- scene.materials.forEach((m, index) => { if ("neutral" in m)
129
- reflectance[index] = m.neutral;
130
- else {
131
- materialClass[index] = 1 + spectral.length;
132
- spectral.push(m.radiance);
133
- } });
134
- const classes = 1 + spectral.length, classRadiance = [scene.whiteRadiance, ...spectral];
135
- const tableSize = 3 * classes * taps;
136
- // W_{c,m}(k) = Σ_λ A_c(λ)·L_m(λ)·K_λ(k) for the kernels of one field point.
116
+ // Each material's spectral radiance at the nodes (a neutral one: its reflectance times the white radiance). Only the spectra are
117
+ // kept: a site's signal is integrated from its taps' material spectra when it is read, so no material is folded into a kernel.
118
+ const M = scene.materials.length;
119
+ const spectra = scene.materials.map((m) => ("neutral" in m ? Float64Array.from(scene.whiteRadiance, (w) => m.neutral * w) : Float64Array.from(m.radiance)));
120
+ const tableSize = 3 * taps * nodes;
121
+ // A_c(λ_n)·K_n(k) for the kernels of one field point, at ((c·taps + k)·nodes + n): the channel response per node, no material.
137
122
  const foldKernels = (fieldXmm, fieldYmm) => {
138
123
  const table = new Float64Array(tableSize);
139
124
  for (let n = 0; n < nodes; n++) {
@@ -142,15 +127,8 @@ export function createCatalogueLensSignal(given) {
142
127
  throw new InvalidConfigurationError("lens.catalogue fast: a resolved kernel differs in size.");
143
128
  for (let c = 0; c < 3; c++) {
144
129
  const a = A[c][n];
145
- if (a === 0)
146
- continue;
147
- for (let cl = 0; cl < classes; cl++) {
148
- const weight = a * classRadiance[cl][n], base = (c * classes + cl) * taps;
149
- if (weight === 0)
150
- continue;
151
- for (let k = 0; k < taps; k++)
152
- table[base + k] = table[base + k] + weight * kernel[k];
153
- }
130
+ for (let k = 0; k < taps; k++)
131
+ table[(c * taps + k) * nodes + n] = a * kernel[k];
154
132
  }
155
133
  }
156
134
  return table;
@@ -279,40 +257,39 @@ export function createCatalogueLensSignal(given) {
279
257
  winFactor[q * w + p] = lf;
280
258
  }
281
259
  };
282
- const kernelOf = new Float64Array(tableSize);
283
- let cellI = -1, cellJ = -1, c00 = kernelOf, c10 = kernelOf, c01 = kernelOf, c11 = kernelOf;
260
+ // A site's kernel is the bilinear blend of its cell's four grid kernels. Blended along y first, once per grid column and native
261
+ // row (every site of a row has the same fy): R_i(y) = (1 − fy)·T(i, j) + fy·T(i, j + 1), kept for the row the sites are on.
262
+ const rowTables = [], rowOf = new Int32Array(GX).fill(-1);
263
+ const rowTable = (i, y) => {
264
+ let table = rowTables[i];
265
+ if (!table)
266
+ table = rowTables[i] = new Float64Array(tableSize);
267
+ if (rowOf[i] !== y) {
268
+ const j = cellY[y], fy = weightY[y], a = tableAt(i, j), b = tableAt(i, j + 1);
269
+ for (let e = 0; e < tableSize; e++)
270
+ table[e] = (1 - fy) * a[e] + fy * b[e];
271
+ rowOf[i] = y;
272
+ }
273
+ return table;
274
+ };
284
275
  const site = (x, y) => {
285
276
  const c = channelAt(x, y);
286
- let t00, t10, t01, t11, b00, b10, b01, b11;
277
+ let r0, r1, fx;
287
278
  if (input.exactKernels) {
288
- kernelOf.set(foldKernels((x + 0.5 - W / 2) * mmPerSite, -(y + 0.5 - H / 2) * mmPerSite));
289
- t00 = t10 = t01 = t11 = kernelOf;
290
- b00 = 1;
291
- b10 = b01 = b11 = 0;
279
+ r0 = r1 = foldKernels((x + 0.5 - W / 2) * mmPerSite, -(y + 0.5 - H / 2) * mmPerSite);
280
+ fx = 0;
292
281
  }
293
282
  else {
294
- const i = cellX[x], j = cellY[y], fx = weightX[x], fy = weightY[y];
295
- // Neighboring sites share a cell: its four tables are kept from the last site.
296
- if (i !== cellI || j !== cellJ) {
297
- cellI = i;
298
- cellJ = j;
299
- c00 = tableAt(i, j);
300
- c10 = tableAt(i + 1, j);
301
- c01 = tableAt(i, j + 1);
302
- c11 = tableAt(i + 1, j + 1);
303
- }
304
- t00 = c00;
305
- t10 = c10;
306
- t01 = c01;
307
- t11 = c11;
308
- b00 = (1 - fx) * (1 - fy);
309
- b10 = fx * (1 - fy);
310
- b01 = (1 - fx) * fy;
311
- b11 = fx * fy;
283
+ const i = cellX[x];
284
+ r0 = rowTable(i, y);
285
+ r1 = rowTable(i + 1, y);
286
+ fx = weightX[x];
312
287
  }
313
288
  const inside = ww > 0 && x - reachLeft >= wx0 && y - reachUp >= wy0 && x + reachRight < wx0 + ww && y + reachDown < wy0 + wh;
314
- const channelBase = c * classes * taps;
315
- let sum = 0;
289
+ const channelBase = c * taps * nodes;
290
+ // Each tap's signal is integrated from its material's spectrum against the two row-blended kernels per node; the blend along x
291
+ // is linear, so it is applied to the two sums.
292
+ let s0 = 0, s1 = 0;
316
293
  for (let ky = 0, k = 0; ky < kh; ky++) {
317
294
  const q = y + ky - cy;
318
295
  for (let kx = 0; kx < kw; kx++, k++) {
@@ -330,12 +307,22 @@ export function createCatalogueLensSignal(given) {
330
307
  }
331
308
  if (f === 0)
332
309
  continue;
333
- const cl = materialClass[m], at = channelBase + cl * taps + k;
334
- const w = b00 * t00[at] + b10 * t10[at] + b01 * t01[at] + b11 * t11[at];
335
- sum += f * (cl === 0 ? reflectance[m] * w : w);
310
+ const L = spectra[m], at = channelBase + k * nodes;
311
+ let w0 = 0, w1 = 0;
312
+ if (fx === 0)
313
+ for (let n = 0; n < nodes; n++)
314
+ w0 += r0[at + n] * L[n];
315
+ else
316
+ for (let n = 0; n < nodes; n++) {
317
+ const l = L[n];
318
+ w0 += r0[at + n] * l;
319
+ w1 += r1[at + n] * l;
320
+ }
321
+ s0 += f * w0;
322
+ s1 += f * w1;
336
323
  }
337
324
  }
338
- return sum;
325
+ return fx === 0 ? s0 : (1 - fx) * s0 + fx * s1;
339
326
  };
340
327
  return {
341
328
  window, site,
@@ -349,7 +336,7 @@ export function catalogueFastAssumptions(run) {
349
336
  return [{ id: "raw.expectedSignal", value: { method: "tabulated-expected-electrons-catalogue-psf-field-grid", verificationSites: 4, tolerance,
350
337
  verificationRelativeDifference: run.verificationRelativeDifference },
351
338
  explanation: "Each native site's expected photo electrons are tabulated: the sum over the PSF taps of the field factor (vignetting × distortion area factor) times the " +
352
- "grid-blended kernel folded with the channel response and the material radiance (see fast.lens.catalogue). The sensor stage (dark charge, Poisson draws, read noise, ADC) " +
339
+ "grid-blended kernel per spectral node times the channel response, integrated with each tap's material spectrum (see fast.lens.catalogue). The sensor stage (dark charge, Poisson draws, read noise, ADC) " +
353
340
  "is the native one with each site's own seeds. One site of each CFA phase ran the exact chain (the environment PSF quadrature): its readout was reproduced bit for bit and " +
354
341
  "its electrons agreed with the fast ones within the tolerance (verificationRelativeDifference: the largest difference there)." },
355
342
  { id: "fast.lens.catalogue", value: { method: "field-grid-bilinear-psf", gridColumns: run.columns, gridRows: run.rows, gridPointsResolved: run.resolved,