@photivra/engine 2.11.1 → 2.13.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 (55) hide show
  1. package/CHANGELOG.md +94 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +2 -4
  4. package/README.md +6 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +1 -0
  7. package/dist/capture/export-jpeg.d.ts +1 -1
  8. package/dist/capture/export-jpeg.js +6 -4
  9. package/dist/capture/export-jpeg.js.map +1 -1
  10. package/dist/core/version.d.ts +1 -1
  11. package/dist/core/version.js +1 -1
  12. package/dist/core/version.js.map +1 -1
  13. package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
  14. package/dist/exposure/auto-iso-slowest-shutter.js +216 -0
  15. package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
  16. package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
  17. package/dist/exposure/exposure-mode-resolver.js +238 -33
  18. package/dist/exposure/exposure-mode-resolver.js.map +1 -1
  19. package/dist/facade/diffraction-provider.js +12 -13
  20. package/dist/facade/diffraction-provider.js.map +1 -1
  21. package/dist/facade/display-view.d.ts +27 -0
  22. package/dist/facade/display-view.js +234 -65
  23. package/dist/facade/display-view.js.map +1 -1
  24. package/dist/facade/simulate-capture.d.ts +159 -3
  25. package/dist/facade/simulate-capture.js +203 -10
  26. package/dist/facade/simulate-capture.js.map +1 -1
  27. package/dist/facade/simulate-display-view.js +2 -2
  28. package/dist/facade/simulate-display-view.js.map +1 -1
  29. package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
  30. package/dist/gpu/display-view-quality-gpu.js +24 -14
  31. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  32. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
  33. package/dist/gpu/wgsl-display-view-quality-kernel.js +36 -2
  34. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  35. package/dist/index.d.ts +4 -3
  36. package/dist/index.js +2 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/optics/irradiance-convolution.d.ts +10 -0
  39. package/dist/optics/irradiance-convolution.js +48 -13
  40. package/dist/optics/irradiance-convolution.js.map +1 -1
  41. package/dist/output/print-jpeg.js +12 -9
  42. package/dist/output/print-jpeg.js.map +1 -1
  43. package/dist/output/text-metadata.d.ts +22 -2
  44. package/dist/output/text-metadata.js +58 -9
  45. package/dist/output/text-metadata.js.map +1 -1
  46. package/docs/API_REFERENCE.md +23 -1
  47. package/docs/QUICK_START.md +109 -3
  48. package/docs/USAGE.md +234 -1
  49. package/docs/api/core-version.md +1 -1
  50. package/docs/api/exports.json +77 -2
  51. package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
  52. package/docs/api/exposure-exposure-mode-resolver.md +68 -0
  53. package/docs/api/facade-simulate-capture.md +153 -5
  54. package/docs/api/output-text-metadata.md +34 -0
  55. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import type { EvidenceProvenance } from "../core/evidence-provenance.js";
2
2
  import { type GenericEquipmentTier, type GenericEquipmentTierPreset } from "../equipment/generic-tier-assets.js";
3
+ import { type AutoIsoLimits, type ExposureResolutionConstraint } from "../exposure/exposure-mode-resolver.js";
3
4
  import { resolveColorTemperatureWhiteBalance, type ResolvedWhiteBalanceState } from "../color/white-balance.js";
4
5
  import { type CaptureWhiteBalanceIntent } from "../capture/simulated-capture.js";
5
6
  import { type NativeRawOutput, type NativeRawPlan } from "../capture/native-raw.js";
@@ -27,6 +28,95 @@ import { type ChartSizeOption, type CaptureIlluminant, type CallerMaterialScene,
27
28
  /** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
28
29
  export declare const SIMULATE_CAPTURE_FACADE_VERSION: "1.0.0";
29
30
  export type SimulateCaptureExposureMode = "manual" | "aperture-priority" | "shutter-priority" | "program";
31
+ /**
32
+ * #516: what aperture priority does with Auto ISO once the ISO reaches the body's Auto ISO maximum and the exposure is
33
+ * still under the meter target: `"allow-slower-shutter"` lengthens the shutter past the slowest preferred shutter to reach
34
+ * the target; `"hold-preferred-shutter"` keeps the preferred shutter and leaves the exposure under target.
35
+ */
36
+ export type SimulateCaptureAutoIsoAfterMaximum = "allow-slower-shutter" | "hold-preferred-shutter";
37
+ /**
38
+ * #516: how Auto ISO chose the ISO (`exposure.iso: "auto"`), reported in the result's `exposure.autoIso`. The full
39
+ * resolver output stays in the capture's `exposure.resolution`.
40
+ */
41
+ export interface SimulateCaptureAutoIsoReport {
42
+ /** The ISO the meter target asked for at the resolved aperture and shutter, before the Auto ISO range and the ISO grid. */
43
+ idealIsoBeforeConstraints: number;
44
+ /**
45
+ * The body's Auto ISO range: its Auto ISO bounds, or its ISO range when it declares none (the tiers: ISO 100 to their
46
+ * maximum). Without `limits` the ISO was chosen in this range.
47
+ */
48
+ autoIsoMinimum: number;
49
+ autoIsoMaximum: number;
50
+ /**
51
+ * #505: present only with `exposure.autoIso.limits`: the photographer's Auto ISO range the ISO was chosen in, inside the
52
+ * body's (`minimum` is the body's Auto ISO minimum when the option omits it).
53
+ */
54
+ limits?: {
55
+ minimum: number;
56
+ maximum: number;
57
+ };
58
+ /** Whether the ISO was held at an end of the Auto ISO range. */
59
+ clamped: false | "minimum" | "maximum";
60
+ /**
61
+ * The constraint that limited the resolved exposure: the resolver's `targetResidual.limitingConstraint`, for example
62
+ * `"iso-maximum"` (the exposure stays under target at the body's Auto ISO maximum), `"user-iso-maximum"` (#505: at the
63
+ * photographer's `limits.maximum`, below the body's), `"iso-grid-quantization"` (the nearest selectable ISO) or `"none"`
64
+ * (on target).
65
+ */
66
+ limitingConstraint: ExposureResolutionConstraint;
67
+ /** The resolver's residual, log2(target / achieved exposure), in stops: positive when the exposure is under the meter target, negative when over it. */
68
+ residualStops: number;
69
+ /**
70
+ * Aperture priority, and program with a slowest preferred shutter (#504): the policy and slowest preferred shutter used,
71
+ * and whether the slower-shutter fallback ran.
72
+ */
73
+ afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
74
+ slowestPreferredShutterSeconds?: number;
75
+ afterMaximumIsoFallbackUsed?: boolean;
76
+ /** #504, program only: whether the program line's shutter was longer than the slowest preferred shutter and was held. */
77
+ slowestPreferredShutterApplied?: boolean;
78
+ /** #504: present only with `slowestPreferredShutterSeconds: "auto"`: how the engine derived it (`estimateAutoIsoSlowestShutter`). */
79
+ automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterReport;
80
+ }
81
+ /**
82
+ * #504: the settings of `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`. Every field is optional; the defaults are
83
+ * labelled generic values, recorded as `default` assumptions.
84
+ */
85
+ export interface SimulateCaptureAutomaticSlowestShutterOptions {
86
+ /** The bias in whole stops, as cameras' Auto minimum-shutter settings offer: +1 one stop faster, −1 one stop slower; −2 to +2. Default 0. */
87
+ fasterByStops?: number;
88
+ /** The shake blur allowed, in sensor pixels of `sensor.pixelPitchMicrometers`. Default 1 (a generic pixel-level budget). */
89
+ blurBudgetPixels?: number;
90
+ /**
91
+ * The hand's constant angular velocity, rad/s (signs as in `motion.angularVelocityRadPerSec`; only the magnitude matters).
92
+ * Default `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`: 4.39 °/s on yaw, a cited generic value for a 100 g phone held in
93
+ * two hands (Xiao et al., Proc. SPIE 7250, 2009), not a measurement of you or of this camera.
94
+ */
95
+ angularVelocityRadPerSec?: {
96
+ yaw: number;
97
+ pitch: number;
98
+ };
99
+ /** Stabilization in equivalent stops (≥ 0, at most 10), scaling the motion by 2^−stops. Default 0: the tiers declare no stabilization. */
100
+ stabilizationStopsEquivalent?: number;
101
+ }
102
+ /** #504: the automatic slowest preferred shutter in the Auto ISO report. */
103
+ export interface SimulateCaptureAutomaticSlowestShutterReport {
104
+ /** The exposure time whose camera-shake blur equals the budget (`estimateCameraShakeBlur`), before the bias and the grid. */
105
+ derivedShutterSeconds: number;
106
+ fasterByStops: number;
107
+ /** derivedShutterSeconds · 2^−fasterByStops. */
108
+ biasedShutterSeconds: number;
109
+ /** The slowest preferred shutter used: the biased time within the body's shutter range. */
110
+ shutterSeconds: number;
111
+ blurBudgetPixels: number;
112
+ /** The shake blur at `shutterSeconds`, in pixels. */
113
+ blurAtShutterPixels: number;
114
+ angularVelocityRadPerSec: {
115
+ yaw: number;
116
+ pitch: number;
117
+ };
118
+ stabilizationStopsEquivalent: number;
119
+ }
30
120
  export type SimulateCaptureOutputFormat = "srgb-8" | "srgb-16" | "jpeg" | "png" | "dng";
31
121
  export type SimulateCaptureWhiteBalance = "scene-illuminant" | "auto" | "none" | {
32
122
  kelvin: number;
@@ -191,8 +281,38 @@ export interface SimulateCaptureOptions {
191
281
  exposure?: {
192
282
  /** Default "manual". */
193
283
  mode?: SimulateCaptureExposureMode;
194
- /** Default 100. */
195
- iso?: number;
284
+ /**
285
+ * Default 100. #516: `"auto"` lets the body's Auto ISO choose the ISO from the meter target, within its Auto ISO range
286
+ * (the tiers: ISO 100 to their maximum), in every mode: manual keeps the aperture and shutter; aperture priority keeps the
287
+ * aperture and follows `autoIso`; shutter priority picks the aperture at the lowest Auto ISO (no wider than the target
288
+ * needs, so at most the widest) and raises the ISO for the rest; program picks the program line at the lowest Auto ISO and
289
+ * raises the ISO beyond the line's end. The result reports the ISO in `exposure.settings.iso` and how it was chosen in
290
+ * `exposure.autoIso`.
291
+ */
292
+ iso?: number | "auto";
293
+ /**
294
+ * Auto ISO settings, with `iso: "auto"` only.
295
+ *
296
+ * - `slowestPreferredShutterSeconds` and `afterMaximumIso` (#516): aperture priority, where the slowest shutter is
297
+ * required, and (#504) program, where it is optional. The shutter gets as long as `slowestPreferredShutterSeconds`
298
+ * (seconds, within the body's shutter range) at the lowest Auto ISO; beyond it the ISO rises; at the Auto ISO maximum
299
+ * `afterMaximumIso` decides (default `"allow-slower-shutter"`; in program the shutter then never gets longer than the
300
+ * program line's own). Without it, program follows its line as before.
301
+ * - `slowestPreferredShutterSeconds: "auto"` (#504): the engine derives it from the focal length, the pixel pitch and a
302
+ * hand-shake profile with `estimateAutoIsoSlowestShutter` (the camera-shake model, not the 1/focal-length rule of
303
+ * thumb); `automaticSlowestShutter` sets the bias, the blur budget, the shake and the stabilization.
304
+ * - `limits` (#505): every mode. The photographer's Auto ISO range `{ maximum, minimum? }`, a choice kept apart from
305
+ * the body's Auto ISO capability: both are selectable ISOs inside the body's Auto ISO range, `minimum` at most
306
+ * `maximum` (default: the body's minimum). Outside the body's range or off its ISO grid the request fails; the
307
+ * engine never clips a limit. When the ISO stops at a limit strictly inside the body's range, the report's
308
+ * `limitingConstraint` is `"user-iso-maximum"` or `"user-iso-minimum"`.
309
+ */
310
+ autoIso?: {
311
+ slowestPreferredShutterSeconds?: number | "auto";
312
+ afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
313
+ limits?: AutoIsoLimits;
314
+ automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterOptions;
315
+ };
196
316
  /** Shutter for manual and shutter-priority modes. Default 1/125 s. */
197
317
  shutterSeconds?: number;
198
318
  /** Exposure compensation applied to the meter target, in stops. Default 0. */
@@ -430,7 +550,25 @@ export interface SimulateCaptureResult {
430
550
  };
431
551
  /** Exposure value at ISO 100 of the settings, log2(N²/t). */
432
552
  ev100: number;
553
+ /** #516: `"automatic"` when the request asked for `iso: "auto"`, else `"manual"`. */
554
+ isoControl: "manual" | "automatic";
555
+ /** #516: present with `iso: "auto"`: how Auto ISO chose `settings.iso` and what limited it. */
556
+ autoIso?: SimulateCaptureAutoIsoReport;
433
557
  meteredSceneLuminanceCdPerSquareMeter: number;
558
+ /**
559
+ * #515: deviation, in stops, of the exposure used from the CIPA DC-004-2020 standard exposure for the metered luminance L:
560
+ * log2(N²/t) − log2(0.65·L·S/10). Positive is darker than the standard exposure (under), negative brighter (over). In an
561
+ * automatic mode it is the compensation with the opposite sign plus any step or limit the resolver hit. Finite, or null when
562
+ * the metered luminance is 0 (a black scene has no standard exposure).
563
+ */
564
+ standardExposureDeviationStops: number | null;
565
+ /**
566
+ * The illuminance, lux, at which the settings used give the standard exposure: E·L_ref(N, t, S)/L = E·2^Δ, with E the
567
+ * request's `illuminanceLux`. Exact for a reflective scene (a chart, a reflectance function or a material scene), whose
568
+ * metered luminance is proportional to E. Null for a black scene (L = 0) and for a radiance-function scene, whose
569
+ * radiance does not depend on the illuminance. It may exceed the 1e7 lux that `illuminanceLux` accepts.
570
+ */
571
+ standardExposureIlluminanceLux: number | null;
434
572
  /** Full output of the existing exposure-mode resolver. */
435
573
  resolution: unknown;
436
574
  };
@@ -619,6 +757,9 @@ export interface IdealReferenceResult {
619
757
  iso: number;
620
758
  };
621
759
  ev100: number;
760
+ /** #516: as in the capture result. */
761
+ isoControl: "manual" | "automatic";
762
+ autoIso?: SimulateCaptureAutoIsoReport;
622
763
  };
623
764
  scene: SimulateCaptureResult["scene"];
624
765
  region?: {
@@ -699,7 +840,7 @@ export interface DisplayViewResult {
699
840
  height: number;
700
841
  };
701
842
  };
702
- /** The resolved exposure of the capture this view shows. */
843
+ /** The resolved exposure of the capture this view shows; `standardExposureDeviationStops` and `standardExposureIlluminanceLux` as in {@link SimulateCaptureResult}. */
703
844
  exposure: {
704
845
  mode: SimulateCaptureExposureMode;
705
846
  settings: {
@@ -708,6 +849,12 @@ export interface DisplayViewResult {
708
849
  iso: number;
709
850
  };
710
851
  ev100: number;
852
+ standardExposureDeviationStops: number | null;
853
+ /** As in the capture result. */
854
+ standardExposureIlluminanceLux: number | null;
855
+ /** #516: as in the capture result. */
856
+ isoControl: "manual" | "automatic";
857
+ autoIso?: SimulateCaptureAutoIsoReport;
711
858
  };
712
859
  scene: SimulateCaptureResult["scene"];
713
860
  /**
@@ -809,6 +956,15 @@ export interface DisplayViewRunContext {
809
956
  regimeId: string;
810
957
  }>;
811
958
  resolvedIso: number;
959
+ /** #516: how the ISO was chosen, reported in the view's `exposure`. */
960
+ isoReport: {
961
+ isoControl: "manual" | "automatic";
962
+ autoIso?: SimulateCaptureAutoIsoReport;
963
+ };
964
+ /** #515: the capture's `exposure.standardExposureDeviationStops`. */
965
+ standardExposureDeviationStops: number | null;
966
+ /** The capture's `exposure.standardExposureIlluminanceLux`. */
967
+ standardExposureIlluminanceLux: number | null;
812
968
  sceneOption: unknown;
813
969
  scenePreset: "composite" | "uniform" | "patches" | "slanted-edge" | "siemens-star" | "step-wedge" | "grey-ramp" | "checkerboard" | "point-grid" | "dead-leaves" | "bar-groups" | "hyperbolic-wedge" | "zone-plate" | "slanted-squares" | "line-grid" | "fiducial" | "reflectance-function" | "radiance-function" | "material-function";
814
970
  shutterSeconds: number;
@@ -30,6 +30,7 @@
30
30
  */
31
31
  import { markInternalNativeTileReader, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from "../core/internal-radiance-evaluator.js";
32
32
  import { InvalidConfigurationError } from "../core/configuration-error.js";
33
+ import { InvalidScientificResultError } from "../core/validation.js";
33
34
  import { requireInputObject } from "../core/record-validation.js";
34
35
  import { ENGINE_API_VERSION } from "../core/version.js";
35
36
  import { GENERIC_TIER_ASSETS } from "../equipment/generic-tier-assets.js";
@@ -39,7 +40,8 @@ import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposu
39
40
  import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
40
41
  import { meterRelativeExposure } from "../exposure/metering.js";
41
42
  import { createExposureMeterTargetFromMeteringResult, setExposureCompensationOnMeterTarget } from "../exposure/metering-target.js";
42
- import { resolveManualExposureMode, resolveAperturePriorityExposureMode, resolveShutterPriorityExposureMode, resolveProgramAutoExposureMode } from "../exposure/exposure-mode-resolver.js";
43
+ import { resolveManualExposureMode, resolveAperturePriorityExposureMode, resolveAperturePriorityAutoIsoExposureMode, resolveShutterPriorityExposureMode, resolveProgramAutoExposureMode } from "../exposure/exposure-mode-resolver.js";
44
+ import { estimateAutoIsoSlowestShutter, GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC } from "../exposure/auto-iso-slowest-shutter.js";
43
45
  import { parseExposureProgramLineProfile } from "../exposure/program-line.js";
44
46
  import { resolveCaptureGeometry } from "../output/capture-geometry.js";
45
47
  import { estimateCie1931ColorMatchingFunctions } from "../color/color-matching-functions.js";
@@ -121,6 +123,36 @@ const GREY_CARD_REFLECTANCE = 0.18;
121
123
  function sosReferenceLuminance(fNumber, shutterSeconds, iso) {
122
124
  return SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS * fNumber ** 2 / (CAMERA_EQUATION_CONSTANT * shutterSeconds * iso);
123
125
  }
126
+ /**
127
+ * #515: deviation, in stops, of the exposure the settings give from the CIPA DC-004-2020 standard exposure for the metered
128
+ * luminance L: Δ = log2(N²/t) − log2(0.65·L·S/10) = log2(L_ref(N, t, S) / L), with L_ref the luminance that the settings
129
+ * expose at H = 10/S (S = 10/H, §3.2; H = 0.65·L·t/N², §5.5 (4)). Positive is darker than the standard exposure (under).
130
+ * A scene with no metered luminance (L = 0, which manual mode still captures) has no standard exposure: null, never ±Infinity.
131
+ */
132
+ function standardExposureDeviationStops(fNumber, shutterSeconds, iso, meteredLuminance) {
133
+ if (!(meteredLuminance > 0))
134
+ return null;
135
+ const deviation = Math.log2(sosReferenceLuminance(fNumber, shutterSeconds, iso) / meteredLuminance);
136
+ if (!Number.isFinite(deviation))
137
+ throw new InvalidScientificResultError("exposure.standardExposureDeviationStops is not finite (metered scene luminance " +
138
+ meteredLuminance + " cd/m²).");
139
+ return deviation;
140
+ }
141
+ /**
142
+ * The illuminance at which the settings give the standard exposure, E·L_ref/L (= E·2^Δ). A reflective scene's radiance is
143
+ * reflectance × k(E)·S(λ)/π with k linear in E (calculateIlluminantIrradianceScale), so the metered L is proportional to E
144
+ * and re-metering at this illuminance gives Δ = 0 up to rounding. A radiance-function scene ignores E, and a black scene
145
+ * has no standard exposure: null.
146
+ */
147
+ function standardExposureIlluminanceLux(fNumber, shutterSeconds, iso, meteredLuminance, illuminanceLux, radianceScalesWithIlluminance) {
148
+ if (!radianceScalesWithIlluminance || !(meteredLuminance > 0))
149
+ return null;
150
+ const lux = illuminanceLux * sosReferenceLuminance(fNumber, shutterSeconds, iso) / meteredLuminance;
151
+ if (!Number.isFinite(lux))
152
+ throw new InvalidScientificResultError("exposure.standardExposureIlluminanceLux is not finite (metered scene luminance " +
153
+ meteredLuminance + " cd/m²).");
154
+ return lux;
155
+ }
124
156
  /**
125
157
  * Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
126
158
  * output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
@@ -392,6 +424,92 @@ export function bilinearPhaseProfiles() {
392
424
  kernels: CHANNELS.map((c) => ({ outputChannelId: c, contributions: kernels[px + "," + py][c] })), evidence: FACADE_EVIDENCE,
393
425
  limitations: ["Bilinear interpolation of the RGGB mosaic; no edge-directed demosaic, false-colour suppression or calibration."] } }));
394
426
  }
427
+ /**
428
+ * #516: the result's Auto ISO report, read from a resolved Auto ISO resolution (every mode's resolved Auto ISO branch carries
429
+ * `idealIsoBeforeConstraints`, `isoResolution` and a resolved `targetResidual`; aperture priority adds its fallback flag).
430
+ */
431
+ function autoIsoReport(resolution, iso, shutterPolicy, limits, automatic) {
432
+ const r = resolution;
433
+ // Program reports its hold under slowestPreferredShutter (#504); aperture priority at the top level.
434
+ const program = r.slowestPreferredShutter;
435
+ const ideal = r.idealIsoBeforeConstraints, residual = r.targetResidual?.residualStops, clamped = r.isoResolution?.clamped;
436
+ if (typeof ideal !== "number" || !Number.isFinite(ideal) || typeof residual !== "number" || !Number.isFinite(residual) || r.targetResidual?.status !== "resolved" ||
437
+ (clamped !== false && clamped !== "minimum" && clamped !== "maximum"))
438
+ throw new InvalidScientificResultError("Auto ISO resolution did not report a finite ideal ISO and residual.");
439
+ // The resolvers' Auto ISO range: the body's Auto ISO bounds, else its ISO range.
440
+ return { idealIsoBeforeConstraints: ideal, autoIsoMinimum: iso.autoIsoMinimum ?? iso.minimum, autoIsoMaximum: iso.autoIsoMaximum ?? iso.maximum, clamped,
441
+ ...(limits ? { limits: { ...limits } } : {}), limitingConstraint: r.targetResidual.limitingConstraint, residualStops: residual,
442
+ ...(shutterPolicy ? { ...shutterPolicy, afterMaximumIsoFallbackUsed: (program ? program.afterMaximumIsoFallbackUsed : r.afterMaximumIsoFallbackUsed) === true } : {}),
443
+ ...(program ? { slowestPreferredShutterApplied: program.applied === true } : {}),
444
+ ...(automatic ? { automaticSlowestShutter: automatic } : {}) };
445
+ }
446
+ /**
447
+ * #505: `exposure.autoIso.limits`, the photographer's Auto ISO range. Both limits are selectable ISOs inside the body's
448
+ * Auto ISO range, `minimum` at most `maximum`; anything else fails (the resolvers check the same, but an option defect
449
+ * is a configuration error here). `minimum` defaults to the body's Auto ISO minimum.
450
+ */
451
+ function parseAutoIsoLimitsOption(value, iso) {
452
+ const record = allowKeys(value, "exposure.autoIso.limits", ["maximum", "minimum"]);
453
+ if (record.maximum === undefined)
454
+ fail("exposure.autoIso.limits.maximum is required: the highest ISO Auto ISO may choose.");
455
+ const bodyMinimum = iso.autoIsoMinimum ?? iso.minimum, bodyMaximum = iso.autoIsoMaximum ?? iso.maximum;
456
+ const limit = (field) => {
457
+ const v = positive(record[field], "exposure.autoIso.limits." + field);
458
+ if (v < bodyMinimum || v > bodyMaximum)
459
+ fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be within the body's Auto ISO range, ISO " + bodyMinimum + " to " + bodyMaximum + ".");
460
+ if (iso.settingGrid.kind === "discrete-values" && !iso.settingGrid.values.includes(v))
461
+ fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be a selectable ISO of the body: " + iso.settingGrid.values.join(", ") + ".");
462
+ return v;
463
+ };
464
+ const maximum = limit("maximum"), minimum = record.minimum === undefined ? bodyMinimum : limit("minimum");
465
+ if (minimum > maximum)
466
+ fail("exposure.autoIso.limits.minimum (ISO " + minimum + ") must not exceed exposure.autoIso.limits.maximum (ISO " + maximum + ").");
467
+ return { minimum, maximum };
468
+ }
469
+ /**
470
+ * #504: `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`: the slowest preferred shutter from the camera-shake model
471
+ * (`estimateAutoIsoSlowestShutter`) for this lens, focus and pixel pitch, within the body's shutter range. Option defects are
472
+ * configuration errors; the defaults are recorded as `default` assumptions and the derivation as a `model` assumption.
473
+ */
474
+ function resolveAutomaticSlowestShutterOption(value, context) {
475
+ const path = "exposure.autoIso.automaticSlowestShutter";
476
+ const record = allowKeys(value, path, ["fasterByStops", "blurBudgetPixels", "angularVelocityRadPerSec", "stabilizationStopsEquivalent"]);
477
+ const fasterByStops = context.pick(record, "fasterByStops", path + ".fasterByStops", 0, "No bias: the derived slowest shutter is used as is.");
478
+ if (typeof fasterByStops !== "number" || !Number.isInteger(fasterByStops) || Math.abs(fasterByStops) > 2)
479
+ fail(path + ".fasterByStops must be a whole number of stops from -2 to +2.");
480
+ const blurBudgetPixels = positive(context.pick(record, "blurBudgetPixels", path + ".blurBudgetPixels", 1, "A generic pixel-level blur budget: the shake trace during the exposure may be one pixel long. A choice, not a measured threshold."), path + ".blurBudgetPixels");
481
+ const angularOption = context.pick(record, "angularVelocityRadPerSec", path + ".angularVelocityRadPerSec", { ...GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC }, "Generic hand shake: 4.39 °/s (0.0766 rad/s) on yaw, the linear camera-motion rate Xiao, Farrell, Catrysse and Wandell (Proc. SPIE 7250, 2009) use for an average user " +
482
+ "holding a 100 g mobile handset with two hands. A labelled generic default, not a measurement of this photographer or camera.");
483
+ const angular = allowKeys(angularOption, path + ".angularVelocityRadPerSec", ["yaw", "pitch"]);
484
+ for (const axis of ["yaw", "pitch"]) {
485
+ const v = angular[axis];
486
+ if (typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > 100)
487
+ fail(path + ".angularVelocityRadPerSec." + axis + " must be a finite number within ±100 rad/s.");
488
+ }
489
+ const angularVelocityRadPerSec = { yaw: angular.yaw, pitch: angular.pitch };
490
+ if (angularVelocityRadPerSec.yaw === 0 && angularVelocityRadPerSec.pitch === 0)
491
+ fail(path + ".angularVelocityRadPerSec must be non-zero on at least one axis: without shake no slowest shutter follows.");
492
+ const stabilizationStopsEquivalent = context.pick(record, "stabilizationStopsEquivalent", path + ".stabilizationStopsEquivalent", 0, "No stabilization: the generic tiers declare none.");
493
+ if (typeof stabilizationStopsEquivalent !== "number" || !Number.isFinite(stabilizationStopsEquivalent) || stabilizationStopsEquivalent < 0 || stabilizationStopsEquivalent > 10)
494
+ fail(path + ".stabilizationStopsEquivalent must be a number of stops from 0 to 10.");
495
+ let estimate;
496
+ try {
497
+ estimate = estimateAutoIsoSlowestShutter({ focalLengthMm: context.focalLengthMm, focusDistanceM: context.focusDistanceM, angularVelocityRadPerSec,
498
+ stabilizationStopsEquivalent, blurBudget: { kind: "pixels", pixels: blurBudgetPixels, pixelPitchMicrometers: context.pitch }, fasterByStops,
499
+ shutterCapability: context.shutter }).value;
500
+ }
501
+ catch (error) {
502
+ fail(path + ": " + (error instanceof Error ? error.message : String(error)));
503
+ }
504
+ const report = { derivedShutterSeconds: estimate.derivedShutterSeconds, fasterByStops,
505
+ biasedShutterSeconds: estimate.biasedShutterSeconds, shutterSeconds: estimate.shutterSeconds, blurBudgetPixels, blurAtShutterPixels: estimate.blurAtShutterPixels,
506
+ angularVelocityRadPerSec, stabilizationStopsEquivalent };
507
+ context.assume("exposure.autoIso.automaticSlowestShutter", "model", { ...report, angularVelocityRadPerSec: { ...angularVelocityRadPerSec },
508
+ focalLengthMm: context.focalLengthMm, focusDistanceM: context.focusDistanceM, pixelPitchMicrometers: context.pitch }, "Automatic slowest shutter (#504): the exposure time at which estimateCameraShakeBlur()'s on-axis blur (constant angular velocity, thin-lens projection at the focus " +
509
+ "distance, stabilization as 2^-stops) equals the blur budget, times 2^-fasterByStops, held within the body's shutter range. An approximation; the 1/focal-length " +
510
+ "reciprocal rule is a rule of thumb and is not used.");
511
+ return report;
512
+ }
395
513
  function defaultProgramLine() {
396
514
  // Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
397
515
  // it first opens the aperture toward f/4, then lengthens the shutter. For more light it first closes toward
@@ -1186,11 +1304,18 @@ export async function runCapture(options, run, viewOption, renderView) {
1186
1304
  registerInternalRadianceAccess(provider.evaluateRadiance, { keyOfDirection: materialOfDirection, radianceOfKey: provider.materialRadiance, tabulated: true });
1187
1305
  assume("model.scene-radiometry", "model", "lambertian-plane", "Radiance = reflectance × irradiance / π on one plane; illuminance from K_m = 683 lm/W and the engine's analytic CIE 1931 ȳ fit.");
1188
1306
  // Metering (CIPA DC-004 standard output sensitivity and camera equation) and exposure-mode resolution
1189
- const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops"]);
1307
+ const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops", "autoIso"]);
1190
1308
  const mode = pick(exposureOptions, "mode", "exposure.mode", "manual", "Manual exposure.");
1191
1309
  if (!["manual", "aperture-priority", "shutter-priority", "program"].includes(mode))
1192
1310
  fail("Unknown exposure mode.");
1193
- const iso = positive(pick(exposureOptions, "iso", "exposure.iso", DEFAULTS.iso, "ISO 100, the tiers' base ISO."), "exposure.iso");
1311
+ const isoOption = pick(exposureOptions, "iso", "exposure.iso", DEFAULTS.iso, "ISO 100, the tiers' base ISO.");
1312
+ // #516: "auto" resolves the ISO with the mode's Auto ISO resolver; NaN marks the ISO as not yet known.
1313
+ const automaticIso = isoOption === "auto";
1314
+ if (typeof isoOption === "string" && !automaticIso)
1315
+ fail('exposure.iso must be a positive finite number or "auto".');
1316
+ const iso = automaticIso ? NaN : positive(isoOption, "exposure.iso");
1317
+ if (exposureOptions.autoIso !== undefined && !automaticIso)
1318
+ fail('exposure.autoIso applies only with exposure.iso "auto"; omit it.');
1194
1319
  const compensation = exposureOptions.compensationStops === undefined ? DEFAULTS.compensationStops : exposureOptions.compensationStops;
1195
1320
  if (typeof compensation !== "number" || !Number.isFinite(compensation) || Math.abs(compensation) > 10)
1196
1321
  fail("exposure.compensationStops must be within ±10.");
@@ -1238,9 +1363,68 @@ export async function runCapture(options, run, viewOption, renderView) {
1238
1363
  fail("exposure.shutterSeconds is resolved automatically in " + mode + " mode; omit it.");
1239
1364
  shutterSeconds = NaN;
1240
1365
  }
1366
+ // #516: the resolvers' Auto ISO policies; their baseline is the lowest selectable ISO in the body's Auto ISO range.
1367
+ const autoIsoBaseline = { isoBaseline: "minimum-selectable", isoQuantizationPolicy: "nearest-log2-lower-on-tie" };
1368
+ let autoIsoAperturePriority;
1369
+ // #505: the photographer's Auto ISO limits, validated against the body's Auto ISO range; absent unless given.
1370
+ let autoIsoOptions = {};
1371
+ let autoIsoLimits;
1372
+ // #504: the slowest preferred shutter (aperture priority; optional in program) and, with "auto", how it was derived.
1373
+ let autoIsoShutterPolicy;
1374
+ let automaticSlowestShutter;
1375
+ if (automaticIso) {
1376
+ if (capabilities.iso.autoIsoAvailability !== "supported")
1377
+ fail('exposure.iso "auto" needs a body with Auto ISO; this camera\'s Auto ISO is ' + capabilities.iso.autoIsoAvailability + ".");
1378
+ autoIsoOptions = allowKeys(exposureOptions.autoIso, "exposure.autoIso", ["slowestPreferredShutterSeconds", "afterMaximumIso", "limits", "automaticSlowestShutter"]);
1379
+ if (mode !== "aperture-priority" && mode !== "program")
1380
+ for (const key of ["slowestPreferredShutterSeconds", "afterMaximumIso", "automaticSlowestShutter"])
1381
+ if (autoIsoOptions[key] !== undefined)
1382
+ fail("exposure.autoIso." + key + ' applies only to exposure.mode "aperture-priority" or "program"; omit it.');
1383
+ if (autoIsoOptions.limits !== undefined)
1384
+ autoIsoLimits = parseAutoIsoLimitsOption(autoIsoOptions.limits, capabilities.iso);
1385
+ assume("exposure.autoIso", "model", { minimum: capabilities.iso.autoIsoMinimum ?? capabilities.iso.minimum, maximum: capabilities.iso.autoIsoMaximum ?? capabilities.iso.maximum,
1386
+ ...(autoIsoLimits ? { limits: { ...autoIsoLimits } } : {}), ...autoIsoBaseline }, "Auto ISO: the engine's exposure-mode resolver chooses the ISO from the meter target, starting from the lowest selectable ISO in the body's Auto ISO range and " +
1387
+ "snapping to the nearest selectable ISO in log2 (the lower on a tie), held at the range's ends. A generic policy, not a manufacturer's Auto ISO." +
1388
+ (autoIsoLimits ? " The photographer's limits (exposure.autoIso.limits, #505) narrow that range to ISO " + autoIsoLimits.minimum + " to " + autoIsoLimits.maximum + "." : ""));
1389
+ }
1390
+ if (automaticIso && mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds === undefined) {
1391
+ for (const key of ["afterMaximumIso", "automaticSlowestShutter"])
1392
+ if (autoIsoOptions[key] !== undefined)
1393
+ fail("exposure.autoIso." + key + ' in program needs exposure.autoIso.slowestPreferredShutterSeconds; omit it or set the slowest shutter.');
1394
+ }
1395
+ if (automaticIso && (mode === "aperture-priority" || (mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds !== undefined))) {
1396
+ if (autoIsoOptions.slowestPreferredShutterSeconds === undefined)
1397
+ fail('exposure.autoIso.slowestPreferredShutterSeconds is required with exposure.mode "aperture-priority" and exposure.iso "auto": the slowest shutter, in seconds, before Auto ISO raises the ISO, or "auto".');
1398
+ if (autoIsoOptions.automaticSlowestShutter !== undefined && autoIsoOptions.slowestPreferredShutterSeconds !== "auto")
1399
+ fail('exposure.autoIso.automaticSlowestShutter applies only with exposure.autoIso.slowestPreferredShutterSeconds "auto"; omit it.');
1400
+ let slowestPreferredShutterSeconds;
1401
+ if (autoIsoOptions.slowestPreferredShutterSeconds === "auto") {
1402
+ automaticSlowestShutter = resolveAutomaticSlowestShutterOption(autoIsoOptions.automaticSlowestShutter, { focalLengthMm, focusDistanceM, pitch,
1403
+ shutter: capabilities.shutter, assume, pick });
1404
+ slowestPreferredShutterSeconds = automaticSlowestShutter.shutterSeconds;
1405
+ }
1406
+ else {
1407
+ if (typeof autoIsoOptions.slowestPreferredShutterSeconds === "string")
1408
+ fail('exposure.autoIso.slowestPreferredShutterSeconds must be a positive number of seconds or "auto".');
1409
+ slowestPreferredShutterSeconds = positive(autoIsoOptions.slowestPreferredShutterSeconds, "exposure.autoIso.slowestPreferredShutterSeconds");
1410
+ if (slowestPreferredShutterSeconds < capabilities.shutter.minimumSeconds || slowestPreferredShutterSeconds > capabilities.shutter.maximumSeconds)
1411
+ fail("exposure.autoIso.slowestPreferredShutterSeconds must be within the body's shutter range, " + capabilities.shutter.minimumSeconds + " s to " + capabilities.shutter.maximumSeconds + " s.");
1412
+ }
1413
+ const afterMaximumIso = pick(autoIsoOptions, "afterMaximumIso", "exposure.autoIso.afterMaximumIso", "allow-slower-shutter", "At the Auto ISO maximum, the shutter may get slower than the slowest preferred shutter to reach the meter target.");
1414
+ if (afterMaximumIso !== "allow-slower-shutter" && afterMaximumIso !== "hold-preferred-shutter")
1415
+ fail('exposure.autoIso.afterMaximumIso must be "allow-slower-shutter" or "hold-preferred-shutter".');
1416
+ autoIsoShutterPolicy = { slowestPreferredShutterSeconds, afterMaximumIso };
1417
+ if (mode === "aperture-priority")
1418
+ autoIsoAperturePriority = autoIsoShutterPolicy;
1419
+ }
1420
+ const limitsPolicy = autoIsoLimits ? { autoIsoLimits: { ...autoIsoLimits } } : {};
1241
1421
  if (mode === "manual") {
1242
1422
  resolution = resolveManualExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
1243
- isoControl: { kind: "manual", iso } });
1423
+ isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy, ...limitsPolicy } : { kind: "manual", iso } });
1424
+ }
1425
+ else if (autoIsoAperturePriority) {
1426
+ resolution = resolveAperturePriorityAutoIsoExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber,
1427
+ policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority, ...limitsPolicy } });
1244
1428
  }
1245
1429
  else if (mode === "aperture-priority") {
1246
1430
  resolution = resolveAperturePriorityExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
@@ -1248,17 +1432,25 @@ export async function runCapture(options, run, viewOption, renderView) {
1248
1432
  }
1249
1433
  else if (mode === "shutter-priority") {
1250
1434
  resolution = resolveShutterPriorityExposureMode({ target, capabilities, referenceExposure: reference, manualShutterSeconds: shutterSeconds,
1251
- isoControl: { kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
1435
+ isoControl: automaticIso ? { kind: "automatic", policy: { kind: "minimum-iso-aperture-first", apertureSelectionPolicy: "not-wider-than-target", ...autoIsoBaseline, ...limitsPolicy } } :
1436
+ { kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
1252
1437
  }
1253
1438
  else {
1254
1439
  assume("exposure.programLine", "model", "facade-generic-program-line", "Engine-authored generic program line (f/16 1/2000 s … f/8 1/125 s … f/4 1.024 s); not a manufacturer program.");
1255
- resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(), isoControl: { kind: "manual", iso } });
1440
+ resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(),
1441
+ isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
1442
+ { kind: "manual", iso } });
1256
1443
  }
1257
1444
  if (resolution.status !== "resolved" || !resolution.resolvedSettings)
1258
1445
  fail("Exposure could not be resolved (" + resolution.status + "); the scene may have no signal.");
1259
1446
  fNumber = resolution.resolvedSettings.aperture ?? fNumber;
1260
1447
  shutterSeconds = resolution.resolvedSettings.shutterSeconds ?? shutterSeconds;
1261
1448
  const resolvedIso = resolution.resolvedSettings.iso ?? iso;
1449
+ if (!Number.isFinite(resolvedIso) || resolvedIso <= 0)
1450
+ fail("Exposure could not be resolved: the resolved ISO is not a positive finite number.");
1451
+ const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution, capabilities.iso, autoIsoShutterPolicy, autoIsoLimits, automaticSlowestShutter) } : { isoControl: "manual" };
1452
+ const exposureDeviationStops = standardExposureDeviationStops(fNumber, shutterSeconds, resolvedIso, meteredLuminance);
1453
+ const exposureIlluminanceLux = standardExposureIlluminanceLux(fNumber, shutterSeconds, resolvedIso, meteredLuminance, illuminanceLux, callerScene?.kind !== "radiance-function");
1262
1454
  const defocusDisc = defocusModel === undefined ? undefined :
1263
1455
  resolveDefocusOption({ run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch, diffraction: diffractionRadiusOption !== undefined });
1264
1456
  // #492: the capture applies the disc as an object-space convolution, which needs every disc point a site sees to be another site's scene point.
@@ -1699,7 +1891,7 @@ export async function runCapture(options, run, viewOption, renderView) {
1699
1891
  // Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
1700
1892
  // that only captures does not contain the display view.
1701
1893
  if (displayView)
1702
- return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
1894
+ return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, standardExposureIlluminanceLux: exposureIlluminanceLux, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
1703
1895
  // Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
1704
1896
  if (idealReference) {
1705
1897
  const referenceStarted = clockMs();
@@ -1772,7 +1964,7 @@ export async function runCapture(options, run, viewOption, renderView) {
1772
1964
  "Each pixel samples the scene at its centre, like the capture's default point aperture; use sensor.pixelAperture \"box\" for photosite-averaged pixels.");
1773
1965
  return Object.freeze({ kind: "ideal-reference", label: "Ideal reference (no camera effects); not a capture.", facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
1774
1966
  image: { width: outputWidth, height: outputHeight, format, samples, ...(png ? { png, colorEncoding: colorEncoding } : {}) },
1775
- exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds) },
1967
+ exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport },
1776
1968
  scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
1777
1969
  illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
1778
1970
  framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
@@ -2062,8 +2254,9 @@ export async function runCapture(options, run, viewOption, renderView) {
2062
2254
  ...(region && regionNative ? { region: { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
2063
2255
  request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
2064
2256
  ...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" } } : {}) },
2065
- exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds),
2066
- meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, resolution },
2257
+ exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
2258
+ meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
2259
+ standardExposureIlluminanceLux: exposureIlluminanceLux, resolution },
2067
2260
  scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
2068
2261
  illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
2069
2262
  framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },