@photivra/engine 2.16.0 → 2.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +24 -0
- package/dist/camera/lens-projection.d.ts +5 -0
- package/dist/camera/lens-projection.js +10 -0
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/camera/sensor-equivalence.d.ts +156 -0
- package/dist/camera/sensor-equivalence.js +208 -0
- package/dist/camera/sensor-equivalence.js.map +1 -0
- package/dist/capture/native-environment-tabulated.d.ts +5 -0
- package/dist/capture/native-environment-tabulated.js +1 -1
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/generic-lens-catalogue.d.ts +227 -0
- package/dist/equipment/generic-lens-catalogue.js +243 -0
- package/dist/equipment/generic-lens-catalogue.js.map +1 -0
- package/dist/equipment/ideal-lens.d.ts +25 -0
- package/dist/equipment/ideal-lens.js +21 -0
- package/dist/equipment/ideal-lens.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +94 -0
- package/dist/facade/catalogue-lens-signal.js +362 -0
- package/dist/facade/catalogue-lens-signal.js.map +1 -0
- package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
- package/dist/facade/defocus-bokeh-zones.js +53 -0
- package/dist/facade/defocus-bokeh-zones.js.map +1 -0
- package/dist/facade/defocus-image-space-provider.d.ts +9 -0
- package/dist/facade/defocus-image-space-provider.js +101 -9
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +29 -0
- package/dist/facade/diffraction-provider.js +117 -11
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +4 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +10 -0
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +61 -0
- package/dist/facade/display-view-lens-signal.js +502 -0
- package/dist/facade/display-view-lens-signal.js.map +1 -0
- package/dist/facade/expected-signal-setup.d.ts +15 -0
- package/dist/facade/expected-signal-setup.js +19 -2
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +13 -0
- package/dist/facade/expected-signal.js +155 -1
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +7 -0
- package/dist/facade/generic-lens-capture.js +9 -0
- package/dist/facade/generic-lens-capture.js.map +1 -0
- package/dist/facade/large-defocus-lattice.d.ts +39 -0
- package/dist/facade/large-defocus-lattice.js +64 -0
- package/dist/facade/large-defocus-lattice.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +5 -0
- package/dist/facade/scene-radiance.js +5 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +67 -7
- package/dist/facade/simulate-capture.js +400 -64
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +23 -17
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +0 -9
- package/dist/gpu/display-view-quality-gpu.js +6 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.js +2 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
- package/dist/optics/defocus-bokeh-kernel.js +380 -0
- package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
- package/dist/optics/defocus-disc-kernel.d.ts +41 -19
- package/dist/optics/defocus-disc-kernel.js +44 -23
- package/dist/optics/defocus-disc-kernel.js.map +1 -1
- package/dist/optics/generic-lens-psf.d.ts +68 -0
- package/dist/optics/generic-lens-psf.js +337 -0
- package/dist/optics/generic-lens-psf.js.map +1 -0
- package/dist/optics/illumination-vignetting.d.ts +6 -0
- package/dist/optics/illumination-vignetting.js +24 -7
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/sensor-environment-query.d.ts +6 -0
- package/dist/optics/sensor-environment-query.js +13 -6
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-kernel-runs.d.ts +77 -0
- package/dist/optics/site-kernel-runs.js +200 -0
- package/dist/optics/site-kernel-runs.js.map +1 -0
- package/dist/output/generic-lens-corrections.d.ts +24 -0
- package/dist/output/generic-lens-corrections.js +51 -0
- package/dist/output/generic-lens-corrections.js.map +1 -0
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +77 -1
- package/docs/QUICK_START.md +305 -17
- package/docs/USAGE.md +98 -5
- package/docs/api/camera-sensor-equivalence.md +209 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +281 -0
- package/docs/api/equipment-ideal-lens.md +26 -0
- package/docs/api/exports.json +202 -2
- package/docs/api/facade-simulate-capture.md +48 -4
- package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
- package/docs/api/optics-defocus-disc-kernel.md +22 -15
- package/docs/api/optics-sensor-environment-query.md +6 -0
- package/docs/api/output-generic-lens-corrections.md +39 -0
- package/llms.txt +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Format equivalence for teaching: what a lens of focal length f at f-number N on one sensor
|
|
3
|
+
* corresponds to on a reference format (36 × 24 mm by default), and how large a blur circle is
|
|
4
|
+
* as a share of the frame or of a print.
|
|
5
|
+
*
|
|
6
|
+
* With the diagonal crop factor k = referenceDiagonal / sensorDiagonal:
|
|
7
|
+
* - equivalent focal length k·f (same diagonal angle of view; exact by the diagonal convention);
|
|
8
|
+
* - equivalent f-number k·N for depth of field, background blur and total light (same framing,
|
|
9
|
+
* same output size). It keeps the entrance-pupil diameter f/N unchanged. It is an
|
|
10
|
+
* approximation: in the thin-lens defocus-circle equation it is exact only when the focus
|
|
11
|
+
* distance is much greater than the focal length;
|
|
12
|
+
* - the f-number for exposure stays N: image illuminance depends on N, not on the sensor size;
|
|
13
|
+
* - sensor area ratio and the total-light difference in stops at the same f-number and shutter.
|
|
14
|
+
*
|
|
15
|
+
* Sources (free): thin-lens defocus-circle diameter f²|s − d|/(N d (s − f)) and f-number N = f/D,
|
|
16
|
+
* Greivenkamp, OPTI-502 course notes, sec. 12 (Objectives),
|
|
17
|
+
* https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-12-Objectives.pdf;
|
|
18
|
+
* image illuminance proportional to 1/N², sec. 11 (Radiative transfer),
|
|
19
|
+
* https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-11-Radiative-Transfer.pdf.
|
|
20
|
+
* @see docs/PHYSICS_FOUNDATION.md "Sensor equivalence" for the derivation, the exact and
|
|
21
|
+
* approximated quantities, and support limits.
|
|
22
|
+
*/
|
|
23
|
+
import { type CalculationResult } from "../core/calculation-result.js";
|
|
24
|
+
import { type SensorImagingArea } from "../sensor/sensor-geometry.js";
|
|
25
|
+
/** Whether a value is exact under its stated conditions or an approximation. */
|
|
26
|
+
export type EquivalenceExactness = "exact" | "approximation";
|
|
27
|
+
/** How one equivalence quantity was obtained, and under which conditions it holds. */
|
|
28
|
+
export interface EquivalenceValueBasis {
|
|
29
|
+
/** `"exact"` under the stated conditions, or `"approximation"`. */
|
|
30
|
+
kind: EquivalenceExactness;
|
|
31
|
+
/** Plain statement of the definition and its conditions. */
|
|
32
|
+
statement: string;
|
|
33
|
+
}
|
|
34
|
+
export interface CalculateSensorEquivalenceInput {
|
|
35
|
+
/** Physical active imaging area of the sensor the lens is used on. */
|
|
36
|
+
sensor: SensorImagingArea;
|
|
37
|
+
/** Physical focal length of the lens in millimetres. */
|
|
38
|
+
focalLengthMm: number;
|
|
39
|
+
/** f-number set on the lens (N = focal length / entrance-pupil diameter). */
|
|
40
|
+
fNumber: number;
|
|
41
|
+
/** Format to compare against. Defaults to the 36 × 24 mm frame. */
|
|
42
|
+
reference?: SensorImagingArea;
|
|
43
|
+
}
|
|
44
|
+
/** Angles of view of an ideal rectilinear lens focused at infinity, in degrees. */
|
|
45
|
+
export interface SensorEquivalenceFieldOfView {
|
|
46
|
+
horizontalDegrees: number;
|
|
47
|
+
verticalDegrees: number;
|
|
48
|
+
diagonalDegrees: number;
|
|
49
|
+
}
|
|
50
|
+
/** Quantities of a sensor equivalence result that carry an exactness statement. */
|
|
51
|
+
export type SensorEquivalenceQuantity = "cropFactor" | "equivalentFocalLengthMm" | "equivalentFNumber" | "exposureFNumber" | "entrancePupilDiameterMm" | "areaRatio" | "totalLightDifferenceStops" | "fieldOfView";
|
|
52
|
+
export interface SensorEquivalence {
|
|
53
|
+
/** Physical focal length supplied by the caller, in millimetres. */
|
|
54
|
+
focalLengthMm: number;
|
|
55
|
+
/** f-number supplied by the caller. */
|
|
56
|
+
fNumber: number;
|
|
57
|
+
/** Sensor active imaging area used. */
|
|
58
|
+
sensor: SensorImagingArea;
|
|
59
|
+
/** Reference format used (36 × 24 mm unless the caller passed one). */
|
|
60
|
+
reference: SensorImagingArea;
|
|
61
|
+
/** Sensor diagonal in millimetres. */
|
|
62
|
+
sensorDiagonalMm: number;
|
|
63
|
+
/** Reference diagonal in millimetres. */
|
|
64
|
+
referenceDiagonalMm: number;
|
|
65
|
+
/** Diagonal crop factor: reference diagonal / sensor diagonal. Greater than 1 for a smaller sensor. */
|
|
66
|
+
cropFactor: number;
|
|
67
|
+
/** Focal length on the reference format with the same diagonal angle of view: cropFactor × focalLengthMm. */
|
|
68
|
+
equivalentFocalLengthMm: number;
|
|
69
|
+
/**
|
|
70
|
+
* f-number on the reference format that gives the same depth of field, background blur and total
|
|
71
|
+
* light for the same framing and output size: cropFactor × fNumber. Not an exposure setting.
|
|
72
|
+
*/
|
|
73
|
+
equivalentFNumber: number;
|
|
74
|
+
/** f-number that sets exposure (image brightness): the f-number on the lens, unchanged by sensor size. */
|
|
75
|
+
exposureFNumber: number;
|
|
76
|
+
/** Entrance-pupil diameter f/N in millimetres; the equivalent lens has the same diameter. */
|
|
77
|
+
entrancePupilDiameterMm: number;
|
|
78
|
+
/** Sensor area / reference area. Less than 1 for a smaller sensor. */
|
|
79
|
+
areaRatio: number;
|
|
80
|
+
/**
|
|
81
|
+
* log2(areaRatio): total light collected by the sensor relative to the reference at the same
|
|
82
|
+
* f-number, shutter time and scene, in stops. Negative for a smaller sensor.
|
|
83
|
+
*/
|
|
84
|
+
totalLightDifferenceStops: number;
|
|
85
|
+
/** Angles of view of this lens on the sensor (the equivalent lens gives the same diagonal angle). */
|
|
86
|
+
fieldOfView: SensorEquivalenceFieldOfView;
|
|
87
|
+
/** Exact or approximated, with the conditions, for every quantity above. */
|
|
88
|
+
basis: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>>;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Equivalence of a lens (focal length, f-number) on one sensor against a reference format,
|
|
92
|
+
* 36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of
|
|
93
|
+
* field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light
|
|
94
|
+
* difference in stops and the angles of view.
|
|
95
|
+
*
|
|
96
|
+
* Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it
|
|
97
|
+
* is exact under its stated conditions or an approximation. The equivalent f-number is an
|
|
98
|
+
* approximation (exact when the focus distance is much greater than the focal length) and never an
|
|
99
|
+
* exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.
|
|
100
|
+
*
|
|
101
|
+
* Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.
|
|
102
|
+
*
|
|
103
|
+
* @param input Sensor active area, focal length, f-number and optional reference format.
|
|
104
|
+
* @returns The equivalence numbers with a per-quantity exactness statement.
|
|
105
|
+
*/
|
|
106
|
+
export declare function calculateSensorEquivalence(input: CalculateSensorEquivalenceInput): CalculationResult<SensorEquivalence>;
|
|
107
|
+
export interface CalculateBlurShareOfFrameInput {
|
|
108
|
+
/** Blur-circle diameter on the sensor in millimetres (for example from calculateDefocusCircle). Zero is allowed. */
|
|
109
|
+
blurCircleMm: number;
|
|
110
|
+
/** Physical active imaging area of the sensor. */
|
|
111
|
+
sensor: SensorImagingArea;
|
|
112
|
+
/**
|
|
113
|
+
* Optional print size. The whole frame is enlarged to fit inside it without cropping, long side
|
|
114
|
+
* to long side.
|
|
115
|
+
*/
|
|
116
|
+
print?: {
|
|
117
|
+
widthMm: number;
|
|
118
|
+
heightMm: number;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export interface BlurShareOfPrint {
|
|
122
|
+
/** Linear enlargement from sensor to print: the smaller of the long-side and short-side ratios. */
|
|
123
|
+
enlargement: number;
|
|
124
|
+
/** Blur-circle diameter on the print in millimetres: blurCircleMm × enlargement. */
|
|
125
|
+
blurOnPrintMm: number;
|
|
126
|
+
/** Size of the printed image in millimetres (equal to the print when the aspect ratios match). */
|
|
127
|
+
imageLongSideMm: number;
|
|
128
|
+
imageShortSideMm: number;
|
|
129
|
+
}
|
|
130
|
+
export interface BlurShareOfFrame {
|
|
131
|
+
/** Blur-circle diameter on the sensor in millimetres. */
|
|
132
|
+
blurCircleMm: number;
|
|
133
|
+
/** Sensor diagonal in millimetres. */
|
|
134
|
+
frameDiagonalMm: number;
|
|
135
|
+
/** Blur diameter / frame diagonal. Equal shares look the same at the same output size. */
|
|
136
|
+
shareOfDiagonal: number;
|
|
137
|
+
/** Blur diameter / frame width. */
|
|
138
|
+
shareOfWidth: number;
|
|
139
|
+
/** Blur diameter / frame height. */
|
|
140
|
+
shareOfHeight: number;
|
|
141
|
+
/** Blur on the print; present only when a print size was given. */
|
|
142
|
+
print?: BlurShareOfPrint;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a
|
|
146
|
+
* print size is given, its diameter on that print.
|
|
147
|
+
*
|
|
148
|
+
* Equal shares of the diagonal look the same when the images are shown at the same size, which
|
|
149
|
+
* is how "same framing" comparisons across sensor sizes are stated. Every value is an exact ratio
|
|
150
|
+
* or scaling; whether a blur is visible depends on viewing distance and acuity, which this
|
|
151
|
+
* function does not judge.
|
|
152
|
+
*
|
|
153
|
+
* @param input Blur diameter in millimetres, sensor active area and optional print size.
|
|
154
|
+
* @returns Shares of the frame, and the blur on the print when a print is given.
|
|
155
|
+
*/
|
|
156
|
+
export declare function calculateBlurShareOfFrame(input: CalculateBlurShareOfFrameInput): CalculationResult<BlurShareOfFrame>;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Format equivalence for teaching: what a lens of focal length f at f-number N on one sensor
|
|
4
|
+
* corresponds to on a reference format (36 × 24 mm by default), and how large a blur circle is
|
|
5
|
+
* as a share of the frame or of a print.
|
|
6
|
+
*
|
|
7
|
+
* With the diagonal crop factor k = referenceDiagonal / sensorDiagonal:
|
|
8
|
+
* - equivalent focal length k·f (same diagonal angle of view; exact by the diagonal convention);
|
|
9
|
+
* - equivalent f-number k·N for depth of field, background blur and total light (same framing,
|
|
10
|
+
* same output size). It keeps the entrance-pupil diameter f/N unchanged. It is an
|
|
11
|
+
* approximation: in the thin-lens defocus-circle equation it is exact only when the focus
|
|
12
|
+
* distance is much greater than the focal length;
|
|
13
|
+
* - the f-number for exposure stays N: image illuminance depends on N, not on the sensor size;
|
|
14
|
+
* - sensor area ratio and the total-light difference in stops at the same f-number and shutter.
|
|
15
|
+
*
|
|
16
|
+
* Sources (free): thin-lens defocus-circle diameter f²|s − d|/(N d (s − f)) and f-number N = f/D,
|
|
17
|
+
* Greivenkamp, OPTI-502 course notes, sec. 12 (Objectives),
|
|
18
|
+
* https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-12-Objectives.pdf;
|
|
19
|
+
* image illuminance proportional to 1/N², sec. 11 (Radiative transfer),
|
|
20
|
+
* https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-11-Radiative-Transfer.pdf.
|
|
21
|
+
* @see docs/PHYSICS_FOUNDATION.md "Sensor equivalence" for the derivation, the exact and
|
|
22
|
+
* approximated quantities, and support limits.
|
|
23
|
+
*/
|
|
24
|
+
import { approximationResult, calculatedResult } from "../core/calculation-result.js";
|
|
25
|
+
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
26
|
+
import { portableHypot, portableLog2 } from "../core/portable-math.js";
|
|
27
|
+
import { requireInputKeys, requireInputObject } from "../core/record-validation.js";
|
|
28
|
+
import { calculateImagingAreaMetrics } from "../sensor/sensor-geometry.js";
|
|
29
|
+
/** The default equivalence reference: the 36 × 24 mm frame. */
|
|
30
|
+
const REFERENCE_FULL_FRAME = Object.freeze({ widthMm: 36, heightMm: 24 });
|
|
31
|
+
const SENSOR_EQUIVALENCE_BASIS = Object.freeze({
|
|
32
|
+
cropFactor: Object.freeze({
|
|
33
|
+
kind: "exact",
|
|
34
|
+
statement: "Ratio of the reference diagonal to the sensor diagonal (diagonal convention)."
|
|
35
|
+
}),
|
|
36
|
+
equivalentFocalLengthMm: Object.freeze({
|
|
37
|
+
kind: "exact",
|
|
38
|
+
statement: "Same diagonal angle of view on the reference format, for an ideal rectilinear lens focused at infinity. " +
|
|
39
|
+
"Horizontal and vertical angles match too only when the aspect ratios match."
|
|
40
|
+
}),
|
|
41
|
+
equivalentFNumber: Object.freeze({
|
|
42
|
+
kind: "approximation",
|
|
43
|
+
statement: "Same depth of field and background blur as a share of the frame, at the same subject distance, framing and output size, " +
|
|
44
|
+
"and the same entrance-pupil diameter (so the same total light at the same shutter time). In the thin-lens defocus equation it is exact " +
|
|
45
|
+
"only when the focus distance s is much greater than the focal length f; with focal lengths in the ratio cropFactor, the blur shares match " +
|
|
46
|
+
"at cropFactor·N·(s − f)/(s − cropFactor·f). " +
|
|
47
|
+
"Total light matches exactly only when the aspect ratios match. It does not set exposure."
|
|
48
|
+
}),
|
|
49
|
+
exposureFNumber: Object.freeze({
|
|
50
|
+
kind: "exact",
|
|
51
|
+
statement: "Image illuminance is proportional to 1/N², independent of sensor size (ideal lens; transmission, vignetting and " +
|
|
52
|
+
"close-focus bellows factor excluded). Equivalence never changes the exposure setting."
|
|
53
|
+
}),
|
|
54
|
+
entrancePupilDiameterMm: Object.freeze({
|
|
55
|
+
kind: "exact",
|
|
56
|
+
statement: "Entrance-pupil diameter D = f/N by the definition of the f-number. The equivalent lens (cropFactor·f at cropFactor·N) has the same D."
|
|
57
|
+
}),
|
|
58
|
+
areaRatio: Object.freeze({
|
|
59
|
+
kind: "exact",
|
|
60
|
+
statement: "Product of width and height of the sensor divided by that of the reference."
|
|
61
|
+
}),
|
|
62
|
+
totalLightDifferenceStops: Object.freeze({
|
|
63
|
+
kind: "exact",
|
|
64
|
+
statement: "log2 of the area ratio: at the same f-number, shutter time and scene the image illuminance is the same, so total light scales " +
|
|
65
|
+
"with area (uniform illuminance; vignetting excluded)."
|
|
66
|
+
}),
|
|
67
|
+
fieldOfView: Object.freeze({
|
|
68
|
+
kind: "exact",
|
|
69
|
+
statement: "2·atan(dimension / 2f) for an ideal rectilinear lens focused at infinity; distortion and focus breathing excluded."
|
|
70
|
+
})
|
|
71
|
+
});
|
|
72
|
+
function requireImagingArea(area, label) {
|
|
73
|
+
requireInputObject(area, label);
|
|
74
|
+
requireInputKeys(area, label, ["widthMm", "heightMm"]);
|
|
75
|
+
const record = area;
|
|
76
|
+
requirePositiveFinite(`${label}.widthMm`, record.widthMm);
|
|
77
|
+
requirePositiveFinite(`${label}.heightMm`, record.heightMm);
|
|
78
|
+
}
|
|
79
|
+
/** √((rw² + rh²)/(w² + h²)), with only correctly rounded operations when nothing overflows (portable). */
|
|
80
|
+
function diagonalRatio(reference, sensor) {
|
|
81
|
+
const referenceSquared = reference.widthMm * reference.widthMm + reference.heightMm * reference.heightMm;
|
|
82
|
+
const sensorSquared = sensor.widthMm * sensor.widthMm + sensor.heightMm * sensor.heightMm;
|
|
83
|
+
const ratio = referenceSquared / sensorSquared;
|
|
84
|
+
return Number.isFinite(referenceSquared) && Number.isFinite(sensorSquared) && sensorSquared > 0 && ratio > 0 && Number.isFinite(ratio)
|
|
85
|
+
? Math.sqrt(ratio)
|
|
86
|
+
: portableHypot(reference.widthMm, reference.heightMm) / portableHypot(sensor.widthMm, sensor.heightMm);
|
|
87
|
+
}
|
|
88
|
+
function rectilinearAngleDegrees(dimensionMm, focalLengthMm) {
|
|
89
|
+
return (2 * Math.atan(dimensionMm / (2 * focalLengthMm)) * 180) / Math.PI;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Equivalence of a lens (focal length, f-number) on one sensor against a reference format,
|
|
93
|
+
* 36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of
|
|
94
|
+
* field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light
|
|
95
|
+
* difference in stops and the angles of view.
|
|
96
|
+
*
|
|
97
|
+
* Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it
|
|
98
|
+
* is exact under its stated conditions or an approximation. The equivalent f-number is an
|
|
99
|
+
* approximation (exact when the focus distance is much greater than the focal length) and never an
|
|
100
|
+
* exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.
|
|
101
|
+
*
|
|
102
|
+
* Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.
|
|
103
|
+
*
|
|
104
|
+
* @param input Sensor active area, focal length, f-number and optional reference format.
|
|
105
|
+
* @returns The equivalence numbers with a per-quantity exactness statement.
|
|
106
|
+
*/
|
|
107
|
+
export function calculateSensorEquivalence(input) {
|
|
108
|
+
requireInputKeys(input, "calculateSensorEquivalence", ["sensor", "focalLengthMm", "fNumber", "reference"]);
|
|
109
|
+
requireImagingArea(input.sensor, "sensor");
|
|
110
|
+
requirePositiveFinite("focalLengthMm", input.focalLengthMm);
|
|
111
|
+
requirePositiveFinite("fNumber", input.fNumber);
|
|
112
|
+
if (input.reference !== undefined)
|
|
113
|
+
requireImagingArea(input.reference, "reference");
|
|
114
|
+
const sensor = { widthMm: input.sensor.widthMm, heightMm: input.sensor.heightMm };
|
|
115
|
+
const reference = input.reference === undefined
|
|
116
|
+
? { ...REFERENCE_FULL_FRAME }
|
|
117
|
+
: { widthMm: input.reference.widthMm, heightMm: input.reference.heightMm };
|
|
118
|
+
// The default reference reuses the canonical 35 mm crop factor, so it is bit-identical to
|
|
119
|
+
// calculateEquivalentFocalLength35Mm and calculateImagingAreaMetrics.
|
|
120
|
+
const cropFactor = input.reference === undefined
|
|
121
|
+
? calculateImagingAreaMetrics(sensor).value.cropFactor35Mm
|
|
122
|
+
: diagonalRatio(reference, sensor);
|
|
123
|
+
const areaRatio = (sensor.widthMm / reference.widthMm) * (sensor.heightMm / reference.heightMm);
|
|
124
|
+
const sensorDiagonalMm = portableHypot(sensor.widthMm, sensor.heightMm);
|
|
125
|
+
if (!(areaRatio > 0) || !Number.isFinite(areaRatio)) {
|
|
126
|
+
throw new InvalidScientificInputError("sensor and reference areas must differ by a ratio that is a finite number greater than zero.");
|
|
127
|
+
}
|
|
128
|
+
return approximationResult({
|
|
129
|
+
focalLengthMm: input.focalLengthMm,
|
|
130
|
+
fNumber: input.fNumber,
|
|
131
|
+
sensor,
|
|
132
|
+
reference,
|
|
133
|
+
sensorDiagonalMm,
|
|
134
|
+
referenceDiagonalMm: portableHypot(reference.widthMm, reference.heightMm),
|
|
135
|
+
cropFactor,
|
|
136
|
+
equivalentFocalLengthMm: input.focalLengthMm * cropFactor,
|
|
137
|
+
equivalentFNumber: input.fNumber * cropFactor,
|
|
138
|
+
exposureFNumber: input.fNumber,
|
|
139
|
+
entrancePupilDiameterMm: input.focalLengthMm / input.fNumber,
|
|
140
|
+
areaRatio,
|
|
141
|
+
totalLightDifferenceStops: portableLog2(areaRatio),
|
|
142
|
+
fieldOfView: {
|
|
143
|
+
horizontalDegrees: rectilinearAngleDegrees(sensor.widthMm, input.focalLengthMm),
|
|
144
|
+
verticalDegrees: rectilinearAngleDegrees(sensor.heightMm, input.focalLengthMm),
|
|
145
|
+
diagonalDegrees: rectilinearAngleDegrees(sensorDiagonalMm, input.focalLengthMm)
|
|
146
|
+
},
|
|
147
|
+
basis: SENSOR_EQUIVALENCE_BASIS
|
|
148
|
+
}, "sensor-equivalence", "1.0.0", [
|
|
149
|
+
"Crop factor and equivalence are diagonal-based against the reference format (36 × 24 mm unless given).",
|
|
150
|
+
"Equivalent f-number concerns depth of field, background blur and total light for the same framing and output size; it is not an exposure setting.",
|
|
151
|
+
"Equivalent f-number is a thin-lens approximation, exact when the focus distance is much greater than the focal length.",
|
|
152
|
+
"Exposure f-number is the f-number on the lens: image illuminance does not depend on sensor size.",
|
|
153
|
+
"Ideal rectilinear lens focused at infinity; distortion, vignetting, transmission and focus breathing excluded.",
|
|
154
|
+
"Physical focal length and f-number are not changed; see basis for each quantity."
|
|
155
|
+
]);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a
|
|
159
|
+
* print size is given, its diameter on that print.
|
|
160
|
+
*
|
|
161
|
+
* Equal shares of the diagonal look the same when the images are shown at the same size, which
|
|
162
|
+
* is how "same framing" comparisons across sensor sizes are stated. Every value is an exact ratio
|
|
163
|
+
* or scaling; whether a blur is visible depends on viewing distance and acuity, which this
|
|
164
|
+
* function does not judge.
|
|
165
|
+
*
|
|
166
|
+
* @param input Blur diameter in millimetres, sensor active area and optional print size.
|
|
167
|
+
* @returns Shares of the frame, and the blur on the print when a print is given.
|
|
168
|
+
*/
|
|
169
|
+
export function calculateBlurShareOfFrame(input) {
|
|
170
|
+
requireInputKeys(input, "calculateBlurShareOfFrame", ["blurCircleMm", "sensor", "print"]);
|
|
171
|
+
if (typeof input.blurCircleMm !== "number" || !Number.isFinite(input.blurCircleMm) || input.blurCircleMm < 0) {
|
|
172
|
+
throw new InvalidScientificInputError("blurCircleMm must be a finite number greater than or equal to zero.");
|
|
173
|
+
}
|
|
174
|
+
requireImagingArea(input.sensor, "sensor");
|
|
175
|
+
if (input.print !== undefined)
|
|
176
|
+
requireImagingArea(input.print, "print");
|
|
177
|
+
const { widthMm, heightMm } = input.sensor;
|
|
178
|
+
const blurCircleMm = input.blurCircleMm;
|
|
179
|
+
const frameDiagonalMm = portableHypot(widthMm, heightMm);
|
|
180
|
+
let print;
|
|
181
|
+
if (input.print !== undefined) {
|
|
182
|
+
const sensorLong = Math.max(widthMm, heightMm), sensorShort = Math.min(widthMm, heightMm);
|
|
183
|
+
const printLong = Math.max(input.print.widthMm, input.print.heightMm), printShort = Math.min(input.print.widthMm, input.print.heightMm);
|
|
184
|
+
const enlargement = Math.min(printLong / sensorLong, printShort / sensorShort);
|
|
185
|
+
if (!(enlargement > 0) || !Number.isFinite(enlargement)) {
|
|
186
|
+
throw new InvalidScientificInputError("print enlargement must be a finite number greater than zero.");
|
|
187
|
+
}
|
|
188
|
+
print = {
|
|
189
|
+
enlargement,
|
|
190
|
+
blurOnPrintMm: blurCircleMm * enlargement,
|
|
191
|
+
imageLongSideMm: sensorLong * enlargement,
|
|
192
|
+
imageShortSideMm: sensorShort * enlargement
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
return calculatedResult({
|
|
196
|
+
blurCircleMm,
|
|
197
|
+
frameDiagonalMm,
|
|
198
|
+
shareOfDiagonal: blurCircleMm / frameDiagonalMm,
|
|
199
|
+
shareOfWidth: blurCircleMm / widthMm,
|
|
200
|
+
shareOfHeight: blurCircleMm / heightMm,
|
|
201
|
+
...(print === undefined ? {} : { print })
|
|
202
|
+
}, "blur-share-of-frame", "1.0.0", [
|
|
203
|
+
"Shares are exact ratios of the blur diameter to the active frame's diagonal, width and height.",
|
|
204
|
+
"A print shows the whole frame enlarged to fit inside it without cropping, long side to long side.",
|
|
205
|
+
"Visibility of the blur depends on viewing distance and acuity; no acceptability criterion is applied."
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=sensor-equivalence.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensor-equivalence.js","sourceRoot":"","sources":["../../src/camera/sensor-equivalence.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Format equivalence for teaching: what a lens of focal length f at f-number N on one sensor\n * corresponds to on a reference format (36 × 24 mm by default), and how large a blur circle is\n * as a share of the frame or of a print.\n *\n * With the diagonal crop factor k = referenceDiagonal / sensorDiagonal:\n * - equivalent focal length k·f (same diagonal angle of view; exact by the diagonal convention);\n * - equivalent f-number k·N for depth of field, background blur and total light (same framing,\n * same output size). It keeps the entrance-pupil diameter f/N unchanged. It is an\n * approximation: in the thin-lens defocus-circle equation it is exact only when the focus\n * distance is much greater than the focal length;\n * - the f-number for exposure stays N: image illuminance depends on N, not on the sensor size;\n * - sensor area ratio and the total-light difference in stops at the same f-number and shutter.\n *\n * Sources (free): thin-lens defocus-circle diameter f²|s − d|/(N d (s − f)) and f-number N = f/D,\n * Greivenkamp, OPTI-502 course notes, sec. 12 (Objectives),\n * https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-12-Objectives.pdf;\n * image illuminance proportional to 1/N², sec. 11 (Radiative transfer),\n * https://wp.optics.arizona.edu/jgreivenkamp/wp-content/uploads/sites/11/2019/08/502-11-Radiative-Transfer.pdf.\n * @see docs/PHYSICS_FOUNDATION.md \"Sensor equivalence\" for the derivation, the exact and\n * approximated quantities, and support limits.\n */\n\nimport {\n approximationResult,\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { portableHypot, portableLog2 } from \"../core/portable-math.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { calculateImagingAreaMetrics, type SensorImagingArea } from \"../sensor/sensor-geometry.js\";\n\n/** The default equivalence reference: the 36 × 24 mm frame. */\nconst REFERENCE_FULL_FRAME: Readonly<SensorImagingArea> = Object.freeze({ widthMm: 36, heightMm: 24 });\n\n/** Whether a value is exact under its stated conditions or an approximation. */\nexport type EquivalenceExactness = \"exact\" | \"approximation\";\n\n/** How one equivalence quantity was obtained, and under which conditions it holds. */\nexport interface EquivalenceValueBasis {\n /** `\"exact\"` under the stated conditions, or `\"approximation\"`. */\n kind: EquivalenceExactness;\n /** Plain statement of the definition and its conditions. */\n statement: string;\n}\n\nexport interface CalculateSensorEquivalenceInput {\n /** Physical active imaging area of the sensor the lens is used on. */\n sensor: SensorImagingArea;\n /** Physical focal length of the lens in millimetres. */\n focalLengthMm: number;\n /** f-number set on the lens (N = focal length / entrance-pupil diameter). */\n fNumber: number;\n /** Format to compare against. Defaults to the 36 × 24 mm frame. */\n reference?: SensorImagingArea;\n}\n\n/** Angles of view of an ideal rectilinear lens focused at infinity, in degrees. */\nexport interface SensorEquivalenceFieldOfView {\n horizontalDegrees: number;\n verticalDegrees: number;\n diagonalDegrees: number;\n}\n\n/** Quantities of a sensor equivalence result that carry an exactness statement. */\nexport type SensorEquivalenceQuantity =\n | \"cropFactor\"\n | \"equivalentFocalLengthMm\"\n | \"equivalentFNumber\"\n | \"exposureFNumber\"\n | \"entrancePupilDiameterMm\"\n | \"areaRatio\"\n | \"totalLightDifferenceStops\"\n | \"fieldOfView\";\n\nexport interface SensorEquivalence {\n /** Physical focal length supplied by the caller, in millimetres. */\n focalLengthMm: number;\n /** f-number supplied by the caller. */\n fNumber: number;\n /** Sensor active imaging area used. */\n sensor: SensorImagingArea;\n /** Reference format used (36 × 24 mm unless the caller passed one). */\n reference: SensorImagingArea;\n /** Sensor diagonal in millimetres. */\n sensorDiagonalMm: number;\n /** Reference diagonal in millimetres. */\n referenceDiagonalMm: number;\n /** Diagonal crop factor: reference diagonal / sensor diagonal. Greater than 1 for a smaller sensor. */\n cropFactor: number;\n /** Focal length on the reference format with the same diagonal angle of view: cropFactor × focalLengthMm. */\n equivalentFocalLengthMm: number;\n /**\n * f-number on the reference format that gives the same depth of field, background blur and total\n * light for the same framing and output size: cropFactor × fNumber. Not an exposure setting.\n */\n equivalentFNumber: number;\n /** f-number that sets exposure (image brightness): the f-number on the lens, unchanged by sensor size. */\n exposureFNumber: number;\n /** Entrance-pupil diameter f/N in millimetres; the equivalent lens has the same diameter. */\n entrancePupilDiameterMm: number;\n /** Sensor area / reference area. Less than 1 for a smaller sensor. */\n areaRatio: number;\n /**\n * log2(areaRatio): total light collected by the sensor relative to the reference at the same\n * f-number, shutter time and scene, in stops. Negative for a smaller sensor.\n */\n totalLightDifferenceStops: number;\n /** Angles of view of this lens on the sensor (the equivalent lens gives the same diagonal angle). */\n fieldOfView: SensorEquivalenceFieldOfView;\n /** Exact or approximated, with the conditions, for every quantity above. */\n basis: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>>;\n}\n\nconst SENSOR_EQUIVALENCE_BASIS: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>> = Object.freeze({\n cropFactor: Object.freeze({\n kind: \"exact\",\n statement: \"Ratio of the reference diagonal to the sensor diagonal (diagonal convention).\"\n }),\n equivalentFocalLengthMm: Object.freeze({\n kind: \"exact\",\n statement: \"Same diagonal angle of view on the reference format, for an ideal rectilinear lens focused at infinity. \" +\n \"Horizontal and vertical angles match too only when the aspect ratios match.\"\n }),\n equivalentFNumber: Object.freeze({\n kind: \"approximation\",\n statement: \"Same depth of field and background blur as a share of the frame, at the same subject distance, framing and output size, \" +\n \"and the same entrance-pupil diameter (so the same total light at the same shutter time). In the thin-lens defocus equation it is exact \" +\n \"only when the focus distance s is much greater than the focal length f; with focal lengths in the ratio cropFactor, the blur shares match \" +\n \"at cropFactor·N·(s − f)/(s − cropFactor·f). \" +\n \"Total light matches exactly only when the aspect ratios match. It does not set exposure.\"\n }),\n exposureFNumber: Object.freeze({\n kind: \"exact\",\n statement: \"Image illuminance is proportional to 1/N², independent of sensor size (ideal lens; transmission, vignetting and \" +\n \"close-focus bellows factor excluded). Equivalence never changes the exposure setting.\"\n }),\n entrancePupilDiameterMm: Object.freeze({\n kind: \"exact\",\n statement: \"Entrance-pupil diameter D = f/N by the definition of the f-number. The equivalent lens (cropFactor·f at cropFactor·N) has the same D.\"\n }),\n areaRatio: Object.freeze({\n kind: \"exact\",\n statement: \"Product of width and height of the sensor divided by that of the reference.\"\n }),\n totalLightDifferenceStops: Object.freeze({\n kind: \"exact\",\n statement: \"log2 of the area ratio: at the same f-number, shutter time and scene the image illuminance is the same, so total light scales \" +\n \"with area (uniform illuminance; vignetting excluded).\"\n }),\n fieldOfView: Object.freeze({\n kind: \"exact\",\n statement: \"2·atan(dimension / 2f) for an ideal rectilinear lens focused at infinity; distortion and focus breathing excluded.\"\n })\n});\n\nfunction requireImagingArea(area: unknown, label: string): asserts area is SensorImagingArea {\n requireInputObject(area, label);\n requireInputKeys(area, label, [\"widthMm\", \"heightMm\"]);\n const record = area as SensorImagingArea;\n requirePositiveFinite(`${label}.widthMm`, record.widthMm);\n requirePositiveFinite(`${label}.heightMm`, record.heightMm);\n}\n\n/** √((rw² + rh²)/(w² + h²)), with only correctly rounded operations when nothing overflows (portable). */\nfunction diagonalRatio(reference: SensorImagingArea, sensor: SensorImagingArea): number {\n const referenceSquared = reference.widthMm * reference.widthMm + reference.heightMm * reference.heightMm;\n const sensorSquared = sensor.widthMm * sensor.widthMm + sensor.heightMm * sensor.heightMm;\n const ratio = referenceSquared / sensorSquared;\n return Number.isFinite(referenceSquared) && Number.isFinite(sensorSquared) && sensorSquared > 0 && ratio > 0 && Number.isFinite(ratio)\n ? Math.sqrt(ratio)\n : portableHypot(reference.widthMm, reference.heightMm) / portableHypot(sensor.widthMm, sensor.heightMm);\n}\n\nfunction rectilinearAngleDegrees(dimensionMm: number, focalLengthMm: number): number {\n return (2 * Math.atan(dimensionMm / (2 * focalLengthMm)) * 180) / Math.PI;\n}\n\n/**\n * Equivalence of a lens (focal length, f-number) on one sensor against a reference format,\n * 36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of\n * field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light\n * difference in stops and the angles of view.\n *\n * Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it\n * is exact under its stated conditions or an approximation. The equivalent f-number is an\n * approximation (exact when the focus distance is much greater than the focal length) and never an\n * exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.\n *\n * Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.\n *\n * @param input Sensor active area, focal length, f-number and optional reference format.\n * @returns The equivalence numbers with a per-quantity exactness statement.\n */\nexport function calculateSensorEquivalence(\n input: CalculateSensorEquivalenceInput\n): CalculationResult<SensorEquivalence> {\n requireInputKeys(input, \"calculateSensorEquivalence\", [\"sensor\", \"focalLengthMm\", \"fNumber\", \"reference\"]);\n requireImagingArea(input.sensor, \"sensor\");\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n requirePositiveFinite(\"fNumber\", input.fNumber);\n if (input.reference !== undefined) requireImagingArea(input.reference, \"reference\");\n\n const sensor: SensorImagingArea = { widthMm: input.sensor.widthMm, heightMm: input.sensor.heightMm };\n const reference: SensorImagingArea = input.reference === undefined\n ? { ...REFERENCE_FULL_FRAME }\n : { widthMm: input.reference.widthMm, heightMm: input.reference.heightMm };\n // The default reference reuses the canonical 35 mm crop factor, so it is bit-identical to\n // calculateEquivalentFocalLength35Mm and calculateImagingAreaMetrics.\n const cropFactor = input.reference === undefined\n ? calculateImagingAreaMetrics(sensor).value.cropFactor35Mm\n : diagonalRatio(reference, sensor);\n const areaRatio = (sensor.widthMm / reference.widthMm) * (sensor.heightMm / reference.heightMm);\n const sensorDiagonalMm = portableHypot(sensor.widthMm, sensor.heightMm);\n if (!(areaRatio > 0) || !Number.isFinite(areaRatio)) {\n throw new InvalidScientificInputError(\"sensor and reference areas must differ by a ratio that is a finite number greater than zero.\");\n }\n\n return approximationResult(\n {\n focalLengthMm: input.focalLengthMm,\n fNumber: input.fNumber,\n sensor,\n reference,\n sensorDiagonalMm,\n referenceDiagonalMm: portableHypot(reference.widthMm, reference.heightMm),\n cropFactor,\n equivalentFocalLengthMm: input.focalLengthMm * cropFactor,\n equivalentFNumber: input.fNumber * cropFactor,\n exposureFNumber: input.fNumber,\n entrancePupilDiameterMm: input.focalLengthMm / input.fNumber,\n areaRatio,\n totalLightDifferenceStops: portableLog2(areaRatio),\n fieldOfView: {\n horizontalDegrees: rectilinearAngleDegrees(sensor.widthMm, input.focalLengthMm),\n verticalDegrees: rectilinearAngleDegrees(sensor.heightMm, input.focalLengthMm),\n diagonalDegrees: rectilinearAngleDegrees(sensorDiagonalMm, input.focalLengthMm)\n },\n basis: SENSOR_EQUIVALENCE_BASIS\n },\n \"sensor-equivalence\",\n \"1.0.0\",\n [\n \"Crop factor and equivalence are diagonal-based against the reference format (36 × 24 mm unless given).\",\n \"Equivalent f-number concerns depth of field, background blur and total light for the same framing and output size; it is not an exposure setting.\",\n \"Equivalent f-number is a thin-lens approximation, exact when the focus distance is much greater than the focal length.\",\n \"Exposure f-number is the f-number on the lens: image illuminance does not depend on sensor size.\",\n \"Ideal rectilinear lens focused at infinity; distortion, vignetting, transmission and focus breathing excluded.\",\n \"Physical focal length and f-number are not changed; see basis for each quantity.\"\n ]\n );\n}\n\nexport interface CalculateBlurShareOfFrameInput {\n /** Blur-circle diameter on the sensor in millimetres (for example from calculateDefocusCircle). Zero is allowed. */\n blurCircleMm: number;\n /** Physical active imaging area of the sensor. */\n sensor: SensorImagingArea;\n /**\n * Optional print size. The whole frame is enlarged to fit inside it without cropping, long side\n * to long side.\n */\n print?: { widthMm: number; heightMm: number };\n}\n\nexport interface BlurShareOfPrint {\n /** Linear enlargement from sensor to print: the smaller of the long-side and short-side ratios. */\n enlargement: number;\n /** Blur-circle diameter on the print in millimetres: blurCircleMm × enlargement. */\n blurOnPrintMm: number;\n /** Size of the printed image in millimetres (equal to the print when the aspect ratios match). */\n imageLongSideMm: number;\n imageShortSideMm: number;\n}\n\nexport interface BlurShareOfFrame {\n /** Blur-circle diameter on the sensor in millimetres. */\n blurCircleMm: number;\n /** Sensor diagonal in millimetres. */\n frameDiagonalMm: number;\n /** Blur diameter / frame diagonal. Equal shares look the same at the same output size. */\n shareOfDiagonal: number;\n /** Blur diameter / frame width. */\n shareOfWidth: number;\n /** Blur diameter / frame height. */\n shareOfHeight: number;\n /** Blur on the print; present only when a print size was given. */\n print?: BlurShareOfPrint;\n}\n\n/**\n * A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a\n * print size is given, its diameter on that print.\n *\n * Equal shares of the diagonal look the same when the images are shown at the same size, which\n * is how \"same framing\" comparisons across sensor sizes are stated. Every value is an exact ratio\n * or scaling; whether a blur is visible depends on viewing distance and acuity, which this\n * function does not judge.\n *\n * @param input Blur diameter in millimetres, sensor active area and optional print size.\n * @returns Shares of the frame, and the blur on the print when a print is given.\n */\nexport function calculateBlurShareOfFrame(\n input: CalculateBlurShareOfFrameInput\n): CalculationResult<BlurShareOfFrame> {\n requireInputKeys(input, \"calculateBlurShareOfFrame\", [\"blurCircleMm\", \"sensor\", \"print\"]);\n if (typeof input.blurCircleMm !== \"number\" || !Number.isFinite(input.blurCircleMm) || input.blurCircleMm < 0) {\n throw new InvalidScientificInputError(\"blurCircleMm must be a finite number greater than or equal to zero.\");\n }\n requireImagingArea(input.sensor, \"sensor\");\n if (input.print !== undefined) requireImagingArea(input.print, \"print\");\n\n const { widthMm, heightMm } = input.sensor;\n const blurCircleMm = input.blurCircleMm;\n const frameDiagonalMm = portableHypot(widthMm, heightMm);\n let print: BlurShareOfPrint | undefined;\n if (input.print !== undefined) {\n const sensorLong = Math.max(widthMm, heightMm), sensorShort = Math.min(widthMm, heightMm);\n const printLong = Math.max(input.print.widthMm, input.print.heightMm), printShort = Math.min(input.print.widthMm, input.print.heightMm);\n const enlargement = Math.min(printLong / sensorLong, printShort / sensorShort);\n if (!(enlargement > 0) || !Number.isFinite(enlargement)) {\n throw new InvalidScientificInputError(\"print enlargement must be a finite number greater than zero.\");\n }\n print = {\n enlargement,\n blurOnPrintMm: blurCircleMm * enlargement,\n imageLongSideMm: sensorLong * enlargement,\n imageShortSideMm: sensorShort * enlargement\n };\n }\n\n return calculatedResult(\n {\n blurCircleMm,\n frameDiagonalMm,\n shareOfDiagonal: blurCircleMm / frameDiagonalMm,\n shareOfWidth: blurCircleMm / widthMm,\n shareOfHeight: blurCircleMm / heightMm,\n ...(print === undefined ? {} : { print })\n },\n \"blur-share-of-frame\",\n \"1.0.0\",\n [\n \"Shares are exact ratios of the blur diameter to the active frame's diagonal, width and height.\",\n \"A print shows the whole frame enlarged to fit inside it without cropping, long side to long side.\",\n \"Visibility of the blur depends on viewing distance and acuity; no acceptability criterion is applied.\"\n ]\n );\n}\n"]}
|
|
@@ -60,6 +60,11 @@ export interface TabulatedNativeRawInput {
|
|
|
60
60
|
/** Called once per native tile-equivalent (progress), as the environment task reads its tiles. */
|
|
61
61
|
onTile?: () => void;
|
|
62
62
|
tileWidth: number;
|
|
63
|
+
/**
|
|
64
|
+
* #568: the largest relative difference the verification accepts (default `TABULATED_RELATIVE_TOLERANCE`): a fast method that
|
|
65
|
+
* approximates the chain (`lens.catalogue`'s field-grid PSF) passes its own measured bound.
|
|
66
|
+
*/
|
|
67
|
+
relativeTolerance?: number;
|
|
63
68
|
yieldControl(): Promise<void>;
|
|
64
69
|
checkAbort(): void;
|
|
65
70
|
}
|
|
@@ -78,7 +78,7 @@ export async function renderTabulatedNativeRaw(t) {
|
|
|
78
78
|
t.electrons.window(x, y, x + 1, y + 1);
|
|
79
79
|
const chain = photo.expectedGeneratedElectronCount, tabulated = testHook?.tabulatedElectrons ? testHook.tabulatedElectrons(t.electrons.site(x, y), x, y) : t.electrons.site(x, y);
|
|
80
80
|
const difference = chain === tabulated ? 0 : Math.abs(tabulated - chain) / Math.max(Math.abs(chain), Number.MIN_VALUE);
|
|
81
|
-
if (!(difference <= TABULATED_RELATIVE_TOLERANCE))
|
|
81
|
+
if (!(difference <= (t.relativeTolerance ?? TABULATED_RELATIVE_TOLERANCE)))
|
|
82
82
|
return undefined;
|
|
83
83
|
worst = Math.max(worst, difference);
|
|
84
84
|
readouts[(y & 1) * 2 + (x & 1)] = readout;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native-environment-tabulated.js","sourceRoot":"","sources":["../../src/capture/native-environment-tabulated.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394 step 2, not exported from the package): the native RAW of an engine-built planar scene (a chart preset or a\n * caller material scene, still camera, rectilinear, no distortion, vignetting, pupil rays or caller PSF) from tabulated\n * expected electrons instead of the per-site environment chain.\n *\n * Each site's expected photo electrons come from the display view's tabulated expected signal (`createDisplayViewExpectedSignal`):\n * the same sum over pixel-aperture nodes, anti-aliasing spots, diffraction taps and spectral nodes as the environment chain,\n * formed in another order, so the value differs from the chain's only in the last bits. The sensor stage is the native one:\n * dark charge, both Poisson draws, read noise and the ADC with the site's own seeds (`createVerifiedNativeSiteReadout`). A\n * site whose expected electrons differ in the last bit can therefore, rarely, draw a different integer; the statistics are\n * unchanged.\n *\n * Verification at run time: one site of each CFA phase runs the full chain (planning, the evaluator's callbacks, composition,\n * parsing and `calculateNativeRawSite`). Its readout template must reproduce that site bit for bit, and its tabulated\n * electrons must agree with the chain's within `TABULATED_RELATIVE_TOLERANCE`; otherwise the caller runs the environment task.\n * #453: the chain's photo signal comes from `executeEngineOwnedEnvironmentPhotoSignal`, the same operations and values as\n * `executeEnvironmentSensorPhotoSignal` for these engine-built records, without re-validating records that cannot have changed.\n */\n\nimport type { EnvironmentRadianceEvaluator } from \"../sensor/environment-photo-signal.js\";\nimport { executeEngineOwnedEnvironmentPhotoSignal, executeEnvironmentSensorPhotoSignal } from \"../sensor/environment-photo-signal.js\";\nimport { isInternalSceneRadianceEvaluator } from \"../core/internal-radiance-evaluator.js\";\nimport { calculateCaptureExposureWindows, prepareCaptureExposureWindowSampler } from \"../sensor/exposure-window.js\";\nimport { createEnvironmentRawFrameCanonicalMemo, planEnvironmentRawSite, type SimulateEnvironmentSensorRawFrameInput } from \"./environment-raw-producer.js\";\nimport { admitNativeEnvironmentInput, type NativeEnvironmentRawInput } from \"./native-environment-raw.js\";\nimport { composeNativeEnvironmentSensorSite } from \"./native-environment-optical-stage.js\";\nimport { calculateNativeRawSite, parseNativeRawProducerSite } from \"./sensor-raw-producer.js\";\nimport { calculateSiteEffectiveSaturationCode, createPhaseWhiteLevelAccumulator } from \"./raw-white-level.js\";\nimport { createVerifiedNativeSiteReadout, type NativeSiteReadout } from \"./native-environment-lean.js\";\nimport type { NativeRawOutput } from \"./native-raw.js\";\n\n/** Largest relative difference between a verification site's tabulated and chain electrons: rounding only (a few ulps of each term). */\nexport const TABULATED_RELATIVE_TOLERANCE = 1e-9;\n/** Rows per expected-signal cache window. */\nconst BAND_ROWS = 32;\n/** Host-yield budget in milliseconds (the native task's cadence); the task also yields after its last row. */\nconst YIELD_MS = 8;\n\n/** Internal counters (tests assert how many verification sites ran the chain per call; four, one per CFA phase). */\nexport const tabulatedVerificationStatistics = { chainExecutions: 0 };\n\n/**\n * Internal test hook (never set by the engine): `fullPhoto` also runs each verification site through\n * `executeEnvironmentSensorPhotoSignal` (with the uncounted evaluator) and hands both photo signals to the test;\n * `tabulatedElectrons` replaces the tabulated electrons the gate compares, to show that a wrong fast path is rejected.\n */\nexport interface TabulatedVerificationTestHook {\n fullPhoto?(engineOwned: unknown, full: unknown): void;\n tabulatedElectrons?(value: number, x: number, y: number): number;\n}\nlet testHook: TabulatedVerificationTestHook | undefined;\nexport function setTabulatedVerificationTestHook(hook: TabulatedVerificationTestHook | undefined): void { testHook = hook; }\n\nlet tabulatedDisabled = false;\n/** Disables the tabulated path process-wide (internal; tests of the environment path itself turn it off). */\nexport function setTabulatedNativeRawEnabled(enabled: boolean): void { tabulatedDisabled = !enabled; }\n\nexport interface TabulatedNativeRawInput {\n input: NativeEnvironmentRawInput;\n /** Native rectangle to render (region rendering); absent: the whole raster. */\n region?: { x: number; y: number; width: number; height: number } | undefined;\n /** The engine-built site record of a native index (the facade's `siteInput`). */\n siteInput(index: number): SimulateEnvironmentSensorRawFrameInput[\"sites\"][number];\n /** The evaluator the environment task would use (for the verification sites). */\n evaluateRadiance: EnvironmentRadianceEvaluator;\n /** Expected photo electrons of native sites (`createDisplayViewExpectedSignal`). */\n electrons: { window(x0: number, y0: number, x1: number, y1: number): void; site(x: number, y: number): number; readonly lookups: number };\n /** Called once per native tile-equivalent (progress), as the environment task reads its tiles. */\n onTile?: () => void;\n tileWidth: number;\n yieldControl(): Promise<void>;\n checkAbort(): void;\n}\n\nexport interface TabulatedNativeRawResult {\n raw: NativeRawOutput;\n /** Evaluator callbacks of the verification sites plus the tabulated signal's scene lookups. */\n providerEvaluationCount: number;\n /** Largest relative difference between tabulated and chain electrons over the verification sites. */\n verificationRelativeDifference: number;\n}\n\n/**\n * #449: the local exposure duration shared by every site of `region`, or undefined when the sampler must run per site. With\n * simultaneous opening and closing boundaries both boundary phases are 0 at every point (exposure-window.ts, boundaryPhase), so\n * each site's duration is the nominal one the sampler gives at the region's first site. The sampler's only other per-point step\n * is its check that the point lies in the active capture rectangle; the region's first and last site centres bound every other\n * centre, so when both pass every site passes. When either throws, the per-site path runs (and throws where it did).\n */\nexport function simultaneousExposureDuration(schedule: { opening: unknown; closing: unknown }, sampler: (point: { x: number; y: number }) => { localExposureDurationSeconds: number },\n region: { x: number; y: number; width: number; height: number }): number | undefined {\n if ((schedule.opening as { kind?: unknown }).kind !== \"simultaneous\" || (schedule.closing as { kind?: unknown }).kind !== \"simultaneous\") return undefined;\n try {\n const first = sampler({ x: region.x + 0.5, y: region.y + 0.5 }).localExposureDurationSeconds;\n const last = sampler({ x: region.x + region.width - 0.5, y: region.y + region.height - 0.5 }).localExposureDurationSeconds;\n return Object.is(first, last) ? first : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Renders the native RAW from tabulated expected electrons, or returns undefined when verification fails (see the module comment). */\nexport async function renderTabulatedNativeRaw(t: TabulatedNativeRawInput): Promise<TabulatedNativeRawResult | undefined> {\n if (tabulatedDisabled) return undefined;\n const { plan, sceneBinding } = admitNativeEnvironmentInput(t.input);\n const native = plan.exposure.geometry.nativeRaster, W = native.pixelWidth, layout = plan.frame.colorSamplingProfile.layout;\n if (layout.kind !== \"periodic-mosaic\" || layout.repeatWidthSites !== 2 || layout.repeatHeightSites !== 2 || !plan.exposureWindow) return undefined;\n const region = t.region ?? { x: 0, y: 0, width: W, height: native.pixelHeight };\n if (region.width < 2 || region.height < 2) return undefined;\n const seed = plan.exposure.noise.seedUint32, schedule = plan.exposureWindow, memo = createEnvironmentRawFrameCanonicalMemo(plan.frame, schedule);\n let evaluations = 0;\n const evaluate: EnvironmentRadianceEvaluator = (request) => { evaluations++; return t.evaluateRadiance(request); };\n // The counting wrapper returns the evaluator's own results, so they are engine-built exactly when the evaluator is.\n const engineBuiltResults = isInternalSceneRadianceEvaluator(t.evaluateRadiance);\n // Verification: the region's top-left 2 × 2 block, one site per CFA phase, through the full chain.\n const readouts: NativeSiteReadout[] = [];\n let worst = 0;\n for (let k = 0; k < 4; k++) {\n const x = region.x + (k & 1), y = region.y + (k >> 1), index = y * W + x, site = t.siteInput(index);\n const full = planEnvironmentRawSite(site, index, plan.frame, sceneBinding, schedule, \"engine-owned\", memo, \"photo-only\");\n // #453: the chain for these engine-built site records (the same operations and values as executeEnvironmentSensorPhotoSignal,\n // without re-validating records that have not changed; see executeEngineOwnedEnvironmentPhotoSignal).\n const photoResult = executeEngineOwnedEnvironmentPhotoSignal(full, evaluate, engineBuiltResults), photo = photoResult.value.photoSignal;\n tabulatedVerificationStatistics.chainExecutions++;\n if (testHook?.fullPhoto) testHook.fullPhoto(photoResult, executeEnvironmentSensorPhotoSignal(full, t.evaluateRadiance, undefined, \"photo-only\").value.photo);\n const timing = calculateCaptureExposureWindows({ ...schedule, nativeRaster: native, samplePointsNative: [{ x: x + 0.5, y: y + 0.5 }] }).value.samples[0];\n const parsed = parseNativeRawProducerSite(composeNativeEnvironmentSensorSite(site, photo), index, plan.frame, timing);\n const result = calculateNativeRawSite(parsed, seed, index);\n const whiteCode = calculateSiteEffectiveSaturationCode(result.readout.value, parsed.readoutProfile, parsed.regimeId);\n const readout = createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed);\n if (!readout) return undefined;\n t.electrons.window(x, y, x + 1, y + 1);\n const chain = photo.expectedGeneratedElectronCount, tabulated = testHook?.tabulatedElectrons ? testHook.tabulatedElectrons(t.electrons.site(x, y), x, y) : t.electrons.site(x, y);\n const difference = chain === tabulated ? 0 : Math.abs(tabulated - chain) / Math.max(Math.abs(chain), Number.MIN_VALUE);\n if (!(difference <= TABULATED_RELATIVE_TOLERANCE)) return undefined;\n worst = Math.max(worst, difference);\n readouts[(y & 1) * 2 + (x & 1)] = readout;\n }\n const length = region.width * region.height;\n const raw: NativeRawOutput = { plan, codes: new Uint16Array(length), blackLevels: new Uint16Array(length), digitalSaturationCodes: new Uint16Array(length),\n saturationFlags: new Uint8Array(length), effectiveSaturationCodes: new Uint16Array(4) };\n const white = createPhaseWhiteLevelAccumulator(W, 2, 2), sampler = prepareCaptureExposureWindowSampler({ ...schedule, nativeRaster: native });\n const tilesPerRow = Math.ceil(region.width / t.tileWidth), point = { x: 0, y: 0 };\n const constantDuration = simultaneousExposureDuration(schedule, sampler, region);\n let last = Date.now();\n for (let y0 = region.y; y0 < region.y + region.height; y0 += BAND_ROWS) {\n const y1 = Math.min(region.y + region.height, y0 + BAND_ROWS);\n t.electrons.window(region.x, y0, region.x + region.width, y1);\n for (let y = y0; y < y1; y++) {\n for (let x = region.x; x < region.x + region.width; x++) {\n const index = y * W + x, at = (y - region.y) * region.width + x - region.x;\n let duration = constantDuration;\n if (duration === undefined) { point.x = x + 0.5; point.y = y + 0.5; duration = sampler(point).localExposureDurationSeconds; }\n const o = readouts[(y & 1) * 2 + (x & 1)]!.compute(t.electrons.site(x, y), duration, x, y, index, seed);\n // A check of the full path failed for this site: let the environment task decide (it throws its own error).\n if (!o) return undefined;\n raw.codes[at] = o.rawCode; raw.blackLevels[at] = o.blackLevelCode; raw.digitalSaturationCodes[at] = o.digitalSaturationCode; raw.saturationFlags[at] = o.flags;\n white.record(index, o.whiteCode);\n }\n for (let k = 0; k < tilesPerRow; k++) t.onTile?.();\n if (y === region.y + region.height - 1 || Date.now() - last >= YIELD_MS) { await t.yieldControl(); t.checkAbort(); last = Date.now(); }\n }\n }\n raw.effectiveSaturationCodes.set(t.region ? white.partial() : white.finish());\n return { raw, providerEvaluationCount: evaluations + t.electrons.lookups, verificationRelativeDifference: worst };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"native-environment-tabulated.js","sourceRoot":"","sources":["../../src/capture/native-environment-tabulated.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#394 step 2, not exported from the package): the native RAW of an engine-built planar scene (a chart preset or a\n * caller material scene, still camera, rectilinear, no distortion, vignetting, pupil rays or caller PSF) from tabulated\n * expected electrons instead of the per-site environment chain.\n *\n * Each site's expected photo electrons come from the display view's tabulated expected signal (`createDisplayViewExpectedSignal`):\n * the same sum over pixel-aperture nodes, anti-aliasing spots, diffraction taps and spectral nodes as the environment chain,\n * formed in another order, so the value differs from the chain's only in the last bits. The sensor stage is the native one:\n * dark charge, both Poisson draws, read noise and the ADC with the site's own seeds (`createVerifiedNativeSiteReadout`). A\n * site whose expected electrons differ in the last bit can therefore, rarely, draw a different integer; the statistics are\n * unchanged.\n *\n * Verification at run time: one site of each CFA phase runs the full chain (planning, the evaluator's callbacks, composition,\n * parsing and `calculateNativeRawSite`). Its readout template must reproduce that site bit for bit, and its tabulated\n * electrons must agree with the chain's within `TABULATED_RELATIVE_TOLERANCE`; otherwise the caller runs the environment task.\n * #453: the chain's photo signal comes from `executeEngineOwnedEnvironmentPhotoSignal`, the same operations and values as\n * `executeEnvironmentSensorPhotoSignal` for these engine-built records, without re-validating records that cannot have changed.\n */\n\nimport type { EnvironmentRadianceEvaluator } from \"../sensor/environment-photo-signal.js\";\nimport { executeEngineOwnedEnvironmentPhotoSignal, executeEnvironmentSensorPhotoSignal } from \"../sensor/environment-photo-signal.js\";\nimport { isInternalSceneRadianceEvaluator } from \"../core/internal-radiance-evaluator.js\";\nimport { calculateCaptureExposureWindows, prepareCaptureExposureWindowSampler } from \"../sensor/exposure-window.js\";\nimport { createEnvironmentRawFrameCanonicalMemo, planEnvironmentRawSite, type SimulateEnvironmentSensorRawFrameInput } from \"./environment-raw-producer.js\";\nimport { admitNativeEnvironmentInput, type NativeEnvironmentRawInput } from \"./native-environment-raw.js\";\nimport { composeNativeEnvironmentSensorSite } from \"./native-environment-optical-stage.js\";\nimport { calculateNativeRawSite, parseNativeRawProducerSite } from \"./sensor-raw-producer.js\";\nimport { calculateSiteEffectiveSaturationCode, createPhaseWhiteLevelAccumulator } from \"./raw-white-level.js\";\nimport { createVerifiedNativeSiteReadout, type NativeSiteReadout } from \"./native-environment-lean.js\";\nimport type { NativeRawOutput } from \"./native-raw.js\";\n\n/** Largest relative difference between a verification site's tabulated and chain electrons: rounding only (a few ulps of each term). */\nexport const TABULATED_RELATIVE_TOLERANCE = 1e-9;\n/** Rows per expected-signal cache window. */\nconst BAND_ROWS = 32;\n/** Host-yield budget in milliseconds (the native task's cadence); the task also yields after its last row. */\nconst YIELD_MS = 8;\n\n/** Internal counters (tests assert how many verification sites ran the chain per call; four, one per CFA phase). */\nexport const tabulatedVerificationStatistics = { chainExecutions: 0 };\n\n/**\n * Internal test hook (never set by the engine): `fullPhoto` also runs each verification site through\n * `executeEnvironmentSensorPhotoSignal` (with the uncounted evaluator) and hands both photo signals to the test;\n * `tabulatedElectrons` replaces the tabulated electrons the gate compares, to show that a wrong fast path is rejected.\n */\nexport interface TabulatedVerificationTestHook {\n fullPhoto?(engineOwned: unknown, full: unknown): void;\n tabulatedElectrons?(value: number, x: number, y: number): number;\n}\nlet testHook: TabulatedVerificationTestHook | undefined;\nexport function setTabulatedVerificationTestHook(hook: TabulatedVerificationTestHook | undefined): void { testHook = hook; }\n\nlet tabulatedDisabled = false;\n/** Disables the tabulated path process-wide (internal; tests of the environment path itself turn it off). */\nexport function setTabulatedNativeRawEnabled(enabled: boolean): void { tabulatedDisabled = !enabled; }\n\nexport interface TabulatedNativeRawInput {\n input: NativeEnvironmentRawInput;\n /** Native rectangle to render (region rendering); absent: the whole raster. */\n region?: { x: number; y: number; width: number; height: number } | undefined;\n /** The engine-built site record of a native index (the facade's `siteInput`). */\n siteInput(index: number): SimulateEnvironmentSensorRawFrameInput[\"sites\"][number];\n /** The evaluator the environment task would use (for the verification sites). */\n evaluateRadiance: EnvironmentRadianceEvaluator;\n /** Expected photo electrons of native sites (`createDisplayViewExpectedSignal`). */\n electrons: { window(x0: number, y0: number, x1: number, y1: number): void; site(x: number, y: number): number; readonly lookups: number };\n /** Called once per native tile-equivalent (progress), as the environment task reads its tiles. */\n onTile?: () => void;\n tileWidth: number;\n /**\n * #568: the largest relative difference the verification accepts (default `TABULATED_RELATIVE_TOLERANCE`): a fast method that\n * approximates the chain (`lens.catalogue`'s field-grid PSF) passes its own measured bound.\n */\n relativeTolerance?: number;\n yieldControl(): Promise<void>;\n checkAbort(): void;\n}\n\nexport interface TabulatedNativeRawResult {\n raw: NativeRawOutput;\n /** Evaluator callbacks of the verification sites plus the tabulated signal's scene lookups. */\n providerEvaluationCount: number;\n /** Largest relative difference between tabulated and chain electrons over the verification sites. */\n verificationRelativeDifference: number;\n}\n\n/**\n * #449: the local exposure duration shared by every site of `region`, or undefined when the sampler must run per site. With\n * simultaneous opening and closing boundaries both boundary phases are 0 at every point (exposure-window.ts, boundaryPhase), so\n * each site's duration is the nominal one the sampler gives at the region's first site. The sampler's only other per-point step\n * is its check that the point lies in the active capture rectangle; the region's first and last site centres bound every other\n * centre, so when both pass every site passes. When either throws, the per-site path runs (and throws where it did).\n */\nexport function simultaneousExposureDuration(schedule: { opening: unknown; closing: unknown }, sampler: (point: { x: number; y: number }) => { localExposureDurationSeconds: number },\n region: { x: number; y: number; width: number; height: number }): number | undefined {\n if ((schedule.opening as { kind?: unknown }).kind !== \"simultaneous\" || (schedule.closing as { kind?: unknown }).kind !== \"simultaneous\") return undefined;\n try {\n const first = sampler({ x: region.x + 0.5, y: region.y + 0.5 }).localExposureDurationSeconds;\n const last = sampler({ x: region.x + region.width - 0.5, y: region.y + region.height - 0.5 }).localExposureDurationSeconds;\n return Object.is(first, last) ? first : undefined;\n } catch {\n return undefined;\n }\n}\n\n/** Renders the native RAW from tabulated expected electrons, or returns undefined when verification fails (see the module comment). */\nexport async function renderTabulatedNativeRaw(t: TabulatedNativeRawInput): Promise<TabulatedNativeRawResult | undefined> {\n if (tabulatedDisabled) return undefined;\n const { plan, sceneBinding } = admitNativeEnvironmentInput(t.input);\n const native = plan.exposure.geometry.nativeRaster, W = native.pixelWidth, layout = plan.frame.colorSamplingProfile.layout;\n if (layout.kind !== \"periodic-mosaic\" || layout.repeatWidthSites !== 2 || layout.repeatHeightSites !== 2 || !plan.exposureWindow) return undefined;\n const region = t.region ?? { x: 0, y: 0, width: W, height: native.pixelHeight };\n if (region.width < 2 || region.height < 2) return undefined;\n const seed = plan.exposure.noise.seedUint32, schedule = plan.exposureWindow, memo = createEnvironmentRawFrameCanonicalMemo(plan.frame, schedule);\n let evaluations = 0;\n const evaluate: EnvironmentRadianceEvaluator = (request) => { evaluations++; return t.evaluateRadiance(request); };\n // The counting wrapper returns the evaluator's own results, so they are engine-built exactly when the evaluator is.\n const engineBuiltResults = isInternalSceneRadianceEvaluator(t.evaluateRadiance);\n // Verification: the region's top-left 2 × 2 block, one site per CFA phase, through the full chain.\n const readouts: NativeSiteReadout[] = [];\n let worst = 0;\n for (let k = 0; k < 4; k++) {\n const x = region.x + (k & 1), y = region.y + (k >> 1), index = y * W + x, site = t.siteInput(index);\n const full = planEnvironmentRawSite(site, index, plan.frame, sceneBinding, schedule, \"engine-owned\", memo, \"photo-only\");\n // #453: the chain for these engine-built site records (the same operations and values as executeEnvironmentSensorPhotoSignal,\n // without re-validating records that have not changed; see executeEngineOwnedEnvironmentPhotoSignal).\n const photoResult = executeEngineOwnedEnvironmentPhotoSignal(full, evaluate, engineBuiltResults), photo = photoResult.value.photoSignal;\n tabulatedVerificationStatistics.chainExecutions++;\n if (testHook?.fullPhoto) testHook.fullPhoto(photoResult, executeEnvironmentSensorPhotoSignal(full, t.evaluateRadiance, undefined, \"photo-only\").value.photo);\n const timing = calculateCaptureExposureWindows({ ...schedule, nativeRaster: native, samplePointsNative: [{ x: x + 0.5, y: y + 0.5 }] }).value.samples[0];\n const parsed = parseNativeRawProducerSite(composeNativeEnvironmentSensorSite(site, photo), index, plan.frame, timing);\n const result = calculateNativeRawSite(parsed, seed, index);\n const whiteCode = calculateSiteEffectiveSaturationCode(result.readout.value, parsed.readoutProfile, parsed.regimeId);\n const readout = createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo, x, y, index, seed);\n if (!readout) return undefined;\n t.electrons.window(x, y, x + 1, y + 1);\n const chain = photo.expectedGeneratedElectronCount, tabulated = testHook?.tabulatedElectrons ? testHook.tabulatedElectrons(t.electrons.site(x, y), x, y) : t.electrons.site(x, y);\n const difference = chain === tabulated ? 0 : Math.abs(tabulated - chain) / Math.max(Math.abs(chain), Number.MIN_VALUE);\n if (!(difference <= (t.relativeTolerance ?? TABULATED_RELATIVE_TOLERANCE))) return undefined;\n worst = Math.max(worst, difference);\n readouts[(y & 1) * 2 + (x & 1)] = readout;\n }\n const length = region.width * region.height;\n const raw: NativeRawOutput = { plan, codes: new Uint16Array(length), blackLevels: new Uint16Array(length), digitalSaturationCodes: new Uint16Array(length),\n saturationFlags: new Uint8Array(length), effectiveSaturationCodes: new Uint16Array(4) };\n const white = createPhaseWhiteLevelAccumulator(W, 2, 2), sampler = prepareCaptureExposureWindowSampler({ ...schedule, nativeRaster: native });\n const tilesPerRow = Math.ceil(region.width / t.tileWidth), point = { x: 0, y: 0 };\n const constantDuration = simultaneousExposureDuration(schedule, sampler, region);\n let last = Date.now();\n for (let y0 = region.y; y0 < region.y + region.height; y0 += BAND_ROWS) {\n const y1 = Math.min(region.y + region.height, y0 + BAND_ROWS);\n t.electrons.window(region.x, y0, region.x + region.width, y1);\n for (let y = y0; y < y1; y++) {\n for (let x = region.x; x < region.x + region.width; x++) {\n const index = y * W + x, at = (y - region.y) * region.width + x - region.x;\n let duration = constantDuration;\n if (duration === undefined) { point.x = x + 0.5; point.y = y + 0.5; duration = sampler(point).localExposureDurationSeconds; }\n const o = readouts[(y & 1) * 2 + (x & 1)]!.compute(t.electrons.site(x, y), duration, x, y, index, seed);\n // A check of the full path failed for this site: let the environment task decide (it throws its own error).\n if (!o) return undefined;\n raw.codes[at] = o.rawCode; raw.blackLevels[at] = o.blackLevelCode; raw.digitalSaturationCodes[at] = o.digitalSaturationCode; raw.saturationFlags[at] = o.flags;\n white.record(index, o.whiteCode);\n }\n for (let k = 0; k < tilesPerRow; k++) t.onTile?.();\n if (y === region.y + region.height - 1 || Date.now() - last >= YIELD_MS) { await t.yieldControl(); t.checkAbort(); last = Date.now(); }\n }\n }\n raw.effectiveSaturationCodes.set(t.region ? white.partial() : white.finish());\n return { raw, providerEvaluationCount: evaluations + t.electrons.lookups, verificationRelativeDifference: worst };\n}\n"]}
|
package/dist/core/version.d.ts
CHANGED
package/dist/core/version.js
CHANGED
package/dist/core/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.18.0\" as const;\n"]}
|