From 5b69225624787ddceee2ff4e0aa747280220e696 Mon Sep 17 00:00:00 2001
From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com>
Date: Thu, 18 Jun 2026 13:26:34 +0200
Subject: [PATCH 1/4] refactor native profile-line and diagnostics
---
src/api/c.zig | 275 +---
src/cache/profile_line_build.zig | 1216 ++++++++++++++++
src/cache/profile_line_memory.zig | 1245 +----------------
src/internal.zig | 16 +-
src/output/atmospheric_budget.zig | 6 +-
src/output/cia_diagnostics.zig | 146 --
src/output/instrument_response.zig | 241 ----
src/output/line_contributions.zig | 651 ---------
src/root.zig | 97 +-
tests/unit/cache/profile_line_memory_test.zig | 50 +-
tests/unit/output/cia_diagnostics_test.zig | 194 ---
.../unit/output/instrument_response_test.zig | 271 ----
tests/unit/output/line_contributions_test.zig | 246 ----
tests/unit/public_surface_test.zig | 9 -
tests/unit/root.zig | 3 -
tests/unit/spectrum/spectrum_run_test.zig | 18 +-
16 files changed, 1315 insertions(+), 3369 deletions(-)
create mode 100644 src/cache/profile_line_build.zig
delete mode 100644 src/output/cia_diagnostics.zig
delete mode 100644 src/output/instrument_response.zig
delete mode 100644 src/output/line_contributions.zig
delete mode 100644 tests/unit/output/cia_diagnostics_test.zig
delete mode 100644 tests/unit/output/instrument_response_test.zig
delete mode 100644 tests/unit/output/line_contributions_test.zig
diff --git a/src/api/c.zig b/src/api/c.zig
index 62e97c56..bfe38ace 100644
--- a/src/api/c.zig
+++ b/src/api/c.zig
@@ -13,9 +13,9 @@ pub const links_libc = builtin.link_libc;
// python/zdisamar/bindings/signatures.py binds the exported `zds_*` symbols with ctypes. |
// |
// boundary |
-// Context owns prepared setup tables, reusable session memory, returned spectrum handles, and error |
+// Context owns prepared setup tables, reusable session memory, returned spectrum handles, and error |
// text. Compute receives only the public root inputs: Prepared, SessionMemory, and SolveConfig. |
-// JSON parsing, diagnostic tables, retrieval, and fastmode return typed failures until they land |
+// JSON parsing, diagnostic tables, retrieval, and fastmode return typed failures until they land |
// ------------------------------------------------------------------------------------------------------------|
pub const ZdsStatus = enum(c_int) {
@@ -331,58 +331,6 @@ pub const ZdsAtmosphericBudget = extern struct {
};
// ------------------------------------------------------------------------------------------------------------|
-// ZdsO2LineContributions -------------------------------------------------------------------------------------|
-// Borrowed O2 line-contribution rows returned by zds_o2_line_contributions. rows owns a Context-free |
-// allocation. |
-// |
-// layout(64-bit) |
-// size: 32 B (0.031 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] len : usize |
-// [ 8..15] total_row_count: usize |
-// [16..16] truncated : u8 |
-// [17..23] padding : 7 B |
-// [24..31] rows : ?[*]const LineContributionRow |
-pub const ZdsO2LineContributions = extern struct {
- len: usize = 0,
- total_row_count: usize = 0,
- truncated: u8 = 0,
- rows: ?[*]const zdisamar.LineContributionRow = null,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// ZdsInstrumentResponse --------------------------------------------------------------------------------------|
-// Borrowed instrument-response rows returned by zds_instrument_response_sampling. rows owns a Context-free |
-// allocation. |
-// |
-// layout(64-bit) |
-// size: 16 B (0.016 KiB), align: 8 B |
-// |
-// memory |
-// [0.. 7] len : usize |
-// [8..15] rows: ?[*]const InstrumentResponseRow |
-pub const ZdsInstrumentResponse = extern struct {
- len: usize = 0,
- rows: ?[*]const zdisamar.InstrumentResponseRow = null,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// ZdsO2O2CIADiagnostics --------------------------------------------------------------------------------------|
-// Borrowed O2-O2 CIA rows returned by zds_o2_o2_cia_diagnostics. rows owns a Context-free allocation. |
-// |
-// layout(64-bit) |
-// size: 16 B (0.016 KiB), align: 8 B |
-// |
-// memory |
-// [0.. 7] len : usize |
-// [8..15] rows: ?[*]const CiaRow |
-pub const ZdsO2O2CIADiagnostics = extern struct {
- len: usize = 0,
- rows: ?[*]const zdisamar.CiaRow = null,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
// CResult ----------------------------------------------------------------------------------------------------|
// Context-owned native spectrum plus optional compact C-facing Jacobian rows. |
// |
@@ -547,7 +495,7 @@ pub export fn zds_context_destroy(ctx: ?*Context) void {
pub export fn zds_prepare_o2a_json(ctx: ?*Context, json_ptr: ?[*]const u8, json_len: usize) c_int {
// zds_prepare_o2a_json -----------------------------------------------------------------------------------|
- // Parse Python's native JSON shape and prepare the resulting typed scene. |
+ // Parse Python's native JSON shape and prepare the resulting typed scene. |
// --------------------------------------------------------------------------------------------------------|
const resolved = ctx orelse return @intFromEnum(ZdsStatus.failure);
@@ -583,7 +531,7 @@ pub export fn zds_prepare_o2a_json(ctx: ?*Context, json_ptr: ?[*]const u8, json_
export fn zds_warm_o2a_session(ctx: ?*Context) c_int {
// zds_warm_o2a_session -----------------------------------------------------------------------------------|
- // Build retained session rows for the prepared scene. |
+ // Build retained session rows for the prepared scene. |
// --------------------------------------------------------------------------------------------------------|
const resolved = ctx orelse return @intFromEnum(ZdsStatus.failure);
@@ -641,14 +589,14 @@ pub export fn zds_warm_o2a_optimal_estimation(
pub export fn zds_run_spectrum(ctx: ?*Context, out: ?*ZdsSpectrum) c_int {
// zds_run_spectrum ---------------------------------------------------------------------------------------|
- // Run the prepared scene without returning Jacobian columns. |
+ // Run the prepared scene without returning Jacobian columns. |
// --------------------------------------------------------------------------------------------------------|
return runSpectrum(ctx, out, false);
}
export fn zds_run_spectrum_jacobian(ctx: ?*Context, out: ?*ZdsSpectrum) c_int {
// zds_run_spectrum_jacobian ------------------------------------------------------------------------------|
- // Run the prepared scene with all fixed Jacobian columns in public order. |
+ // Run the prepared scene with all fixed Jacobian columns in public order. |
// --------------------------------------------------------------------------------------------------------|
return runSpectrum(ctx, out, true);
}
@@ -694,45 +642,7 @@ export fn zds_atmospheric_budget(
// zds_atmospheric_budget ---------------------------------------------------------------------------------|
// Build atmospheric support-row diagnostic records for caller-selected wavelengths. |
// --------------------------------------------------------------------------------------------------------|
- return runWavelengthDiagnostic(.atmospheric_budget, ctx, wavelengths, wavelength_count, {}, out);
-}
-
-export fn zds_o2_line_contributions(
- ctx: ?*Context,
- wavelengths: ?[*]const f64,
- wavelength_count: usize,
- max_rows: usize,
- out: ?*ZdsO2LineContributions,
-) c_int {
- // zds_o2_line_contributions ------------------------------------------------------------------------------|
- // Build O2 line-by-line diagnostic rows for caller-selected wavelengths. |
- // --------------------------------------------------------------------------------------------------------|
- return runWavelengthDiagnostic(.line_contributions, ctx, wavelengths, wavelength_count, max_rows, out);
-}
-
-export fn zds_instrument_response_sampling(
- ctx: ?*Context,
- wavelengths: ?[*]const f64,
- wavelength_count: usize,
- channel_mask: u32,
- out: ?*ZdsInstrumentResponse,
-) c_int {
- // zds_instrument_response_sampling -----------------------------------------------------------------------|
- // Build instrument-response support rows for caller-selected wavelengths and channel mask. |
- // --------------------------------------------------------------------------------------------------------|
- return runWavelengthDiagnostic(.instrument_response, ctx, wavelengths, wavelength_count, channel_mask, out);
-}
-
-export fn zds_o2_o2_cia_diagnostics(
- ctx: ?*Context,
- wavelengths: ?[*]const f64,
- wavelength_count: usize,
- out: ?*ZdsO2O2CIADiagnostics,
-) c_int {
- // zds_o2_o2_cia_diagnostics ------------------------------------------------------------------------------|
- // Build O2-O2 CIA diagnostic rows for caller-selected wavelengths. |
- // --------------------------------------------------------------------------------------------------------|
- return runWavelengthDiagnostic(.cia_diagnostics, ctx, wavelengths, wavelength_count, {}, out);
+ return runAtmosphericBudgetDiagnostic(ctx, wavelengths, wavelength_count, out);
}
// OptimalEstimationMeasurementSlices -------------------------------------------------------------------------|
@@ -920,7 +830,7 @@ pub export fn zds_run_o2a_optimal_estimation_correction(
out: ?*ZdsOptimalEstimationResult,
) c_int {
// zds_run_o2a_optimal_estimation_correction --------------------------------------------------------------|
- // Run one prepared-scene full-physics correction step and return a Context-owned result handle. |
+ // Run one prepared-scene full-physics correction step and return a Context-owned result handle. |
// --------------------------------------------------------------------------------------------------------|
const resolved = ctx orelse return @intFromEnum(ZdsStatus.failure);
@@ -1199,39 +1109,6 @@ export fn zds_atmospheric_budget_free(_: ?*Context, raw: ?*ZdsAtmosphericBudget)
budget.* = .{};
}
-export fn zds_o2_line_contributions_free(_: ?*Context, raw: ?*ZdsO2LineContributions) void {
- // zds_o2_line_contributions_free -------------------------------------------------------------------------|
- // Release O2 line-contribution rows returned by zds_o2_line_contributions. |
- // --------------------------------------------------------------------------------------------------------|
- const contributions = raw orelse return;
- if (contributions.rows) |rows| {
- allocator.free(rows[0..contributions.len]);
- }
- contributions.* = .{};
-}
-
-export fn zds_instrument_response_free(_: ?*Context, raw: ?*ZdsInstrumentResponse) void {
- // zds_instrument_response_free ---------------------------------------------------------------------------|
- // Release instrument-response rows returned by zds_instrument_response_sampling. |
- // --------------------------------------------------------------------------------------------------------|
- const response = raw orelse return;
- if (response.rows) |rows| {
- allocator.free(rows[0..response.len]);
- }
- response.* = .{};
-}
-
-export fn zds_o2_o2_cia_diagnostics_free(_: ?*Context, raw: ?*ZdsO2O2CIADiagnostics) void {
- // zds_o2_o2_cia_diagnostics_free -------------------------------------------------------------------------|
- // Release O2-O2 CIA rows returned by zds_o2_o2_cia_diagnostics. |
- // --------------------------------------------------------------------------------------------------------|
- const diagnostics = raw orelse return;
- if (diagnostics.rows) |rows| {
- allocator.free(rows[0..diagnostics.len]);
- }
- diagnostics.* = .{};
-}
-
pub export fn zds_last_error(ctx: ?*Context) [*:0]const u8 {
// zds_last_error -----------------------------------------------------------------------------------------|
// Return the context's last bounded nul-terminated error string. |
@@ -1503,63 +1380,14 @@ fn optimalEstimationPressureProfile(
};
}
-const WavelengthDiagnostic = enum {
- atmospheric_budget,
- line_contributions,
- instrument_response,
- cia_diagnostics,
-};
-
-fn DiagnosticExtra(comptime diagnostic: WavelengthDiagnostic) type {
- // DiagnosticExtra --------------------------------------------------------------------------------------- |
- // Compile each wavelength-diagnostic ABI wrapper with only the extra scalar it actually accepts. |
- // --------------------------------------------------------------------------------------------------------|
- return switch (diagnostic) {
- .atmospheric_budget, .cia_diagnostics => void,
- .line_contributions => usize,
- .instrument_response => u32,
- };
-}
-
-fn DiagnosticOutput(comptime diagnostic: WavelengthDiagnostic) type {
- // DiagnosticOutput -------------------------------------------------------------------------------------- |
- // Map one diagnostic kind to its stable extern output struct. |
- // --------------------------------------------------------------------------------------------------------|
- return switch (diagnostic) {
- .atmospheric_budget => ZdsAtmosphericBudget,
- .line_contributions => ZdsO2LineContributions,
- .instrument_response => ZdsInstrumentResponse,
- .cia_diagnostics => ZdsO2O2CIADiagnostics,
- };
-}
-
-fn DiagnosticResult(comptime diagnostic: WavelengthDiagnostic) type {
- // DiagnosticResult -------------------------------------------------------------------------------------- |
- // Map one diagnostic kind to the native owned row collection returned by root.zig. |
- // --------------------------------------------------------------------------------------------------------|
- return switch (diagnostic) {
- .atmospheric_budget => zdisamar.AtmosphericBudget,
- .line_contributions => zdisamar.LineContributions,
- .instrument_response => zdisamar.InstrumentResponse,
- .cia_diagnostics => zdisamar.CiaDiagnostics,
- };
-}
-
-fn runWavelengthDiagnostic(
- comptime diagnostic: WavelengthDiagnostic,
+fn runAtmosphericBudgetDiagnostic(
ctx: ?*Context,
wavelengths: ?[*]const f64,
wavelength_count: usize,
- extra: DiagnosticExtra(diagnostic),
- out: ?*DiagnosticOutput(diagnostic),
+ out: ?*ZdsAtmosphericBudget,
) c_int {
- // runWavelengthDiagnostic ------------------------------------------------------------------------------- |
- // Shared C-ABI body for row diagnostics that are selected by wavelength list. |
- // |
- // boundary |
- // The exported zds_* functions keep their exact C signatures. This helper specializes the native |
- // builder, output struct, and extra scalar at comptime so no runtime tag or type-erased row pointer |
- // crosses the ABI boundary. |
+ // runAtmosphericBudgetDiagnostic ----------------------------------------------------------------------- |
+ // Shared validation and row ownership transfer for the retained wavelength-selected diagnostic. |
// --------------------------------------------------------------------------------------------------------|
const resolved = ctx orelse return @intFromEnum(ZdsStatus.failure);
const prepared = &(resolved.prepared orelse {
@@ -1576,91 +1404,29 @@ fn runWavelengthDiagnostic(
return @intFromEnum(ZdsStatus.failure);
}
- if (diagnostic == .line_contributions and extra == 0) {
- resolved.setError("invalid O2 line contribution row limit");
- return @intFromEnum(ZdsStatus.failure);
- }
-
const output = out orelse {
- resolved.setError(nullDiagnosticOutputMessage(diagnostic));
+ resolved.setError("null atmospheric budget output");
return @intFromEnum(ZdsStatus.failure);
};
output.* = .{};
- const result = buildWavelengthDiagnostic(
- diagnostic,
+ const result = zdisamar.buildAtmosphericBudget(
+ allocator,
prepared,
wavelengths_ptr[0..wavelength_count],
- extra,
) catch |err| {
resolved.setError(@errorName(err));
return @intFromEnum(ZdsStatus.failure);
};
- publishWavelengthDiagnostic(diagnostic, output, result);
+ output.* = .{
+ .len = result.rows.len,
+ .rows = diagnosticRowsPointer(zdisamar.AtmosphericBudgetRow, result.rows),
+ };
resolved.setError("");
return @intFromEnum(ZdsStatus.ok);
}
-fn nullDiagnosticOutputMessage(comptime diagnostic: WavelengthDiagnostic) []const u8 {
- // nullDiagnosticOutputMessage --------------------------------------------------------------------------- |
- // Keep the previous per-export null-output error strings while sharing the control flow. |
- // --------------------------------------------------------------------------------------------------------|
- return switch (diagnostic) {
- .atmospheric_budget => "null atmospheric budget output",
- .line_contributions => "null O2 line contribution output",
- .instrument_response => "null instrument response output",
- .cia_diagnostics => "null O2-O2 CIA output",
- };
-}
-
-fn buildWavelengthDiagnostic(
- comptime diagnostic: WavelengthDiagnostic,
- prepared: *const zdisamar.Prepared,
- wavelengths_nm: []const f64,
- extra: DiagnosticExtra(diagnostic),
-) !DiagnosticResult(diagnostic) {
- // buildWavelengthDiagnostic ----------------------------------------------------------------------------- |
- // Dispatch to the native root builder selected by the exported ABI wrapper. |
- // --------------------------------------------------------------------------------------------------------|
- return switch (diagnostic) {
- .atmospheric_budget => zdisamar.buildAtmosphericBudget(allocator, prepared, wavelengths_nm),
- .line_contributions => zdisamar.buildLineContributions(allocator, prepared, wavelengths_nm, extra),
- .instrument_response => zdisamar.buildInstrumentResponse(allocator, prepared, wavelengths_nm, extra),
- .cia_diagnostics => zdisamar.buildCiaDiagnostics(allocator, prepared, wavelengths_nm),
- };
-}
-
-fn publishWavelengthDiagnostic(
- comptime diagnostic: WavelengthDiagnostic,
- output: *DiagnosticOutput(diagnostic),
- result: DiagnosticResult(diagnostic),
-) void {
- // publishWavelengthDiagnostic --------------------------------------------------------------------------- |
- // Move native row ownership to the stable C-facing output struct without copying rows. |
- // --------------------------------------------------------------------------------------------------------|
- switch (diagnostic) {
- .atmospheric_budget => output.* = .{
- .len = result.rows.len,
- .rows = diagnosticRowsPointer(zdisamar.AtmosphericBudgetRow, result.rows),
- },
- .line_contributions => output.* = .{
- .len = result.rows.len,
- .total_row_count = result.total_row_count,
- .truncated = @intFromBool(result.truncated),
- .rows = diagnosticRowsPointer(zdisamar.LineContributionRow, result.rows),
- },
- .instrument_response => output.* = .{
- .len = result.rows.len,
- .rows = diagnosticRowsPointer(zdisamar.InstrumentResponseRow, result.rows),
- },
- .cia_diagnostics => output.* = .{
- .len = result.rows.len,
- .rows = diagnosticRowsPointer(zdisamar.CiaRow, result.rows),
- },
- }
-}
-
fn diagnosticRowsPointer(comptime Row: type, rows: []const Row) ?[*]const Row {
// diagnosticRowsPointer --------------------------------------------------------------------------------- |
// Convert an owned native row slice to the nullable borrowed pointer used by the C ABI structs. |
@@ -1908,8 +1674,5 @@ comptime {
std.debug.assert(@sizeOf(ZdsSpectrum) == 64);
std.debug.assert(@sizeOf(ZdsDiagnosticReport) == 48);
std.debug.assert(@sizeOf(ZdsAtmosphericBudget) == 16);
- std.debug.assert(@sizeOf(ZdsO2LineContributions) == 32);
- std.debug.assert(@sizeOf(ZdsInstrumentResponse) == 16);
- std.debug.assert(@sizeOf(ZdsO2O2CIADiagnostics) == 16);
std.debug.assert(@sizeOf(CResult) == 176);
}
diff --git a/src/cache/profile_line_build.zig b/src/cache/profile_line_build.zig
new file mode 100644
index 00000000..4009d741
--- /dev/null
+++ b/src/cache/profile_line_build.zig
@@ -0,0 +1,1216 @@
+const std = @import("std");
+
+const readers = @import("../assets/readers.zig");
+const hashing = @import("../common/hashing.zig");
+const worker_partition = @import("../common/worker_partition.zig");
+const scene_input = @import("../input/scene.zig");
+const atmosphere_layers = @import("../setup/atmosphere_layers.zig");
+const line_tables = @import("../setup/line_tables.zig");
+const profile_line_memory = @import("profile_line_memory.zig");
+const line_physics = @import("../spectrum/line_physics.zig");
+const CostTiming = @import("../instrumentation/cost_timing.zig");
+const Trace = @import("../instrumentation/trace.zig");
+
+const Allocator = std.mem.Allocator;
+pub const ProfileLineValue = profile_line_memory.ProfileLineValue;
+pub const ProfileLineValues = profile_line_memory.ProfileLineValues;
+
+const min_spectroscopy_pressure_atm = line_physics.min_spectroscopy_pressure_atm;
+const max_spectroscopy_profile_nodes: usize = 64;
+const cutoff_grid_merge_tolerance_nm = 1.0e-9;
+const min_parallel_profile_line_state_count: usize = 4;
+const profile_line_state_chunk_size: usize = 2;
+const min_parallel_profile_cache_build_count: usize = 32;
+const profile_cache_build_chunk_size: usize = 8;
+
+// min_hitran_temperature_k -----------------------------------------------------------------------------------|
+// 150 K before HITRAN strength scaling. This protects the partition ratio, Boltzmann factor, Doppler width, |
+// and finite-difference derivative from nonphysical low-temperature inputs. |
+// ------------------------------------------------------------------------------------------------------------|
+const min_hitran_temperature_k = line_physics.min_hitran_temperature_k;
+
+// profile_line_build.zig -------------------------------------------------------------------------------------|
+// Setup-time build pipeline for ProfileLineValues. |
+// |
+// setup boundary |
+// builds a preparation-time line-value grid from the parsed HITRAN rows and the computed layer grid. |
+// Rows are indexed as wavelength-major, then layer-node minor, so later optics code can read a contiguous |
+// profile column for each exact wavelength without rebuilding weak-line Voigt terms. |
+// ------------------------------------------------------------------------------------------------------------|
+
+pub fn buildProfileLineValues(
+ allocator: Allocator,
+ scene: scene_input.Scene,
+) !ProfileLineValues {
+ // buildProfileLineValues ---------------------------------------------------------------------------------|
+ // Build line values over the scene's evenly spaced setup wavelengths. |
+ // --------------------------------------------------------------------------------------------------------|
+ const wavelength_count = scene.spectral_grid.sample_count;
+ const wavelengths_nm = try allocator.alloc(f64, wavelength_count);
+ defer allocator.free(wavelengths_nm);
+
+ const step_nm = if (wavelength_count > 1)
+ (scene.spectral_grid.end_nm - scene.spectral_grid.start_nm) / @as(f64, @floatFromInt(wavelength_count - 1))
+ else
+ 0.0;
+
+ for (wavelengths_nm, 0..) |*wavelength_nm, wavelength_index| {
+ wavelength_nm.* = scene.spectral_grid.start_nm + step_nm * @as(f64, @floatFromInt(wavelength_index));
+ }
+
+ return buildProfileLineValuesForWavelengths(allocator, scene, wavelengths_nm);
+}
+
+pub fn buildProfileLineValuesForWavelengths(
+ allocator: Allocator,
+ scene: scene_input.Scene,
+ wavelengths_nm: []const f64,
+) !ProfileLineValues {
+ // buildProfileLineValuesForWavelengths -------------------------------------------------------------------|
+ // Build retained line values with the scalar weak-line cutoff fallback used by canonical setup tests. |
+ // --------------------------------------------------------------------------------------------------------|
+ return buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ allocator,
+ scene,
+ wavelengths_nm,
+ &.{},
+ true,
+ true,
+ null,
+ preferredProfileLineWorkerCount(wavelengths_nm.len),
+ &.{},
+ );
+}
+
+pub fn buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ allocator: Allocator,
+ scene: scene_input.Scene,
+ wavelengths_nm: []const f64,
+ cutoff_grid_wavelengths_nm: []const f64,
+ build_layer_values: bool,
+ include_temperature_derivatives: bool,
+ pool: ?*std.Thread.Pool,
+ worker_count: usize,
+ cost_timing_active: []const ?CostTiming.Active,
+) !ProfileLineValues {
+ // buildProfileLineValuesForWavelengthsWithCutoffGrid -----------------------------------------------------|
+ // Build retained line values over a caller-provided exact wavelength list. |
+ // |
+ // the exact radiance wavelengths selected by `spectrum/radiance_wavelengths.zig` instead of assuming |
+ // a uniform public product grid. |
+ // |
+ // row contract |
+ // Output rows stay wavelength-major and preserve the input wavelength order exactly. Dense spectrum |
+ // prefetch can therefore use its `RadianceWavelengthList` index as the ProfileLineValues index. |
+ // `build_layer_values` keeps the diagnostic layer rows for evidence paths. The public spectrum |
+ // route uses only support-profile total sigma, so root skips preparing unused layer rows. |
+ // --------------------------------------------------------------------------------------------------------|
+ var build_arena = std.heap.ArenaAllocator.init(allocator);
+ defer build_arena.deinit();
+ const scratch_allocator = build_arena.allocator();
+
+ const layers = try atmosphere_layers.build(scratch_allocator, scene);
+ const lines = try line_tables.build(scratch_allocator, scene);
+
+ const wavelength_count = wavelengths_nm.len;
+ const profile_node_count = if (build_layer_values) layers.layer_pressures_hpa.len else 0;
+ const support_profile_node_count = layers.spectroscopy_profile.rows.len;
+ const values = try allocator.alloc(ProfileLineValue, wavelength_count * profile_node_count);
+ errdefer allocator.free(values);
+ const support_profile_total_sigma = try allocator.alloc(f64, wavelength_count * support_profile_node_count);
+ errdefer allocator.free(support_profile_total_sigma);
+
+ const cutoff_grid = try prepareCutoffGrid(scratch_allocator, cutoff_grid_wavelengths_nm);
+ const runtime = RuntimeControls{
+ .cutoff_cm1 = lines.cutoff_sim_cm1,
+ .line_mixing_factor = lines.line_mixing_factor,
+ .cutoff_grid_wavelengths_nm = cutoff_grid.wavelengths_nm,
+ .cutoff_grid_wavenumbers_cm1 = cutoff_grid.wavenumbers_cm1,
+ };
+ const total_lines = try collectRuntimeLines(scratch_allocator, lines.rows, lines.isotopes_sim);
+
+ const strong_sidecars = chooseStrongLineSidecars(lines);
+
+ var active_lines: []readers.LineAssetRow = &.{};
+ var weak_states: []WeakLinePreparedState = &.{};
+ var total_weak_states: []WeakLinePreparedState = &.{};
+ var strong_states: []StrongLinePreparedState = &.{};
+
+ if (build_layer_values) {
+ const line_strength_threshold = thresholdStrength(lines.rows, lines.threshold_line_sim);
+ active_lines = try collectActiveLines(
+ scratch_allocator,
+ lines.rows,
+ lines.isotopes_sim,
+ line_strength_threshold,
+ );
+ weak_states = try prepareLayerWeakLineStates(
+ scratch_allocator,
+ active_lines,
+ layers.layer_temperatures_k,
+ layers.layer_pressures_hpa,
+ 0.0,
+ cost_timing_active,
+ );
+ total_weak_states = try prepareLayerWeakLineStates(
+ scratch_allocator,
+ total_lines,
+ layers.layer_temperatures_k,
+ layers.layer_pressures_hpa,
+ 0.0,
+ cost_timing_active,
+ );
+ strong_states = try prepareLayerStrongLineStates(
+ scratch_allocator,
+ strong_sidecars.lines,
+ strong_sidecars.relaxation_matrix,
+ layers.layer_temperatures_k,
+ layers.layer_pressures_hpa,
+ cost_timing_active,
+ );
+ }
+
+ // The canonical temperature derivative is a centered finite difference at T +/- 0.5 K. The public
+ // Jacobian states are surface/aerosol controls, so root spectrum runs do not read d_sigma/dT and skip
+ // these two full weak-line state families. Explicit profile-line canonical builders still request the rows
+ // directly, and the reuse stamp records the choice so mismatched session caches cannot mix.
+ var upper_weak_states: []WeakLinePreparedState = &.{};
+ var lower_weak_states: []WeakLinePreparedState = &.{};
+ if (build_layer_values and include_temperature_derivatives) {
+ upper_weak_states = try prepareLayerWeakLineStates(
+ scratch_allocator,
+ active_lines,
+ layers.layer_temperatures_k,
+ layers.layer_pressures_hpa,
+ 0.5,
+ cost_timing_active,
+ );
+ lower_weak_states = try prepareLayerWeakLineStates(
+ scratch_allocator,
+ active_lines,
+ layers.layer_temperatures_k,
+ layers.layer_pressures_hpa,
+ -0.5,
+ cost_timing_active,
+ );
+ }
+ const support_total_weak_states = try prepareProfileWeakLineStates(
+ scratch_allocator,
+ total_lines,
+ layers.spectroscopy_profile.rows,
+ cost_timing_active,
+ );
+ const support_strong_states = try prepareProfileStrongLineStates(
+ scratch_allocator,
+ strong_sidecars.lines,
+ strong_sidecars.relaxation_matrix,
+ layers.spectroscopy_profile.rows,
+ cost_timing_active,
+ );
+
+ try buildProfileLineValuesByWavelength(
+ pool,
+ worker_count,
+ wavelengths_nm,
+ layers,
+ strong_sidecars.lines,
+ strong_sidecars.relaxation_matrix,
+ runtime,
+ build_layer_values and include_temperature_derivatives,
+ active_lines,
+ total_lines,
+ weak_states,
+ upper_weak_states,
+ lower_weak_states,
+ total_weak_states,
+ support_total_weak_states,
+ strong_states,
+ support_strong_states,
+ values,
+ support_profile_total_sigma,
+ cost_timing_active,
+ );
+
+ return .{
+ .values = values,
+ .support_profile_total_sigma_cm2_per_molecule = support_profile_total_sigma,
+ .wavelength_count = wavelength_count,
+ .profile_node_count = profile_node_count,
+ .support_profile_node_count = support_profile_node_count,
+ .reuse_stamp = profileLineReuseStamp(
+ scene.id,
+ lines,
+ layers.spectroscopy_profile.rows,
+ wavelengths_nm,
+ build_layer_values,
+ include_temperature_derivatives,
+ ),
+ };
+}
+
+// ProfileLineBuildWorker -------------------------------------------------------------------------------------|
+// Site-local worker row for wavelength-major profile-line value construction. |
+// |
+// layout(64-bit) |
+// This row is not size-pinned because it carries many borrowed slice descriptors and only lives on the build |
+// stack. No field owns heap storage; every output write is to a disjoint wavelength-major row range. |
+// |
+// memory groups |
+// inputs : exact wavelengths, layer/support thermodynamic rows, HITRAN/strong-line tables, runtime |
+// prepared : weak-line and strong-line states prepared before workers start |
+// outputs : wavelength-major layer rows and spectroscopy-profile support rows |
+// scheduling : static range, worker index |
+const ProfileLineBuildWorker = struct {
+ wavelengths_nm: []const f64,
+ layer_pressures_hpa: []const f64,
+ layer_temperatures_k: []const f64,
+ layer_interval_indices_1based: []const u32,
+ support_profile_rows: []const readers.AtmosphereProfileRow,
+ runtime: RuntimeControls,
+ include_temperature_derivatives: bool,
+ active_lines: []const readers.LineAssetRow,
+ total_lines: []const readers.LineAssetRow,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ weak_states: []const WeakLinePreparedState,
+ upper_weak_states: []const WeakLinePreparedState,
+ lower_weak_states: []const WeakLinePreparedState,
+ total_weak_states: []const WeakLinePreparedState,
+ support_total_weak_states: []const WeakLinePreparedState,
+ strong_states: []const StrongLinePreparedState,
+ support_strong_states: []const StrongLinePreparedState,
+ values: []ProfileLineValue,
+ support_profile_total_sigma_cm2_per_molecule: []f64,
+ start_index: usize,
+ end_index: usize,
+ worker_index: usize = 0,
+ cost_timing_active: []const ?CostTiming.Active,
+};
+// ------------------------------------------------------------------------------------------------------------|
+
+// ProfileLineStateWorker -------------------------------------------------------------------------------------|
+// Site-local raw worker row for profile-node weak/strong line-state preparation. |
+// |
+// layout(64-bit) |
+// This worker row is not size-pinned because it carries borrowed slices plus a relaxation-matrix view. |
+// Workers perform no allocation; the caller allocates output row storage before launch and deinitializes it |
+// after the later spectroscopy-value phase. |
+// |
+// memory groups |
+// inputs : thermodynamic node rows and parsed weak/strong-line tables |
+// outputs : optional weak-line and strong-line prepared states, indexed by profile node |
+// scheduling : shared chunk queue, worker index |
+const ProfileLineStateWorker = struct {
+ lines: []const readers.LineAssetRow,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ temperatures_k: []const f64,
+ pressures_hpa: []const f64,
+ temperature_offset_k: f64 = 0.0,
+ weak_states: ?[]WeakLinePreparedState = null,
+ strong_states: ?[]StrongLinePreparedState = null,
+ queue: *worker_partition.ChunkQueue,
+ worker_index: usize,
+ cost_timing_active: []const ?CostTiming.Active,
+};
+// ------------------------------------------------------------------------------------------------------------|
+
+fn costTimingForWorker(
+ cost_timing_active: []const ?CostTiming.Active,
+ worker_index: usize,
+) ?CostTiming.Active {
+ // costTimingForWorker ------------------------------------------------------------------------------------|
+ // Borrow the caller-supplied per-worker cost row when this worker group maps to one. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (worker_index >= cost_timing_active.len) return null;
+ return cost_timing_active[worker_index];
+}
+
+fn buildProfileLineValuesByWavelength(
+ pool: ?*std.Thread.Pool,
+ worker_count: usize,
+ wavelengths_nm: []const f64,
+ layers: atmosphere_layers.LayerGrid,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ runtime: RuntimeControls,
+ include_temperature_derivatives: bool,
+ active_lines: []const readers.LineAssetRow,
+ total_lines: []const readers.LineAssetRow,
+ weak_states: []const WeakLinePreparedState,
+ upper_weak_states: []const WeakLinePreparedState,
+ lower_weak_states: []const WeakLinePreparedState,
+ total_weak_states: []const WeakLinePreparedState,
+ support_total_weak_states: []const WeakLinePreparedState,
+ strong_states: []const StrongLinePreparedState,
+ support_strong_states: []const StrongLinePreparedState,
+ values: []ProfileLineValue,
+ support_profile_total_sigma_cm2_per_molecule: []f64,
+ cost_timing_active: []const ?CostTiming.Active,
+) !void {
+ // buildProfileLineValuesByWavelength ---------------------------------------------------------------------|
+ // Fill retained profile-line values over exact wavelengths, partitioned exactly like the profile |
+ // spectroscopy cache build: threshold 32, static worker ranges, chunk size 8, and optional session pool. |
+ // |
+ // instrumentation |
+ // `profile_spectroscopy_cache.build` measures this whole setup phase. Worker and chunk zones live in |
+ // `profileLineBuildWorkerMain` and retain the trace names for profile-cache construction. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (worker_count == 0 or worker_count > worker_partition.max_workers) return error.InvalidShape;
+ if (wavelengths_nm.len == 0) return;
+
+ const profile_node_count = if (values.len != 0) layers.layer_pressures_hpa.len else 0;
+ const support_profile_node_count = layers.spectroscopy_profile.rows.len;
+ if (values.len != wavelengths_nm.len * profile_node_count) return error.InvalidShape;
+ if (support_profile_total_sigma_cm2_per_molecule.len != wavelengths_nm.len * support_profile_node_count) {
+ return error.InvalidShape;
+ }
+
+ // instrumentation: trace zone: profile spectroscopy cache build ----------------------------------------- |
+ // captures: profile-line cache build wall time and exact-wavelength count |
+ // why: shows setup cost that compares before forward prefetch starts. |
+ const zone = Trace.staticZone(@src(), "profile_spectroscopy_cache.build");
+ zone.value(@intCast(wavelengths_nm.len));
+ defer zone.end();
+ // end instrumentation: trace zone: profile spectroscopy cache build ------------------------------------- |
+
+ var worker_storage: [worker_partition.max_workers]ProfileLineBuildWorker = undefined;
+ for (0..worker_count) |worker_index| {
+ const range = worker_partition.staticRange(wavelengths_nm.len, worker_count, worker_index);
+ worker_storage[worker_index] = .{
+ .wavelengths_nm = wavelengths_nm,
+ .layer_pressures_hpa = layers.layer_pressures_hpa,
+ .layer_temperatures_k = layers.layer_temperatures_k,
+ .layer_interval_indices_1based = layers.layer_interval_indices_1based,
+ .support_profile_rows = layers.spectroscopy_profile.rows,
+ .runtime = runtime,
+ .include_temperature_derivatives = include_temperature_derivatives,
+ .active_lines = active_lines,
+ .total_lines = total_lines,
+ .strong_lines = strong_lines,
+ .relaxation_matrix = relaxation_matrix,
+ .weak_states = weak_states,
+ .upper_weak_states = upper_weak_states,
+ .lower_weak_states = lower_weak_states,
+ .total_weak_states = total_weak_states,
+ .support_total_weak_states = support_total_weak_states,
+ .strong_states = strong_states,
+ .support_strong_states = support_strong_states,
+ .values = values,
+ .support_profile_total_sigma_cm2_per_molecule = support_profile_total_sigma_cm2_per_molecule,
+ .start_index = range.start,
+ .end_index = range.end,
+ .worker_index = worker_index,
+ .cost_timing_active = cost_timing_active,
+ };
+ }
+
+ worker_partition.runWorkers(pool, worker_storage[0..worker_count], profileLineBuildWorkerMain);
+}
+
+fn profileLineBuildWorkerMain(worker: *ProfileLineBuildWorker) void {
+ // profileLineBuildWorkerMain -----------------------------------------------------------------------------|
+ // Fill one worker-owned range of wavelength-major profile-line rows. |
+ // --------------------------------------------------------------------------------------------------------|
+ var thread_name_buffer: [64]u8 = undefined;
+ const thread_name = std.fmt.bufPrintZ(
+ &thread_name_buffer,
+ "zdisamar-profile-cache-{d}",
+ .{worker.worker_index},
+ ) catch "zdisamar-profile-cache";
+
+ // instrumentation: trace thread label: profile spectroscopy cache worker -------------------------------- |
+ // captures: profile-line build worker identity |
+ // why: makes parallel profile-cache lanes separable in timeline traces. |
+ Trace.setThreadName(thread_name);
+ // end instrumentation: trace thread label: profile spectroscopy cache worker ---------------------------- |
+
+ // instrumentation: trace zone: profile spectroscopy cache worker ---------------------------------------- |
+ // captures: worker wall time for retained profile-line value construction |
+ // why: exposes load balance across exact-wavelength profile-cache ranges. |
+ const worker_zone = Trace.staticZone(@src(), "profile_spectroscopy_cache.worker");
+ worker_zone.value(@intCast(worker.worker_index));
+ defer worker_zone.end();
+ // end instrumentation: trace zone: profile spectroscopy cache worker ------------------------------------ |
+
+ var chunk_start = worker.start_index;
+ while (worker_partition.nextStaticChunk(
+ &chunk_start,
+ worker.end_index,
+ profile_cache_build_chunk_size,
+ )) |chunk| {
+
+ // instrumentation: trace zone: profile spectroscopy cache build chunk --------------------------------|
+ // captures: chunk wall time and exact-wavelength row count |
+ // why: preserves the chunk boundary while showing uneven wavelength-window costs. |
+ const chunk_zone = Trace.deepStaticZone(@src(), "profile_spectroscopy_cache.build");
+ chunk_zone.value(@intCast(chunk.len()));
+ defer chunk_zone.end();
+ // end instrumentation: trace zone: profile spectroscopy cache build chunk ----------------------------|
+
+ for (chunk.start..chunk.end) |wavelength_index| {
+ if (worker.values.len != 0) fillProfileLineValueRowsAtWavelength(worker, wavelength_index);
+ fillSupportProfileLineValueRowsAtWavelength(worker, wavelength_index);
+ }
+ }
+}
+
+fn fillProfileLineValueRowsAtWavelength(worker: *ProfileLineBuildWorker, wavelength_index: usize) void {
+ // fillProfileLineValueRowsAtWavelength -------------------------------------------------------------------|
+ // Fill all layer-node line-value rows for one exact wavelength. |
+ // --------------------------------------------------------------------------------------------------------|
+ const wavelength_nm = worker.wavelengths_nm[wavelength_index];
+ const active_window = line_physics.relevantLineWindow(
+ worker.active_lines,
+ wavelength_nm,
+ worker.runtime.cutoff_cm1,
+ );
+ const total_window = line_physics.relevantLineWindow(worker.total_lines, wavelength_nm, worker.runtime.cutoff_cm1);
+ const wavelength_state = line_physics.prepareWeakLineWavelengthState(
+ wavelength_nm,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ );
+ const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
+ for (worker.layer_pressures_hpa, worker.layer_temperatures_k, 0..) |pressure_hpa, temperature_k, node_index| {
+ const sigma = line_physics.weakLineSigmaAtPrepared(
+ worker.active_lines,
+ active_window,
+ &worker.weak_states[node_index],
+ wavelength_state,
+ worker.runtime.cutoff_cm1,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ );
+
+ var d_sigma_d_temperature: f64 = 0.0;
+ if (worker.include_temperature_derivatives) {
+ const upper_sigma = line_physics.weakLineSigmaAtPreparedFiniteDifference(
+ worker.active_lines,
+ active_window,
+ &worker.upper_weak_states[node_index],
+ wavelength_state,
+ worker.runtime.cutoff_cm1,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ );
+ const lower_sigma = line_physics.weakLineSigmaAtPreparedFiniteDifference(
+ worker.active_lines,
+ active_window,
+ &worker.lower_weak_states[node_index],
+ wavelength_state,
+ worker.runtime.cutoff_cm1,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ );
+ d_sigma_d_temperature = (upper_sigma - lower_sigma) / 1.0;
+ }
+
+ const total = line_physics.totalSpectroscopyAt(
+ worker.total_lines,
+ total_window,
+ worker.strong_lines,
+ worker.relaxation_matrix,
+ wavelength_nm,
+ wavelength_state,
+ temperature_k,
+ pressure_hpa,
+ worker.runtime.cutoff_cm1,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ worker.runtime.line_mixing_factor,
+ &worker.strong_states[node_index],
+ &worker.total_weak_states[node_index],
+ stage_cost,
+ );
+ const row_index = wavelength_index * worker.layer_pressures_hpa.len + node_index;
+ worker.values[row_index] = .{
+ .wavelength_nm = wavelength_nm,
+ .layer_index = @intCast(node_index),
+ .interval_index_1based = worker.layer_interval_indices_1based[node_index],
+ .pressure_hpa = pressure_hpa,
+ .temperature_k = temperature_k,
+ .weak_line_sigma_cm2_per_molecule = sigma,
+ .strong_line_sigma_cm2_per_molecule = total.strong_line_sigma_cm2_per_molecule,
+ .line_sigma_cm2_per_molecule = total.line_sigma_cm2_per_molecule,
+ .line_mixing_sigma_cm2_per_molecule = total.line_mixing_sigma_cm2_per_molecule,
+ .total_sigma_cm2_per_molecule = total.total_sigma_cm2_per_molecule,
+ .d_sigma_d_temperature_cm2_per_molecule_per_k = d_sigma_d_temperature,
+ };
+ }
+}
+
+fn fillSupportProfileLineValueRowsAtWavelength(worker: *ProfileLineBuildWorker, wavelength_index: usize) void {
+ // fillSupportProfileLineValueRowsAtWavelength ------------------------------------------------------------|
+ // Fill all spectroscopy-profile support rows for one exact wavelength. |
+ // --------------------------------------------------------------------------------------------------------|
+ const wavelength_nm = worker.wavelengths_nm[wavelength_index];
+ const total_window = line_physics.relevantLineWindow(worker.total_lines, wavelength_nm, worker.runtime.cutoff_cm1);
+ const wavelength_state = line_physics.prepareWeakLineWavelengthState(
+ wavelength_nm,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ );
+ const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
+ for (worker.support_profile_rows, 0..) |profile_row, node_index| {
+ const total = line_physics.totalSpectroscopyAt(
+ worker.total_lines,
+ total_window,
+ worker.strong_lines,
+ worker.relaxation_matrix,
+ wavelength_nm,
+ wavelength_state,
+ profile_row.temperature_k,
+ profile_row.pressure_hpa,
+ worker.runtime.cutoff_cm1,
+ worker.runtime.cutoff_grid_wavelengths_nm,
+ worker.runtime.cutoff_grid_wavenumbers_cm1,
+ worker.runtime.line_mixing_factor,
+ &worker.support_strong_states[node_index],
+ &worker.support_total_weak_states[node_index],
+ stage_cost,
+ );
+ const row_index = wavelength_index * worker.support_profile_rows.len + node_index;
+ worker.support_profile_total_sigma_cm2_per_molecule[row_index] = total.total_sigma_cm2_per_molecule;
+ }
+}
+
+fn preferredProfileLineWorkerCount(wavelength_count: usize) usize {
+ // preferredProfileLineWorkerCount ------------------------------------------------------------------------|
+ // Resolve the profile-cache build worker-count policy for retained exact-wavelength spectroscopy. |
+ // --------------------------------------------------------------------------------------------------------|
+ return worker_partition.preferredWorkerCount(
+ wavelength_count,
+ min_parallel_profile_cache_build_count,
+ );
+}
+
+fn preferredProfileLineStateWorkerCount(profile_count: usize) usize {
+ // preferredProfileLineStateWorkerCount -------------------------------------------------------------------|
+ // Resolve the thermodynamic profile-line state worker-count policy. |
+ // --------------------------------------------------------------------------------------------------------|
+ return worker_partition.preferredWorkerCount(
+ profile_count,
+ min_parallel_profile_line_state_count,
+ );
+}
+
+pub fn profileLineReuseStamp(
+ scene_id: []const u8,
+ lines: line_tables.LineTable,
+ spectroscopy_profile_rows: []const readers.AtmosphereProfileRow,
+ wavelengths_nm: []const f64,
+ build_layer_values: bool,
+ include_temperature_derivatives: bool,
+) hashing.ReuseStamp {
+ // profileLineReuseStamp ----------------------------------------------------------------------------------|
+ // Hash only session-fixed inputs: scene identity, exact wavelengths, the parsed line assets, and the |
+ // spectroscopy-profile thermodynamics. Do not hash LayerGrid support rows: profile-line values sit at |
+ // fixed spectroscopy nodes and are resampled per pressure state downstream, so hashing support placement |
+ // would defeat reuse across the pressure changes optimal estimation makes each iteration. |
+ // --------------------------------------------------------------------------------------------------------|
+ var hasher = std.hash.Wyhash.init(0);
+ hasher.update(scene_id);
+ hashF64Slice(&hasher, wavelengths_nm);
+ hashLineInputs(&hasher, lines);
+ hashAtmosphereRows(&hasher, spectroscopy_profile_rows);
+ hashing.updateBool(&hasher, build_layer_values);
+ hashing.updateBool(&hasher, include_temperature_derivatives);
+ return .{ .value = hasher.final() };
+}
+
+fn hashLineInputs(hasher: *std.hash.Wyhash, lines: line_tables.LineTable) void {
+ // hashLineInputs -----------------------------------------------------------------------------------------|
+ // Hash parsed line assets and runtime line-filter controls without reading struct padding bytes. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashLineRows(hasher, lines.rows);
+ hashStrongLineRows(hasher, lines.strong_lines);
+ hashRelaxationMatrix(hasher, lines.relaxation_matrix);
+ hashing.updateValue(hasher, lines.isotopes_sim.len);
+ hasher.update(lines.isotopes_sim);
+ hashing.updateValue(hasher, lines.threshold_line_sim);
+ hashing.updateValue(hasher, lines.cutoff_sim_cm1);
+ hashing.updateValue(hasher, lines.line_mixing_factor);
+}
+
+fn hashLineRows(hasher: *std.hash.Wyhash, rows: []const readers.LineAssetRow) void {
+ // hashLineRows -------------------------------------------------------------------------------------------|
+ // Hash one HITRAN row family field-by-field so optional metadata participates in cache identity. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateValue(hasher, rows.len);
+ for (rows) |row| {
+ hashing.updateValue(hasher, row.gas_index);
+ hashing.updateValue(hasher, row.isotope_number);
+ hashing.updateBool(hasher, row.vendor_filter_metadata_from_source);
+ for ([_]f64{
+ row.center_wavelength_nm,
+ row.center_wavenumber_cm1,
+ row.line_strength_cm2_per_molecule,
+ row.air_half_width_cm1,
+ row.lower_state_energy_cm1,
+ row.temperature_exponent,
+ row.pressure_shift_cm1,
+ }) |value| hashing.updateValue(hasher, value);
+ hashOptionalU8(hasher, row.branch_ic1);
+ hashOptionalU8(hasher, row.branch_ic2);
+ hashOptionalU8(hasher, row.rotational_nf);
+ }
+}
+
+fn hashStrongLineRows(hasher: *std.hash.Wyhash, rows: []const readers.StrongLineAssetRow) void {
+ // hashStrongLineRows -------------------------------------------------------------------------------------|
+ // Hash LISA SDF sidecar rows that can contribute to strong-line and line-mixing sigma. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateValue(hasher, rows.len);
+ for (rows) |row| {
+ for ([_]f64{
+ row.center_wavenumber_cm1,
+ row.center_wavelength_nm,
+ row.population_t0,
+ row.dipole_ratio,
+ row.dipole_t0,
+ row.lower_state_energy_cm1,
+ row.air_half_width_cm1,
+ row.air_half_width_nm,
+ row.temperature_exponent,
+ row.pressure_shift_cm1,
+ row.pressure_shift_nm,
+ }) |value| hashing.updateValue(hasher, value);
+ hashing.updateValue(hasher, row.rotational_index_m1);
+ }
+}
+
+fn hashRelaxationMatrix(hasher: *std.hash.Wyhash, matrix: readers.RelaxationMatrixAsset) void {
+ // hashRelaxationMatrix -----------------------------------------------------------------------------------|
+ // Hash the dense LISA RMF matrices and their square dimension. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateValue(hasher, matrix.line_count);
+ hashF64Slice(hasher, matrix.wt0);
+ hashF64Slice(hasher, matrix.bw);
+}
+
+fn hashAtmosphereRows(hasher: *std.hash.Wyhash, rows: []const readers.AtmosphereProfileRow) void {
+ // hashAtmosphereRows -------------------------------------------------------------------------------------|
+ // Hash spectroscopy-profile thermodynamics used by support-profile sigma rows. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateValue(hasher, rows.len);
+ for (rows) |row| {
+ for ([_]f64{
+ row.altitude_km,
+ row.pressure_hpa,
+ row.temperature_k,
+ row.air_number_density_cm3,
+ }) |value| hashing.updateValue(hasher, value);
+ }
+}
+
+fn hashF64Slice(hasher: *std.hash.Wyhash, values: []const f64) void {
+ // hashF64Slice -------------------------------------------------------------------------------------------|
+ // Hash an f64 slice with its length so empty and repeated-prefix rows stay distinct. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateValue(hasher, values.len);
+ hasher.update(std.mem.sliceAsBytes(values));
+}
+
+fn hashOptionalU8(hasher: *std.hash.Wyhash, value: ?u8) void {
+ // hashOptionalU8 -----------------------------------------------------------------------------------------|
+ // Hash optional compact metadata with explicit presence. |
+ // --------------------------------------------------------------------------------------------------------|
+ hashing.updateBool(hasher, value != null);
+ if (value) |resolved| hashing.updateValue(hasher, resolved);
+}
+
+fn prepareCutoffGrid(allocator: Allocator, support_wavelengths_nm: []const f64) !CutoffGrid {
+ // prepareCutoffGrid --------------------------------------------------------------------------------------|
+ // Sort and merge the realized weak-line support grid before computing matching wavenumbers. |
+ // |
+ // --------------------------------------------------------------------------------------------------------|
+ if (support_wavelengths_nm.len < 2) return .{};
+
+ const wavelengths = try allocator.dupe(f64, support_wavelengths_nm);
+ errdefer allocator.free(wavelengths);
+ std.mem.sort(f64, wavelengths, {}, lessThanF64);
+
+ var merged_count: usize = 0;
+ for (wavelengths) |wavelength_nm| {
+ if (merged_count != 0 and
+ @abs(wavelengths[merged_count - 1] - wavelength_nm) <= cutoff_grid_merge_tolerance_nm)
+ {
+ continue;
+ }
+ wavelengths[merged_count] = wavelength_nm;
+ merged_count += 1;
+ }
+
+ const merged_wavelengths = try allocator.realloc(wavelengths, merged_count);
+ errdefer allocator.free(merged_wavelengths);
+ const wavenumbers = try allocator.alloc(f64, merged_count);
+ errdefer allocator.free(wavenumbers);
+ for (merged_wavelengths, wavenumbers) |wavelength_nm, *wavenumber_cm1| {
+ wavenumber_cm1.* = line_physics.wavelengthToWavenumberCm1(wavelength_nm);
+ }
+ return .{ .wavelengths_nm = merged_wavelengths, .wavenumbers_cm1 = wavenumbers };
+}
+
+fn lessThanF64(_: void, lhs: f64, rhs: f64) bool {
+ // lessThanF64 --------------------------------------------------------------------------------------------|
+ // Sort finite support wavelengths in ascending wavelength order for cutoff-grid index searches. |
+ // --------------------------------------------------------------------------------------------------------|
+ return lhs < rhs;
+}
+
+fn chooseStrongLineSidecars(lines: line_tables.LineTable) StrongLineSidecars {
+ // chooseStrongLineSidecars -------------------------------------------------------------------------------|
+ // Use LISA strong-line sidecars only for isotope-1 O2 rows. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (isotopeOneActive(lines.isotopes_sim)) {
+ return .{
+ .lines = lines.strong_lines,
+ .relaxation_matrix = lines.relaxation_matrix,
+ };
+ }
+
+ return .{
+ .lines = &.{},
+ .relaxation_matrix = emptyRelaxationMatrix(),
+ };
+}
+
+// RuntimeControls --------------------------------------------------------------------------------------------|
+// Borrowed line-list controls needed by weak-line setup evaluation. |
+// |
+// layout(64-bit) |
+// size: 48 B (0.047 KiB), align: 8 B |
+// |
+// memory |
+// [ 0.. 7] cutoff_cm1 : f64 |
+// [ 8..15] line_mixing_factor : f64 |
+// [16..31] cutoff_grid_wavelengths_nm : []const f64 |
+// [32..47] cutoff_grid_wavenumbers_cm1: []const f64 |
+const RuntimeControls = struct {
+ cutoff_cm1: f64,
+ line_mixing_factor: f64,
+ cutoff_grid_wavelengths_nm: []const f64 = &.{},
+ cutoff_grid_wavenumbers_cm1: []const f64 = &.{},
+};
+// ------------------------------------------------------------------------------------------------------------|
+
+const StrongLineSidecars = struct {
+ lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+};
+// ------------------------------------------------------------------------------------------------------------|
+
+// CutoffGrid -------------------------------------------------------------------------------------------------|
+// Owned weak-line cutoff support grid used during setup-time line evaluation. |
+// |
+// layout(64-bit) |
+// size: 32 B (0.031 KiB), align: 8 B |
+// |
+// memory |
+// [ 0..15] wavelengths_nm : []f64 |
+// [16..31] wavenumbers_cm1: []f64 |
+const CutoffGrid = struct {
+ wavelengths_nm: []f64 = &.{},
+ wavenumbers_cm1: []f64 = &.{},
+};
+// ------------------------------------------------------------------------------------------------------------|
+
+const StrongLinePreparedState = line_physics.StrongLinePreparedState;
+
+const WeakLinePreparedLineState = line_physics.WeakLinePreparedLineState;
+
+const WeakLinePreparedState = line_physics.WeakLinePreparedState;
+
+fn thresholdStrength(lines: []const readers.LineAssetRow, scale: f64) f64 {
+ // thresholdStrength --------------------------------------------------------------------------------------|
+ // Convert the relative weak-line threshold control into an absolute line-strength floor. |
+ // --------------------------------------------------------------------------------------------------------|
+ var max_strength: f64 = 0.0;
+ for (lines) |line| {
+ max_strength = @max(max_strength, line.line_strength_cm2_per_molecule);
+ }
+ return max_strength * scale;
+}
+
+fn collectActiveLines(
+ allocator: Allocator,
+ lines: []const readers.LineAssetRow,
+ active_isotopes: []const u8,
+ line_strength_threshold: f64,
+) ![]readers.LineAssetRow {
+ // collectActiveLines -------------------------------------------------------------------------------------|
+ // Copy weak-line rows that participate in setup evaluation, sorted for local window scans. |
+ // --------------------------------------------------------------------------------------------------------|
+ var active_count: usize = 0;
+ for (lines) |line| {
+ if (activeLine(line, active_isotopes, line_strength_threshold)) active_count += 1;
+ }
+
+ const active_lines = try allocator.alloc(readers.LineAssetRow, active_count);
+ errdefer allocator.free(active_lines);
+
+ var active_index: usize = 0;
+ for (lines) |line| {
+ if (!activeLine(line, active_isotopes, line_strength_threshold)) continue;
+ active_lines[active_index] = line;
+ active_index += 1;
+ }
+
+ std.mem.sort(readers.LineAssetRow, active_lines, {}, lessByCenterWavelength);
+ return active_lines;
+}
+
+fn collectRuntimeLines(
+ allocator: Allocator,
+ lines: []const readers.LineAssetRow,
+ active_isotopes: []const u8,
+) ![]readers.LineAssetRow {
+ // collectRuntimeLines ------------------------------------------------------------------------------------|
+ // Copy O2 rows that participate in total-sigma evaluation, without applying the weak-line threshold. |
+ // --------------------------------------------------------------------------------------------------------|
+ var active_count: usize = 0;
+ for (lines) |line| {
+ if (runtimeLine(line, active_isotopes)) active_count += 1;
+ }
+
+ const active_lines = try allocator.alloc(readers.LineAssetRow, active_count);
+ errdefer allocator.free(active_lines);
+
+ var active_index: usize = 0;
+ for (lines) |line| {
+ if (!runtimeLine(line, active_isotopes)) continue;
+ active_lines[active_index] = line;
+ active_index += 1;
+ }
+
+ std.mem.sort(readers.LineAssetRow, active_lines, {}, lessByCenterWavelength);
+ return active_lines;
+}
+
+fn lessByCenterWavelength(_: void, lhs: readers.LineAssetRow, rhs: readers.LineAssetRow) bool {
+ // lessByCenterWavelength ---------------------------------------------------------------------------------|
+ // Order line rows by center wavelength so binary searches can isolate a local HITRAN cutoff window. |
+ // --------------------------------------------------------------------------------------------------------|
+ return lhs.center_wavelength_nm < rhs.center_wavelength_nm;
+}
+
+fn prepareLayerWeakLineStates(
+ allocator: Allocator,
+ lines: []const readers.LineAssetRow,
+ temperatures_k: []const f64,
+ pressures_hpa: []const f64,
+ temperature_offset_k: f64,
+ cost_timing_active: []const ?CostTiming.Active,
+) ![]WeakLinePreparedState {
+ // prepareLayerWeakLineStates -----------------------------------------------------------------------------|
+ // Prepare one weak-line state per layer profile node for the exact-route setup grid. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
+ return prepareWeakLineStatesForRows(
+ allocator,
+ lines,
+ temperatures_k,
+ pressures_hpa,
+ temperature_offset_k,
+ cost_timing_active,
+ );
+}
+
+fn prepareProfileWeakLineStates(
+ allocator: Allocator,
+ lines: []const readers.LineAssetRow,
+ profile_rows: []const readers.AtmosphereProfileRow,
+ cost_timing_active: []const ?CostTiming.Active,
+) ![]WeakLinePreparedState {
+ // prepareProfileWeakLineStates ---------------------------------------------------------------------------|
+ // Prepare one weak-line state per vendor spectroscopy-profile row for support-row interpolation. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (profile_rows.len > max_spectroscopy_profile_nodes) return error.InvalidShape;
+
+ var temperatures_k: [max_spectroscopy_profile_nodes]f64 = undefined;
+ var pressures_hpa: [max_spectroscopy_profile_nodes]f64 = undefined;
+ for (profile_rows, 0..) |profile_row, index| {
+ temperatures_k[index] = profile_row.temperature_k;
+ pressures_hpa[index] = profile_row.pressure_hpa;
+ }
+
+ return prepareWeakLineStatesForRows(
+ allocator,
+ lines,
+ temperatures_k[0..profile_rows.len],
+ pressures_hpa[0..profile_rows.len],
+ 0.0,
+ cost_timing_active,
+ );
+}
+
+fn prepareWeakLineStatesForRows(
+ allocator: Allocator,
+ lines: []const readers.LineAssetRow,
+ temperatures_k: []const f64,
+ pressures_hpa: []const f64,
+ temperature_offset_k: f64,
+ cost_timing_active: []const ?CostTiming.Active,
+) ![]WeakLinePreparedState {
+ // prepareWeakLineStatesForRows ---------------------------------------------------------------------------|
+ // Allocate weak-line row storage serially, then fill thermodynamic state rows through raw worker chunks. |
+ // |
+ // allocation |
+ // Each state owns one per-line slice. Allocation stays on the caller thread because the active |
+ // allocator may not be thread-safe; workers only write already-owned row storage. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
+ const states = try allocator.alloc(WeakLinePreparedState, temperatures_k.len);
+ var initialized_count: usize = 0;
+ errdefer {
+ for (states[0..initialized_count]) |*state| state.deinit(allocator);
+ allocator.free(states);
+ }
+
+ for (states) |*state| {
+ state.* = .{
+ .line_count = lines.len,
+ .safe_temperature = 0.0,
+ .safe_pressure = 0.0,
+ .lines = try allocator.alloc(WeakLinePreparedLineState, lines.len),
+ };
+ initialized_count += 1;
+ }
+
+ fillProfileLineStates(
+ lines,
+ &.{},
+ emptyRelaxationMatrix(),
+ temperatures_k,
+ pressures_hpa,
+ temperature_offset_k,
+ states,
+ null,
+ cost_timing_active,
+ );
+ return states;
+}
+
+fn prepareLayerStrongLineStates(
+ allocator: Allocator,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ temperatures_k: []const f64,
+ pressures_hpa: []const f64,
+ cost_timing_active: []const ?CostTiming.Active,
+) ![]StrongLinePreparedState {
+ // prepareLayerStrongLineStates ---------------------------------------------------------------------------|
+ // Prepare one strong-line ConvTP state per layer profile node through the raw worker policy. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
+
+ const states = try allocator.alloc(StrongLinePreparedState, temperatures_k.len);
+ errdefer allocator.free(states);
+ fillProfileLineStates(
+ &.{},
+ strong_lines,
+ relaxation_matrix,
+ temperatures_k,
+ pressures_hpa,
+ 0.0,
+ null,
+ states,
+ cost_timing_active,
+ );
+ return states;
+}
+
+fn prepareProfileStrongLineStates(
+ allocator: Allocator,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ profile_rows: []const readers.AtmosphereProfileRow,
+ cost_timing_active: []const ?CostTiming.Active,
+) ![]StrongLinePreparedState {
+ // prepareProfileStrongLineStates -------------------------------------------------------------------------|
+ // Prepare one strong-line ConvTP state per spectroscopy-profile row through the raw worker policy. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (profile_rows.len > max_spectroscopy_profile_nodes) return error.InvalidShape;
+
+ var temperatures_k: [max_spectroscopy_profile_nodes]f64 = undefined;
+ var pressures_hpa: [max_spectroscopy_profile_nodes]f64 = undefined;
+ for (profile_rows, 0..) |profile_row, index| {
+ temperatures_k[index] = profile_row.temperature_k;
+ pressures_hpa[index] = profile_row.pressure_hpa;
+ }
+
+ return prepareLayerStrongLineStates(
+ allocator,
+ strong_lines,
+ relaxation_matrix,
+ temperatures_k[0..profile_rows.len],
+ pressures_hpa[0..profile_rows.len],
+ cost_timing_active,
+ );
+}
+
+fn fillProfileLineStates(
+ lines: []const readers.LineAssetRow,
+ strong_lines: []const readers.StrongLineAssetRow,
+ relaxation_matrix: readers.RelaxationMatrixAsset,
+ temperatures_k: []const f64,
+ pressures_hpa: []const f64,
+ temperature_offset_k: f64,
+ weak_states: ?[]WeakLinePreparedState,
+ strong_states: ?[]StrongLinePreparedState,
+ cost_timing_active: []const ?CostTiming.Active,
+) void {
+ // fillProfileLineStates ----------------------------------------------------------------------------------|
+ // Fill weak and/or strong line states with the profile-line-state worker policy. |
+ // |
+ // scheduling |
+ // This phase always uses raw spawn mode; the session pool is reserved for forward-miss cache builds. |
+ // --------------------------------------------------------------------------------------------------------|
+ const row_count = temperatures_k.len;
+ if (row_count == 0) return;
+ std.debug.assert(pressures_hpa.len == row_count);
+ if (weak_states) |states| std.debug.assert(states.len == row_count);
+ if (strong_states) |states| std.debug.assert(states.len == row_count);
+
+ const worker_count = preferredProfileLineStateWorkerCount(row_count);
+ var queue = worker_partition.ChunkQueue.init(row_count, profile_line_state_chunk_size);
+ var worker_storage: [worker_partition.max_workers]ProfileLineStateWorker = undefined;
+ for (0..worker_count) |worker_index| {
+ worker_storage[worker_index] = .{
+ .lines = lines,
+ .strong_lines = strong_lines,
+ .relaxation_matrix = relaxation_matrix,
+ .temperatures_k = temperatures_k,
+ .pressures_hpa = pressures_hpa,
+ .temperature_offset_k = temperature_offset_k,
+ .weak_states = weak_states,
+ .strong_states = strong_states,
+ .queue = &queue,
+ .worker_index = worker_index,
+ .cost_timing_active = cost_timing_active,
+ };
+ }
+
+ worker_partition.runWorkers(null, worker_storage[0..worker_count], profileLineStateWorkerMain);
+}
+
+fn profileLineStateWorkerMain(worker: *ProfileLineStateWorker) void {
+ // profileLineStateWorkerMain -----------------------------------------------------------------------------|
+ // Fill weak/strong line states for raw-spawn profile-node chunks. |
+ // --------------------------------------------------------------------------------------------------------|
+ var thread_name_buffer: [64]u8 = undefined;
+ const thread_name = std.fmt.bufPrintZ(
+ &thread_name_buffer,
+ "zdisamar-optics-{d}",
+ .{worker.worker_index},
+ ) catch "zdisamar-optics";
+
+ // instrumentation: trace thread label: profile line-state worker ---------------------------------------- |
+ // captures: profile-line state worker identity |
+ // why: makes parallel thermodynamic-state preparation lanes separable in timeline traces. |
+ Trace.setThreadName(thread_name);
+ // end instrumentation: trace thread label: profile line-state worker ------------------------------------ |
+
+ // instrumentation: trace zone: profile line-state worker ------------------------------------------------ |
+ // captures: profile-line state worker wall time |
+ // why: exposes work distribution across thermodynamic profile nodes. |
+ const worker_zone = Trace.staticZone(@src(), "optical_prepare.profile_line_state_worker");
+ worker_zone.value(@intCast(worker.worker_index));
+ defer worker_zone.end();
+ // end instrumentation: trace zone: profile line-state worker -------------------------------------------- |
+
+ while (worker.queue.next()) |chunk| {
+
+ // instrumentation: trace zone: profile line-state chunk ----------------------------------------------|
+ // captures: profile-line state chunk wall time and row count |
+ // why: preserves the chunk boundary for P/T spectroscopy preparation. |
+ const chunk_zone = Trace.deepStaticZone(@src(), "optical_prepare.profile_line_state_chunk");
+ chunk_zone.value(@intCast(chunk.len()));
+ defer chunk_zone.end();
+ // end instrumentation: trace zone: profile line-state chunk ------------------------------------------|
+
+ fillProfileLineStateRows(worker, chunk.start, chunk.end);
+ }
+}
+
+fn fillProfileLineStateRows(worker: *ProfileLineStateWorker, start: usize, end: usize) void {
+ // fillProfileLineStateRows -------------------------------------------------------------------------------|
+ // Fill one contiguous thermodynamic profile-node range claimed by the raw worker queue. |
+ // --------------------------------------------------------------------------------------------------------|
+ const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
+ for (start..end) |index| {
+ const pressure_atm = @max(worker.pressures_hpa[index] / 1013.25, min_spectroscopy_pressure_atm);
+ if (worker.weak_states) |states| {
+ line_physics.fillWeakLineStateInto(
+ &states[index],
+ worker.lines,
+ @max(worker.temperatures_k[index] + worker.temperature_offset_k, min_hitran_temperature_k),
+ pressure_atm,
+ stage_cost,
+ );
+ }
+ if (worker.strong_states) |states| {
+ states[index] = line_physics.prepareStrongLineState(
+ worker.strong_lines,
+ worker.relaxation_matrix,
+ worker.temperatures_k[index],
+ pressure_atm,
+ stage_cost,
+ );
+ }
+ }
+}
+
+fn emptyRelaxationMatrix() readers.RelaxationMatrixAsset {
+ // emptyRelaxationMatrix ----------------------------------------------------------------------------------|
+ // Provide an unused, well-formed relaxation-matrix view for weak-only profile-line state workers. |
+ // --------------------------------------------------------------------------------------------------------|
+ return .{ .line_count = 0, .wt0 = &.{}, .bw = &.{} };
+}
+
+fn activeLine(
+ line: readers.LineAssetRow,
+ active_isotopes: []const u8,
+ line_strength_threshold: f64,
+) bool {
+ // activeLine ---------------------------------------------------------------------------------------------|
+ // Apply the line-gas isotope and weak-line threshold controls to one parsed HITRAN row. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (line.gas_index != 7) return false;
+
+ if (line.line_strength_cm2_per_molecule < line_strength_threshold) return false;
+
+ if (active_isotopes.len == 0) return true;
+
+ for (active_isotopes) |isotope_number| {
+ if (line.isotope_number == isotope_number) return true;
+ }
+
+ return false;
+}
+
+fn runtimeLine(line: readers.LineAssetRow, active_isotopes: []const u8) bool {
+ // runtimeLine --------------------------------------------------------------------------------------------|
+ // Apply the total-sigma gas/isotope controls without the weak-line threshold filter. |
+ // --------------------------------------------------------------------------------------------------------|
+ if (line.gas_index != 7) return false;
+
+ if (active_isotopes.len == 0) return true;
+
+ for (active_isotopes) |isotope_number| {
+ if (line.isotope_number == isotope_number) return true;
+ }
+
+ return false;
+}
+
+fn isotopeOneActive(active_isotopes: []const u8) bool {
+ // isotopeOneActive ---------------------------------------------------------------------------------------|
+ // LISA strong-line sidecars are isotope-1 O2 data; disable them when that isotope is not requested. |
+ // --------------------------------------------------------------------------------------------------------|
+ for (active_isotopes) |isotope_number| {
+ if (isotope_number == 1) return true;
+ }
+ return false;
+}
diff --git a/src/cache/profile_line_memory.zig b/src/cache/profile_line_memory.zig
index 5b639d51..cac78c06 100644
--- a/src/cache/profile_line_memory.zig
+++ b/src/cache/profile_line_memory.zig
@@ -1,38 +1,19 @@
const std = @import("std");
-const readers = @import("../assets/readers.zig");
const hashing = @import("../common/hashing.zig");
const spline = @import("../common/math/spline.zig");
-const worker_partition = @import("../common/worker_partition.zig");
-const scene_input = @import("../input/scene.zig");
const atmosphere_layers = @import("../setup/atmosphere_layers.zig");
-const line_tables = @import("../setup/line_tables.zig");
-const line_physics = @import("../spectrum/line_physics.zig");
const CostTiming = @import("../instrumentation/cost_timing.zig");
-const Trace = @import("../instrumentation/trace.zig");
const Allocator = std.mem.Allocator;
-const min_spectroscopy_pressure_atm = line_physics.min_spectroscopy_pressure_atm;
const max_spectroscopy_profile_nodes: usize = 64;
-const cutoff_grid_merge_tolerance_nm = 1.0e-9;
-const min_parallel_profile_line_state_count: usize = 4;
-const profile_line_state_chunk_size: usize = 2;
-const min_parallel_profile_cache_build_count: usize = 32;
-const profile_cache_build_chunk_size: usize = 8;
-
-// min_hitran_temperature_k -----------------------------------------------------------------------------------|
-// 150 K before HITRAN strength scaling. This protects the partition ratio, Boltzmann factor, Doppler width, |
-// and finite-difference derivative from nonphysical low-temperature inputs. |
-// ------------------------------------------------------------------------------------------------------------|
-const min_hitran_temperature_k = line_physics.min_hitran_temperature_k;
// profile_line_memory.zig ------------------------------------------------------------------------------------|
-// Retained weak-line cross-section values for exact setup wavelengths and layer profile nodes. |
+// Retained O2 line cross-section rows and support-profile sigma ownership. |
// |
-// setup boundary |
-// builds a preparation-time line-value grid from the parsed HITRAN rows and the computed layer grid. |
-// Rows are indexed as wavelength-major, then layer-node minor, so later optics code can read a contiguous |
-// profile column for each exact wavelength without rebuilding weak-line Voigt terms. |
+// ownership |
+// ProfileLineValues owns wavelength-major diagnostic rows and support-profile total-sigma columns. Build |
+// code lives in profile_line_build.zig; hot-path readers use this module to borrow retained rows only. |
// ------------------------------------------------------------------------------------------------------------|
// ProfileLineValue -------------------------------------------------------------------------------------------|
@@ -180,1221 +161,3 @@ pub const ProfileLineValues = struct {
}
};
// ------------------------------------------------------------------------------------------------------------|
-
-pub fn buildProfileLineValues(
- allocator: Allocator,
- scene: scene_input.Scene,
-) !ProfileLineValues {
- // buildProfileLineValues ---------------------------------------------------------------------------------|
- // Build line values over the scene's evenly spaced setup wavelengths. |
- // --------------------------------------------------------------------------------------------------------|
- const wavelength_count = scene.spectral_grid.sample_count;
- const wavelengths_nm = try allocator.alloc(f64, wavelength_count);
- defer allocator.free(wavelengths_nm);
-
- const step_nm = if (wavelength_count > 1)
- (scene.spectral_grid.end_nm - scene.spectral_grid.start_nm) / @as(f64, @floatFromInt(wavelength_count - 1))
- else
- 0.0;
-
- for (wavelengths_nm, 0..) |*wavelength_nm, wavelength_index| {
- wavelength_nm.* = scene.spectral_grid.start_nm + step_nm * @as(f64, @floatFromInt(wavelength_index));
- }
-
- return buildProfileLineValuesForWavelengths(allocator, scene, wavelengths_nm);
-}
-
-pub fn buildProfileLineValuesForWavelengths(
- allocator: Allocator,
- scene: scene_input.Scene,
- wavelengths_nm: []const f64,
-) !ProfileLineValues {
- // buildProfileLineValuesForWavelengths -------------------------------------------------------------------|
- // Build retained line values with the scalar weak-line cutoff fallback used by canonical setup tests. |
- // --------------------------------------------------------------------------------------------------------|
- return buildProfileLineValuesForWavelengthsWithCutoffGrid(
- allocator,
- scene,
- wavelengths_nm,
- &.{},
- true,
- true,
- null,
- preferredProfileLineWorkerCount(wavelengths_nm.len),
- &.{},
- );
-}
-
-pub fn buildProfileLineValuesForWavelengthsWithCutoffGrid(
- allocator: Allocator,
- scene: scene_input.Scene,
- wavelengths_nm: []const f64,
- cutoff_grid_wavelengths_nm: []const f64,
- build_layer_values: bool,
- include_temperature_derivatives: bool,
- pool: ?*std.Thread.Pool,
- worker_count: usize,
- cost_timing_active: []const ?CostTiming.Active,
-) !ProfileLineValues {
- // buildProfileLineValuesForWavelengthsWithCutoffGrid -----------------------------------------------------|
- // Build retained line values over a caller-provided exact wavelength list. |
- // |
- // the exact radiance wavelengths selected by `spectrum/radiance_wavelengths.zig` instead of assuming |
- // a uniform public product grid. |
- // |
- // row contract |
- // Output rows stay wavelength-major and preserve the input wavelength order exactly. Dense spectrum |
- // prefetch can therefore use its `RadianceWavelengthList` index as the ProfileLineValues index. |
- // `build_layer_values` keeps the diagnostic layer rows for evidence paths. The public spectrum |
- // route uses only support-profile total sigma, so root skips preparing unused layer rows. |
- // --------------------------------------------------------------------------------------------------------|
- var build_arena = std.heap.ArenaAllocator.init(allocator);
- defer build_arena.deinit();
- const scratch_allocator = build_arena.allocator();
-
- const layers = try atmosphere_layers.build(scratch_allocator, scene);
- const lines = try line_tables.build(scratch_allocator, scene);
-
- const wavelength_count = wavelengths_nm.len;
- const profile_node_count = if (build_layer_values) layers.layer_pressures_hpa.len else 0;
- const support_profile_node_count = layers.spectroscopy_profile.rows.len;
- const values = try allocator.alloc(ProfileLineValue, wavelength_count * profile_node_count);
- errdefer allocator.free(values);
- const support_profile_total_sigma = try allocator.alloc(f64, wavelength_count * support_profile_node_count);
- errdefer allocator.free(support_profile_total_sigma);
-
- const cutoff_grid = try prepareCutoffGrid(scratch_allocator, cutoff_grid_wavelengths_nm);
- const runtime = RuntimeControls{
- .cutoff_cm1 = lines.cutoff_sim_cm1,
- .line_mixing_factor = lines.line_mixing_factor,
- .cutoff_grid_wavelengths_nm = cutoff_grid.wavelengths_nm,
- .cutoff_grid_wavenumbers_cm1 = cutoff_grid.wavenumbers_cm1,
- };
- const total_lines = try collectRuntimeLines(scratch_allocator, lines.rows, lines.isotopes_sim);
-
- const strong_sidecars = chooseStrongLineSidecars(lines);
-
- var active_lines: []readers.LineAssetRow = &.{};
- var weak_states: []WeakLinePreparedState = &.{};
- var total_weak_states: []WeakLinePreparedState = &.{};
- var strong_states: []StrongLinePreparedState = &.{};
-
- if (build_layer_values) {
- const line_strength_threshold = thresholdStrength(lines.rows, lines.threshold_line_sim);
- active_lines = try collectActiveLines(
- scratch_allocator,
- lines.rows,
- lines.isotopes_sim,
- line_strength_threshold,
- );
- weak_states = try prepareLayerWeakLineStates(
- scratch_allocator,
- active_lines,
- layers.layer_temperatures_k,
- layers.layer_pressures_hpa,
- 0.0,
- cost_timing_active,
- );
- total_weak_states = try prepareLayerWeakLineStates(
- scratch_allocator,
- total_lines,
- layers.layer_temperatures_k,
- layers.layer_pressures_hpa,
- 0.0,
- cost_timing_active,
- );
- strong_states = try prepareLayerStrongLineStates(
- scratch_allocator,
- strong_sidecars.lines,
- strong_sidecars.relaxation_matrix,
- layers.layer_temperatures_k,
- layers.layer_pressures_hpa,
- cost_timing_active,
- );
- }
-
- // The canonical temperature derivative is a centered finite difference at T +/- 0.5 K. The public
- // Jacobian states are surface/aerosol controls, so root spectrum runs do not read d_sigma/dT and skip
- // these two full weak-line state families. Explicit profile-line canonical builders still request the rows
- // directly, and the reuse stamp records the choice so mismatched session caches cannot mix.
- var upper_weak_states: []WeakLinePreparedState = &.{};
- var lower_weak_states: []WeakLinePreparedState = &.{};
- if (build_layer_values and include_temperature_derivatives) {
- upper_weak_states = try prepareLayerWeakLineStates(
- scratch_allocator,
- active_lines,
- layers.layer_temperatures_k,
- layers.layer_pressures_hpa,
- 0.5,
- cost_timing_active,
- );
- lower_weak_states = try prepareLayerWeakLineStates(
- scratch_allocator,
- active_lines,
- layers.layer_temperatures_k,
- layers.layer_pressures_hpa,
- -0.5,
- cost_timing_active,
- );
- }
- const support_total_weak_states = try prepareProfileWeakLineStates(
- scratch_allocator,
- total_lines,
- layers.spectroscopy_profile.rows,
- cost_timing_active,
- );
- const support_strong_states = try prepareProfileStrongLineStates(
- scratch_allocator,
- strong_sidecars.lines,
- strong_sidecars.relaxation_matrix,
- layers.spectroscopy_profile.rows,
- cost_timing_active,
- );
-
- try buildProfileLineValuesByWavelength(
- pool,
- worker_count,
- wavelengths_nm,
- layers,
- strong_sidecars.lines,
- strong_sidecars.relaxation_matrix,
- runtime,
- build_layer_values and include_temperature_derivatives,
- active_lines,
- total_lines,
- weak_states,
- upper_weak_states,
- lower_weak_states,
- total_weak_states,
- support_total_weak_states,
- strong_states,
- support_strong_states,
- values,
- support_profile_total_sigma,
- cost_timing_active,
- );
-
- return .{
- .values = values,
- .support_profile_total_sigma_cm2_per_molecule = support_profile_total_sigma,
- .wavelength_count = wavelength_count,
- .profile_node_count = profile_node_count,
- .support_profile_node_count = support_profile_node_count,
- .reuse_stamp = profileLineReuseStamp(
- scene.id,
- lines,
- layers.spectroscopy_profile.rows,
- wavelengths_nm,
- build_layer_values,
- include_temperature_derivatives,
- ),
- };
-}
-
-// ProfileLineBuildWorker -------------------------------------------------------------------------------------|
-// Site-local worker row for wavelength-major profile-line value construction. |
-// |
-// layout(64-bit) |
-// This row is not size-pinned because it carries many borrowed slice descriptors and only lives on the build |
-// stack. No field owns heap storage; every output write is to a disjoint wavelength-major row range. |
-// |
-// memory groups |
-// inputs : exact wavelengths, layer/support thermodynamic rows, HITRAN/strong-line tables, runtime |
-// prepared : weak-line and strong-line states prepared before workers start |
-// outputs : wavelength-major layer rows and spectroscopy-profile support rows |
-// scheduling : static range, worker index |
-const ProfileLineBuildWorker = struct {
- wavelengths_nm: []const f64,
- layer_pressures_hpa: []const f64,
- layer_temperatures_k: []const f64,
- layer_interval_indices_1based: []const u32,
- support_profile_rows: []const readers.AtmosphereProfileRow,
- runtime: RuntimeControls,
- include_temperature_derivatives: bool,
- active_lines: []const readers.LineAssetRow,
- total_lines: []const readers.LineAssetRow,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- weak_states: []const WeakLinePreparedState,
- upper_weak_states: []const WeakLinePreparedState,
- lower_weak_states: []const WeakLinePreparedState,
- total_weak_states: []const WeakLinePreparedState,
- support_total_weak_states: []const WeakLinePreparedState,
- strong_states: []const StrongLinePreparedState,
- support_strong_states: []const StrongLinePreparedState,
- values: []ProfileLineValue,
- support_profile_total_sigma_cm2_per_molecule: []f64,
- start_index: usize,
- end_index: usize,
- worker_index: usize = 0,
- cost_timing_active: []const ?CostTiming.Active,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// ProfileLineStateWorker -------------------------------------------------------------------------------------|
-// Site-local raw worker row for profile-node weak/strong line-state preparation. |
-// |
-// layout(64-bit) |
-// This worker row is not size-pinned because it carries borrowed slices plus a relaxation-matrix view. |
-// Workers perform no allocation; the caller allocates output row storage before launch and deinitializes it |
-// after the later spectroscopy-value phase. |
-// |
-// memory groups |
-// inputs : thermodynamic node rows and parsed weak/strong-line tables |
-// outputs : optional weak-line and strong-line prepared states, indexed by profile node |
-// scheduling : shared chunk queue, worker index |
-const ProfileLineStateWorker = struct {
- lines: []const readers.LineAssetRow,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- temperatures_k: []const f64,
- pressures_hpa: []const f64,
- temperature_offset_k: f64 = 0.0,
- weak_states: ?[]WeakLinePreparedState = null,
- strong_states: ?[]StrongLinePreparedState = null,
- queue: *worker_partition.ChunkQueue,
- worker_index: usize,
- cost_timing_active: []const ?CostTiming.Active,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-fn costTimingForWorker(
- cost_timing_active: []const ?CostTiming.Active,
- worker_index: usize,
-) ?CostTiming.Active {
- // costTimingForWorker ------------------------------------------------------------------------------------|
- // Borrow the caller-supplied per-worker cost row when this worker group maps to one. |
- // --------------------------------------------------------------------------------------------------------|
- if (worker_index >= cost_timing_active.len) return null;
- return cost_timing_active[worker_index];
-}
-
-fn buildProfileLineValuesByWavelength(
- pool: ?*std.Thread.Pool,
- worker_count: usize,
- wavelengths_nm: []const f64,
- layers: atmosphere_layers.LayerGrid,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- runtime: RuntimeControls,
- include_temperature_derivatives: bool,
- active_lines: []const readers.LineAssetRow,
- total_lines: []const readers.LineAssetRow,
- weak_states: []const WeakLinePreparedState,
- upper_weak_states: []const WeakLinePreparedState,
- lower_weak_states: []const WeakLinePreparedState,
- total_weak_states: []const WeakLinePreparedState,
- support_total_weak_states: []const WeakLinePreparedState,
- strong_states: []const StrongLinePreparedState,
- support_strong_states: []const StrongLinePreparedState,
- values: []ProfileLineValue,
- support_profile_total_sigma_cm2_per_molecule: []f64,
- cost_timing_active: []const ?CostTiming.Active,
-) !void {
- // buildProfileLineValuesByWavelength ---------------------------------------------------------------------|
- // Fill retained profile-line values over exact wavelengths, partitioned exactly like the profile |
- // spectroscopy cache build: threshold 32, static worker ranges, chunk size 8, and optional session pool. |
- // |
- // instrumentation |
- // `profile_spectroscopy_cache.build` measures this whole setup phase. Worker and chunk zones live in |
- // `profileLineBuildWorkerMain` and retain the trace names for profile-cache construction. |
- // --------------------------------------------------------------------------------------------------------|
- if (worker_count == 0 or worker_count > worker_partition.max_workers) return error.InvalidShape;
- if (wavelengths_nm.len == 0) return;
-
- const profile_node_count = if (values.len != 0) layers.layer_pressures_hpa.len else 0;
- const support_profile_node_count = layers.spectroscopy_profile.rows.len;
- if (values.len != wavelengths_nm.len * profile_node_count) return error.InvalidShape;
- if (support_profile_total_sigma_cm2_per_molecule.len != wavelengths_nm.len * support_profile_node_count) {
- return error.InvalidShape;
- }
-
- // instrumentation: trace zone: profile spectroscopy cache build ----------------------------------------- |
- // captures: profile-line cache build wall time and exact-wavelength count |
- // why: shows setup cost that compares before forward prefetch starts. |
- const zone = Trace.staticZone(@src(), "profile_spectroscopy_cache.build");
- zone.value(@intCast(wavelengths_nm.len));
- defer zone.end();
- // end instrumentation: trace zone: profile spectroscopy cache build ------------------------------------- |
-
- var worker_storage: [worker_partition.max_workers]ProfileLineBuildWorker = undefined;
- for (0..worker_count) |worker_index| {
- const range = worker_partition.staticRange(wavelengths_nm.len, worker_count, worker_index);
- worker_storage[worker_index] = .{
- .wavelengths_nm = wavelengths_nm,
- .layer_pressures_hpa = layers.layer_pressures_hpa,
- .layer_temperatures_k = layers.layer_temperatures_k,
- .layer_interval_indices_1based = layers.layer_interval_indices_1based,
- .support_profile_rows = layers.spectroscopy_profile.rows,
- .runtime = runtime,
- .include_temperature_derivatives = include_temperature_derivatives,
- .active_lines = active_lines,
- .total_lines = total_lines,
- .strong_lines = strong_lines,
- .relaxation_matrix = relaxation_matrix,
- .weak_states = weak_states,
- .upper_weak_states = upper_weak_states,
- .lower_weak_states = lower_weak_states,
- .total_weak_states = total_weak_states,
- .support_total_weak_states = support_total_weak_states,
- .strong_states = strong_states,
- .support_strong_states = support_strong_states,
- .values = values,
- .support_profile_total_sigma_cm2_per_molecule = support_profile_total_sigma_cm2_per_molecule,
- .start_index = range.start,
- .end_index = range.end,
- .worker_index = worker_index,
- .cost_timing_active = cost_timing_active,
- };
- }
-
- worker_partition.runWorkers(pool, worker_storage[0..worker_count], profileLineBuildWorkerMain);
-}
-
-fn profileLineBuildWorkerMain(worker: *ProfileLineBuildWorker) void {
- // profileLineBuildWorkerMain -----------------------------------------------------------------------------|
- // Fill one worker-owned range of wavelength-major profile-line rows. |
- // --------------------------------------------------------------------------------------------------------|
- var thread_name_buffer: [64]u8 = undefined;
- const thread_name = std.fmt.bufPrintZ(
- &thread_name_buffer,
- "zdisamar-profile-cache-{d}",
- .{worker.worker_index},
- ) catch "zdisamar-profile-cache";
-
- // instrumentation: trace thread label: profile spectroscopy cache worker -------------------------------- |
- // captures: profile-line build worker identity |
- // why: makes parallel profile-cache lanes separable in timeline traces. |
- Trace.setThreadName(thread_name);
- // end instrumentation: trace thread label: profile spectroscopy cache worker ---------------------------- |
-
- // instrumentation: trace zone: profile spectroscopy cache worker ---------------------------------------- |
- // captures: worker wall time for retained profile-line value construction |
- // why: exposes load balance across exact-wavelength profile-cache ranges. |
- const worker_zone = Trace.staticZone(@src(), "profile_spectroscopy_cache.worker");
- worker_zone.value(@intCast(worker.worker_index));
- defer worker_zone.end();
- // end instrumentation: trace zone: profile spectroscopy cache worker ------------------------------------ |
-
- var chunk_start = worker.start_index;
- while (worker_partition.nextStaticChunk(
- &chunk_start,
- worker.end_index,
- profile_cache_build_chunk_size,
- )) |chunk| {
-
- // instrumentation: trace zone: profile spectroscopy cache build chunk --------------------------------|
- // captures: chunk wall time and exact-wavelength row count |
- // why: preserves the chunk boundary while showing uneven wavelength-window costs. |
- const chunk_zone = Trace.deepStaticZone(@src(), "profile_spectroscopy_cache.build");
- chunk_zone.value(@intCast(chunk.len()));
- defer chunk_zone.end();
- // end instrumentation: trace zone: profile spectroscopy cache build chunk ----------------------------|
-
- for (chunk.start..chunk.end) |wavelength_index| {
- if (worker.values.len != 0) fillProfileLineValueRowsAtWavelength(worker, wavelength_index);
- fillSupportProfileLineValueRowsAtWavelength(worker, wavelength_index);
- }
- }
-}
-
-fn fillProfileLineValueRowsAtWavelength(worker: *ProfileLineBuildWorker, wavelength_index: usize) void {
- // fillProfileLineValueRowsAtWavelength -------------------------------------------------------------------|
- // Fill all layer-node line-value rows for one exact wavelength. |
- // --------------------------------------------------------------------------------------------------------|
- const wavelength_nm = worker.wavelengths_nm[wavelength_index];
- const active_window = line_physics.relevantLineWindow(
- worker.active_lines,
- wavelength_nm,
- worker.runtime.cutoff_cm1,
- );
- const total_window = line_physics.relevantLineWindow(worker.total_lines, wavelength_nm, worker.runtime.cutoff_cm1);
- const wavelength_state = line_physics.prepareWeakLineWavelengthState(
- wavelength_nm,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- );
- const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
- for (worker.layer_pressures_hpa, worker.layer_temperatures_k, 0..) |pressure_hpa, temperature_k, node_index| {
- const sigma = line_physics.weakLineSigmaAtPrepared(
- worker.active_lines,
- active_window,
- &worker.weak_states[node_index],
- wavelength_state,
- worker.runtime.cutoff_cm1,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- );
-
- var d_sigma_d_temperature: f64 = 0.0;
- if (worker.include_temperature_derivatives) {
- const upper_sigma = line_physics.weakLineSigmaAtPreparedFiniteDifference(
- worker.active_lines,
- active_window,
- &worker.upper_weak_states[node_index],
- wavelength_state,
- worker.runtime.cutoff_cm1,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- );
- const lower_sigma = line_physics.weakLineSigmaAtPreparedFiniteDifference(
- worker.active_lines,
- active_window,
- &worker.lower_weak_states[node_index],
- wavelength_state,
- worker.runtime.cutoff_cm1,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- );
- d_sigma_d_temperature = (upper_sigma - lower_sigma) / 1.0;
- }
-
- const total = line_physics.totalSpectroscopyAt(
- worker.total_lines,
- total_window,
- worker.strong_lines,
- worker.relaxation_matrix,
- wavelength_nm,
- wavelength_state,
- temperature_k,
- pressure_hpa,
- worker.runtime.cutoff_cm1,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- worker.runtime.line_mixing_factor,
- &worker.strong_states[node_index],
- &worker.total_weak_states[node_index],
- stage_cost,
- );
- const row_index = wavelength_index * worker.layer_pressures_hpa.len + node_index;
- worker.values[row_index] = .{
- .wavelength_nm = wavelength_nm,
- .layer_index = @intCast(node_index),
- .interval_index_1based = worker.layer_interval_indices_1based[node_index],
- .pressure_hpa = pressure_hpa,
- .temperature_k = temperature_k,
- .weak_line_sigma_cm2_per_molecule = sigma,
- .strong_line_sigma_cm2_per_molecule = total.strong_line_sigma_cm2_per_molecule,
- .line_sigma_cm2_per_molecule = total.line_sigma_cm2_per_molecule,
- .line_mixing_sigma_cm2_per_molecule = total.line_mixing_sigma_cm2_per_molecule,
- .total_sigma_cm2_per_molecule = total.total_sigma_cm2_per_molecule,
- .d_sigma_d_temperature_cm2_per_molecule_per_k = d_sigma_d_temperature,
- };
- }
-}
-
-fn fillSupportProfileLineValueRowsAtWavelength(worker: *ProfileLineBuildWorker, wavelength_index: usize) void {
- // fillSupportProfileLineValueRowsAtWavelength ------------------------------------------------------------|
- // Fill all spectroscopy-profile support rows for one exact wavelength. |
- // --------------------------------------------------------------------------------------------------------|
- const wavelength_nm = worker.wavelengths_nm[wavelength_index];
- const total_window = line_physics.relevantLineWindow(worker.total_lines, wavelength_nm, worker.runtime.cutoff_cm1);
- const wavelength_state = line_physics.prepareWeakLineWavelengthState(
- wavelength_nm,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- );
- const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
- for (worker.support_profile_rows, 0..) |profile_row, node_index| {
- const total = line_physics.totalSpectroscopyAt(
- worker.total_lines,
- total_window,
- worker.strong_lines,
- worker.relaxation_matrix,
- wavelength_nm,
- wavelength_state,
- profile_row.temperature_k,
- profile_row.pressure_hpa,
- worker.runtime.cutoff_cm1,
- worker.runtime.cutoff_grid_wavelengths_nm,
- worker.runtime.cutoff_grid_wavenumbers_cm1,
- worker.runtime.line_mixing_factor,
- &worker.support_strong_states[node_index],
- &worker.support_total_weak_states[node_index],
- stage_cost,
- );
- const row_index = wavelength_index * worker.support_profile_rows.len + node_index;
- worker.support_profile_total_sigma_cm2_per_molecule[row_index] = total.total_sigma_cm2_per_molecule;
- }
-}
-
-fn preferredProfileLineWorkerCount(wavelength_count: usize) usize {
- // preferredProfileLineWorkerCount ------------------------------------------------------------------------|
- // Resolve the profile-cache build worker-count policy for retained exact-wavelength spectroscopy. |
- // --------------------------------------------------------------------------------------------------------|
- return worker_partition.preferredWorkerCount(
- wavelength_count,
- min_parallel_profile_cache_build_count,
- );
-}
-
-fn preferredProfileLineStateWorkerCount(profile_count: usize) usize {
- // preferredProfileLineStateWorkerCount -------------------------------------------------------------------|
- // Resolve the thermodynamic profile-line state worker-count policy. |
- // --------------------------------------------------------------------------------------------------------|
- return worker_partition.preferredWorkerCount(
- profile_count,
- min_parallel_profile_line_state_count,
- );
-}
-
-pub fn profileLineReuseStamp(
- scene_id: []const u8,
- lines: line_tables.LineTable,
- spectroscopy_profile_rows: []const readers.AtmosphereProfileRow,
- wavelengths_nm: []const f64,
- build_layer_values: bool,
- include_temperature_derivatives: bool,
-) hashing.ReuseStamp {
- // profileLineReuseStamp ----------------------------------------------------------------------------------|
- // Hash only session-fixed inputs: scene identity, exact wavelengths, the parsed line assets, and the |
- // spectroscopy-profile thermodynamics. Do not hash LayerGrid support rows: profile-line values sit at |
- // fixed spectroscopy nodes and are resampled per pressure state downstream, so hashing support placement |
- // would defeat reuse across the pressure changes optimal estimation makes each iteration. |
- // --------------------------------------------------------------------------------------------------------|
- var hasher = std.hash.Wyhash.init(0);
- hasher.update(scene_id);
- hashF64Slice(&hasher, wavelengths_nm);
- hashLineInputs(&hasher, lines);
- hashAtmosphereRows(&hasher, spectroscopy_profile_rows);
- hashing.updateBool(&hasher, build_layer_values);
- hashing.updateBool(&hasher, include_temperature_derivatives);
- return .{ .value = hasher.final() };
-}
-
-fn hashLineInputs(hasher: *std.hash.Wyhash, lines: line_tables.LineTable) void {
- // hashLineInputs -----------------------------------------------------------------------------------------|
- // Hash parsed line assets and runtime line-filter controls without reading struct padding bytes. |
- // --------------------------------------------------------------------------------------------------------|
- hashLineRows(hasher, lines.rows);
- hashStrongLineRows(hasher, lines.strong_lines);
- hashRelaxationMatrix(hasher, lines.relaxation_matrix);
- hashing.updateValue(hasher, lines.isotopes_sim.len);
- hasher.update(lines.isotopes_sim);
- hashing.updateValue(hasher, lines.threshold_line_sim);
- hashing.updateValue(hasher, lines.cutoff_sim_cm1);
- hashing.updateValue(hasher, lines.line_mixing_factor);
-}
-
-fn hashLineRows(hasher: *std.hash.Wyhash, rows: []const readers.LineAssetRow) void {
- // hashLineRows -------------------------------------------------------------------------------------------|
- // Hash one HITRAN row family field-by-field so optional metadata participates in cache identity. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateValue(hasher, rows.len);
- for (rows) |row| {
- hashing.updateValue(hasher, row.gas_index);
- hashing.updateValue(hasher, row.isotope_number);
- hashing.updateBool(hasher, row.vendor_filter_metadata_from_source);
- for ([_]f64{
- row.center_wavelength_nm,
- row.center_wavenumber_cm1,
- row.line_strength_cm2_per_molecule,
- row.air_half_width_cm1,
- row.lower_state_energy_cm1,
- row.temperature_exponent,
- row.pressure_shift_cm1,
- }) |value| hashing.updateValue(hasher, value);
- hashOptionalU8(hasher, row.branch_ic1);
- hashOptionalU8(hasher, row.branch_ic2);
- hashOptionalU8(hasher, row.rotational_nf);
- }
-}
-
-fn hashStrongLineRows(hasher: *std.hash.Wyhash, rows: []const readers.StrongLineAssetRow) void {
- // hashStrongLineRows -------------------------------------------------------------------------------------|
- // Hash LISA SDF sidecar rows that can contribute to strong-line and line-mixing sigma. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateValue(hasher, rows.len);
- for (rows) |row| {
- for ([_]f64{
- row.center_wavenumber_cm1,
- row.center_wavelength_nm,
- row.population_t0,
- row.dipole_ratio,
- row.dipole_t0,
- row.lower_state_energy_cm1,
- row.air_half_width_cm1,
- row.air_half_width_nm,
- row.temperature_exponent,
- row.pressure_shift_cm1,
- row.pressure_shift_nm,
- }) |value| hashing.updateValue(hasher, value);
- hashing.updateValue(hasher, row.rotational_index_m1);
- }
-}
-
-fn hashRelaxationMatrix(hasher: *std.hash.Wyhash, matrix: readers.RelaxationMatrixAsset) void {
- // hashRelaxationMatrix -----------------------------------------------------------------------------------|
- // Hash the dense LISA RMF matrices and their square dimension. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateValue(hasher, matrix.line_count);
- hashF64Slice(hasher, matrix.wt0);
- hashF64Slice(hasher, matrix.bw);
-}
-
-fn hashAtmosphereRows(hasher: *std.hash.Wyhash, rows: []const readers.AtmosphereProfileRow) void {
- // hashAtmosphereRows -------------------------------------------------------------------------------------|
- // Hash spectroscopy-profile thermodynamics used by support-profile sigma rows. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateValue(hasher, rows.len);
- for (rows) |row| {
- for ([_]f64{
- row.altitude_km,
- row.pressure_hpa,
- row.temperature_k,
- row.air_number_density_cm3,
- }) |value| hashing.updateValue(hasher, value);
- }
-}
-
-fn hashF64Slice(hasher: *std.hash.Wyhash, values: []const f64) void {
- // hashF64Slice -------------------------------------------------------------------------------------------|
- // Hash an f64 slice with its length so empty and repeated-prefix rows stay distinct. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateValue(hasher, values.len);
- hasher.update(std.mem.sliceAsBytes(values));
-}
-
-fn hashOptionalU8(hasher: *std.hash.Wyhash, value: ?u8) void {
- // hashOptionalU8 -----------------------------------------------------------------------------------------|
- // Hash optional compact metadata with explicit presence. |
- // --------------------------------------------------------------------------------------------------------|
- hashing.updateBool(hasher, value != null);
- if (value) |resolved| hashing.updateValue(hasher, resolved);
-}
-
-fn prepareCutoffGrid(allocator: Allocator, support_wavelengths_nm: []const f64) !CutoffGrid {
- // prepareCutoffGrid --------------------------------------------------------------------------------------|
- // Sort and merge the realized weak-line support grid before computing matching wavenumbers. |
- // |
- // --------------------------------------------------------------------------------------------------------|
- if (support_wavelengths_nm.len < 2) return .{};
-
- const wavelengths = try allocator.dupe(f64, support_wavelengths_nm);
- errdefer allocator.free(wavelengths);
- std.mem.sort(f64, wavelengths, {}, lessThanF64);
-
- var merged_count: usize = 0;
- for (wavelengths) |wavelength_nm| {
- if (merged_count != 0 and
- @abs(wavelengths[merged_count - 1] - wavelength_nm) <= cutoff_grid_merge_tolerance_nm)
- {
- continue;
- }
- wavelengths[merged_count] = wavelength_nm;
- merged_count += 1;
- }
-
- const merged_wavelengths = try allocator.realloc(wavelengths, merged_count);
- errdefer allocator.free(merged_wavelengths);
- const wavenumbers = try allocator.alloc(f64, merged_count);
- errdefer allocator.free(wavenumbers);
- for (merged_wavelengths, wavenumbers) |wavelength_nm, *wavenumber_cm1| {
- wavenumber_cm1.* = line_physics.wavelengthToWavenumberCm1(wavelength_nm);
- }
- return .{ .wavelengths_nm = merged_wavelengths, .wavenumbers_cm1 = wavenumbers };
-}
-
-fn lessThanF64(_: void, lhs: f64, rhs: f64) bool {
- // lessThanF64 --------------------------------------------------------------------------------------------|
- // Sort finite support wavelengths in ascending wavelength order for cutoff-grid index searches. |
- // --------------------------------------------------------------------------------------------------------|
- return lhs < rhs;
-}
-
-fn chooseStrongLineSidecars(lines: line_tables.LineTable) StrongLineSidecars {
- // chooseStrongLineSidecars -------------------------------------------------------------------------------|
- // Use LISA strong-line sidecars only for isotope-1 O2 rows. |
- // --------------------------------------------------------------------------------------------------------|
- if (isotopeOneActive(lines.isotopes_sim)) {
- return .{
- .lines = lines.strong_lines,
- .relaxation_matrix = lines.relaxation_matrix,
- };
- }
-
- return .{
- .lines = &.{},
- .relaxation_matrix = emptyRelaxationMatrix(),
- };
-}
-
-// RuntimeControls --------------------------------------------------------------------------------------------|
-// Borrowed line-list controls needed by weak-line setup evaluation. |
-// |
-// layout(64-bit) |
-// size: 48 B (0.047 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] cutoff_cm1 : f64 |
-// [ 8..15] line_mixing_factor : f64 |
-// [16..31] cutoff_grid_wavelengths_nm : []const f64 |
-// [32..47] cutoff_grid_wavenumbers_cm1: []const f64 |
-const RuntimeControls = struct {
- cutoff_cm1: f64,
- line_mixing_factor: f64,
- cutoff_grid_wavelengths_nm: []const f64 = &.{},
- cutoff_grid_wavenumbers_cm1: []const f64 = &.{},
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-const StrongLineSidecars = struct {
- lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// CutoffGrid -------------------------------------------------------------------------------------------------|
-// Owned weak-line cutoff support grid used during setup-time line evaluation. |
-// |
-// layout(64-bit) |
-// size: 32 B (0.031 KiB), align: 8 B |
-// |
-// memory |
-// [ 0..15] wavelengths_nm : []f64 |
-// [16..31] wavenumbers_cm1: []f64 |
-const CutoffGrid = struct {
- wavelengths_nm: []f64 = &.{},
- wavenumbers_cm1: []f64 = &.{},
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// StrongLinePreparedState ------------------------------------------------------------------------------------|
-// Fixed-capacity O2 strong-line ConvTP state prepared for one profile-node thermodynamic point. |
-// |
-// layout(64-bit) |
-// size: 5136 B (5.016 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] line_count : usize |
-// [ 8.. 15] sig_moy_cm1 : f64 |
-// [ 16..1039] population_t : [128]f64 |
-// [1040..2063] dipole_t : [128]f64 |
-// [2064..3087] mod_sig_cm1 : [128]f64 |
-// [3088..4111] half_width_cm1_at_t : [128]f64 |
-// [4112..5135] line_mixing_coefficients : [128]f64 |
-const StrongLinePreparedState = line_physics.StrongLinePreparedState;
-// ------------------------------------------------------------------------------------------------------------|
-
-// WeakLinePreparedLineState ----------------------------------------------------------------------------------|
-// Per-line weak-lane constants prepared for one temperature and pressure. |
-// |
-// |
-// layout(64-bit) |
-// size: 32 B (0.031 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] shifted_center_wavenumber_cm1 : f64 |
-// [ 8..15] cte : f64 |
-// [16..23] line_shape_y : f64 |
-// [24..31] prefactor_base : f64 |
-const WeakLinePreparedLineState = line_physics.WeakLinePreparedLineState;
-// ------------------------------------------------------------------------------------------------------------|
-
-// WeakLinePreparedState --------------------------------------------------------------------------------------|
-// Header over weak-line constants prepared for one temperature and pressure. |
-// |
-// |
-// layout(64-bit) |
-// size: 40 B (0.039 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] line_count : usize |
-// [ 8..15] safe_temperature : f64 |
-// [16..23] safe_pressure : f64 |
-// [24..39] lines : []WeakLinePreparedLineState |
-const WeakLinePreparedState = line_physics.WeakLinePreparedState;
-// ------------------------------------------------------------------------------------------------------------|
-
-fn thresholdStrength(lines: []const readers.LineAssetRow, scale: f64) f64 {
- // thresholdStrength --------------------------------------------------------------------------------------|
- // Convert the relative weak-line threshold control into an absolute line-strength floor. |
- // --------------------------------------------------------------------------------------------------------|
- var max_strength: f64 = 0.0;
- for (lines) |line| {
- max_strength = @max(max_strength, line.line_strength_cm2_per_molecule);
- }
- return max_strength * scale;
-}
-
-fn collectActiveLines(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- active_isotopes: []const u8,
- line_strength_threshold: f64,
-) ![]readers.LineAssetRow {
- // collectActiveLines -------------------------------------------------------------------------------------|
- // Copy weak-line rows that participate in setup evaluation, sorted for local window scans. |
- // --------------------------------------------------------------------------------------------------------|
- var active_count: usize = 0;
- for (lines) |line| {
- if (activeLine(line, active_isotopes, line_strength_threshold)) active_count += 1;
- }
-
- const active_lines = try allocator.alloc(readers.LineAssetRow, active_count);
- errdefer allocator.free(active_lines);
-
- var active_index: usize = 0;
- for (lines) |line| {
- if (!activeLine(line, active_isotopes, line_strength_threshold)) continue;
- active_lines[active_index] = line;
- active_index += 1;
- }
-
- std.mem.sort(readers.LineAssetRow, active_lines, {}, lessByCenterWavelength);
- return active_lines;
-}
-
-fn collectRuntimeLines(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- active_isotopes: []const u8,
-) ![]readers.LineAssetRow {
- // collectRuntimeLines ------------------------------------------------------------------------------------|
- // Copy O2 rows that participate in total-sigma evaluation, without applying the weak-line threshold. |
- // --------------------------------------------------------------------------------------------------------|
- var active_count: usize = 0;
- for (lines) |line| {
- if (runtimeLine(line, active_isotopes)) active_count += 1;
- }
-
- const active_lines = try allocator.alloc(readers.LineAssetRow, active_count);
- errdefer allocator.free(active_lines);
-
- var active_index: usize = 0;
- for (lines) |line| {
- if (!runtimeLine(line, active_isotopes)) continue;
- active_lines[active_index] = line;
- active_index += 1;
- }
-
- std.mem.sort(readers.LineAssetRow, active_lines, {}, lessByCenterWavelength);
- return active_lines;
-}
-
-fn lessByCenterWavelength(_: void, lhs: readers.LineAssetRow, rhs: readers.LineAssetRow) bool {
- // lessByCenterWavelength ---------------------------------------------------------------------------------|
- // Order line rows by center wavelength so binary searches can isolate a local HITRAN cutoff window. |
- // --------------------------------------------------------------------------------------------------------|
- return lhs.center_wavelength_nm < rhs.center_wavelength_nm;
-}
-
-fn prepareLayerWeakLineStates(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- temperatures_k: []const f64,
- pressures_hpa: []const f64,
- temperature_offset_k: f64,
- cost_timing_active: []const ?CostTiming.Active,
-) ![]WeakLinePreparedState {
- // prepareLayerWeakLineStates -----------------------------------------------------------------------------|
- // Prepare one weak-line state per layer profile node for the exact-route setup grid. |
- // --------------------------------------------------------------------------------------------------------|
- if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
- return prepareWeakLineStatesForRows(
- allocator,
- lines,
- temperatures_k,
- pressures_hpa,
- temperature_offset_k,
- cost_timing_active,
- );
-}
-
-fn prepareProfileWeakLineStates(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- profile_rows: []const readers.AtmosphereProfileRow,
- cost_timing_active: []const ?CostTiming.Active,
-) ![]WeakLinePreparedState {
- // prepareProfileWeakLineStates ---------------------------------------------------------------------------|
- // Prepare one weak-line state per vendor spectroscopy-profile row for support-row interpolation. |
- // --------------------------------------------------------------------------------------------------------|
- if (profile_rows.len > max_spectroscopy_profile_nodes) return error.InvalidShape;
-
- var temperatures_k: [max_spectroscopy_profile_nodes]f64 = undefined;
- var pressures_hpa: [max_spectroscopy_profile_nodes]f64 = undefined;
- for (profile_rows, 0..) |profile_row, index| {
- temperatures_k[index] = profile_row.temperature_k;
- pressures_hpa[index] = profile_row.pressure_hpa;
- }
-
- return prepareWeakLineStatesForRows(
- allocator,
- lines,
- temperatures_k[0..profile_rows.len],
- pressures_hpa[0..profile_rows.len],
- 0.0,
- cost_timing_active,
- );
-}
-
-fn prepareWeakLineStatesForRows(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- temperatures_k: []const f64,
- pressures_hpa: []const f64,
- temperature_offset_k: f64,
- cost_timing_active: []const ?CostTiming.Active,
-) ![]WeakLinePreparedState {
- // prepareWeakLineStatesForRows ---------------------------------------------------------------------------|
- // Allocate weak-line row storage serially, then fill thermodynamic state rows through raw worker chunks. |
- // |
- // allocation |
- // Each state owns one per-line slice. Allocation stays on the caller thread because the active |
- // allocator may not be thread-safe; workers only write already-owned row storage. |
- // --------------------------------------------------------------------------------------------------------|
- if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
- const states = try allocator.alloc(WeakLinePreparedState, temperatures_k.len);
- var initialized_count: usize = 0;
- errdefer {
- for (states[0..initialized_count]) |*state| state.deinit(allocator);
- allocator.free(states);
- }
-
- for (states) |*state| {
- state.* = .{
- .line_count = lines.len,
- .safe_temperature = 0.0,
- .safe_pressure = 0.0,
- .lines = try allocator.alloc(WeakLinePreparedLineState, lines.len),
- };
- initialized_count += 1;
- }
-
- fillProfileLineStates(
- lines,
- &.{},
- emptyRelaxationMatrix(),
- temperatures_k,
- pressures_hpa,
- temperature_offset_k,
- states,
- null,
- cost_timing_active,
- );
- return states;
-}
-
-fn prepareLayerStrongLineStates(
- allocator: Allocator,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- temperatures_k: []const f64,
- pressures_hpa: []const f64,
- cost_timing_active: []const ?CostTiming.Active,
-) ![]StrongLinePreparedState {
- // prepareLayerStrongLineStates ---------------------------------------------------------------------------|
- // Prepare one strong-line ConvTP state per layer profile node through the raw worker policy. |
- // --------------------------------------------------------------------------------------------------------|
- if (temperatures_k.len != pressures_hpa.len) return error.InvalidShape;
-
- const states = try allocator.alloc(StrongLinePreparedState, temperatures_k.len);
- errdefer allocator.free(states);
- fillProfileLineStates(
- &.{},
- strong_lines,
- relaxation_matrix,
- temperatures_k,
- pressures_hpa,
- 0.0,
- null,
- states,
- cost_timing_active,
- );
- return states;
-}
-
-fn prepareProfileStrongLineStates(
- allocator: Allocator,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- profile_rows: []const readers.AtmosphereProfileRow,
- cost_timing_active: []const ?CostTiming.Active,
-) ![]StrongLinePreparedState {
- // prepareProfileStrongLineStates -------------------------------------------------------------------------|
- // Prepare one strong-line ConvTP state per spectroscopy-profile row through the raw worker policy. |
- // --------------------------------------------------------------------------------------------------------|
- if (profile_rows.len > max_spectroscopy_profile_nodes) return error.InvalidShape;
-
- var temperatures_k: [max_spectroscopy_profile_nodes]f64 = undefined;
- var pressures_hpa: [max_spectroscopy_profile_nodes]f64 = undefined;
- for (profile_rows, 0..) |profile_row, index| {
- temperatures_k[index] = profile_row.temperature_k;
- pressures_hpa[index] = profile_row.pressure_hpa;
- }
-
- return prepareLayerStrongLineStates(
- allocator,
- strong_lines,
- relaxation_matrix,
- temperatures_k[0..profile_rows.len],
- pressures_hpa[0..profile_rows.len],
- cost_timing_active,
- );
-}
-
-fn fillProfileLineStates(
- lines: []const readers.LineAssetRow,
- strong_lines: []const readers.StrongLineAssetRow,
- relaxation_matrix: readers.RelaxationMatrixAsset,
- temperatures_k: []const f64,
- pressures_hpa: []const f64,
- temperature_offset_k: f64,
- weak_states: ?[]WeakLinePreparedState,
- strong_states: ?[]StrongLinePreparedState,
- cost_timing_active: []const ?CostTiming.Active,
-) void {
- // fillProfileLineStates ----------------------------------------------------------------------------------|
- // Fill weak and/or strong line states with the profile-line-state worker policy. |
- // |
- // scheduling |
- // This phase always uses raw spawn mode; the session pool is reserved for forward-miss cache builds. |
- // --------------------------------------------------------------------------------------------------------|
- const row_count = temperatures_k.len;
- if (row_count == 0) return;
- std.debug.assert(pressures_hpa.len == row_count);
- if (weak_states) |states| std.debug.assert(states.len == row_count);
- if (strong_states) |states| std.debug.assert(states.len == row_count);
-
- const worker_count = preferredProfileLineStateWorkerCount(row_count);
- var queue = worker_partition.ChunkQueue.init(row_count, profile_line_state_chunk_size);
- var worker_storage: [worker_partition.max_workers]ProfileLineStateWorker = undefined;
- for (0..worker_count) |worker_index| {
- worker_storage[worker_index] = .{
- .lines = lines,
- .strong_lines = strong_lines,
- .relaxation_matrix = relaxation_matrix,
- .temperatures_k = temperatures_k,
- .pressures_hpa = pressures_hpa,
- .temperature_offset_k = temperature_offset_k,
- .weak_states = weak_states,
- .strong_states = strong_states,
- .queue = &queue,
- .worker_index = worker_index,
- .cost_timing_active = cost_timing_active,
- };
- }
-
- worker_partition.runWorkers(null, worker_storage[0..worker_count], profileLineStateWorkerMain);
-}
-
-fn profileLineStateWorkerMain(worker: *ProfileLineStateWorker) void {
- // profileLineStateWorkerMain -----------------------------------------------------------------------------|
- // Fill weak/strong line states for raw-spawn profile-node chunks. |
- // --------------------------------------------------------------------------------------------------------|
- var thread_name_buffer: [64]u8 = undefined;
- const thread_name = std.fmt.bufPrintZ(
- &thread_name_buffer,
- "zdisamar-optics-{d}",
- .{worker.worker_index},
- ) catch "zdisamar-optics";
-
- // instrumentation: trace thread label: profile line-state worker ---------------------------------------- |
- // captures: profile-line state worker identity |
- // why: makes parallel thermodynamic-state preparation lanes separable in timeline traces. |
- Trace.setThreadName(thread_name);
- // end instrumentation: trace thread label: profile line-state worker ------------------------------------ |
-
- // instrumentation: trace zone: profile line-state worker ------------------------------------------------ |
- // captures: profile-line state worker wall time |
- // why: exposes work distribution across thermodynamic profile nodes. |
- const worker_zone = Trace.staticZone(@src(), "optical_prepare.profile_line_state_worker");
- worker_zone.value(@intCast(worker.worker_index));
- defer worker_zone.end();
- // end instrumentation: trace zone: profile line-state worker -------------------------------------------- |
-
- while (worker.queue.next()) |chunk| {
-
- // instrumentation: trace zone: profile line-state chunk ----------------------------------------------|
- // captures: profile-line state chunk wall time and row count |
- // why: preserves the chunk boundary for P/T spectroscopy preparation. |
- const chunk_zone = Trace.deepStaticZone(@src(), "optical_prepare.profile_line_state_chunk");
- chunk_zone.value(@intCast(chunk.len()));
- defer chunk_zone.end();
- // end instrumentation: trace zone: profile line-state chunk ------------------------------------------|
-
- fillProfileLineStateRows(worker, chunk.start, chunk.end);
- }
-}
-
-fn fillProfileLineStateRows(worker: *ProfileLineStateWorker, start: usize, end: usize) void {
- // fillProfileLineStateRows -------------------------------------------------------------------------------|
- // Fill one contiguous thermodynamic profile-node range claimed by the raw worker queue. |
- // --------------------------------------------------------------------------------------------------------|
- const stage_cost = costTimingForWorker(worker.cost_timing_active, worker.worker_index);
- for (start..end) |index| {
- const pressure_atm = @max(worker.pressures_hpa[index] / 1013.25, min_spectroscopy_pressure_atm);
- if (worker.weak_states) |states| {
- line_physics.fillWeakLineStateInto(
- &states[index],
- worker.lines,
- @max(worker.temperatures_k[index] + worker.temperature_offset_k, min_hitran_temperature_k),
- pressure_atm,
- stage_cost,
- );
- }
- if (worker.strong_states) |states| {
- states[index] = line_physics.prepareStrongLineState(
- worker.strong_lines,
- worker.relaxation_matrix,
- worker.temperatures_k[index],
- pressure_atm,
- stage_cost,
- );
- }
- }
-}
-
-fn emptyRelaxationMatrix() readers.RelaxationMatrixAsset {
- // emptyRelaxationMatrix ----------------------------------------------------------------------------------|
- // Provide an unused, well-formed relaxation-matrix view for weak-only profile-line state workers. |
- // --------------------------------------------------------------------------------------------------------|
- return .{ .line_count = 0, .wt0 = &.{}, .bw = &.{} };
-}
-
-fn activeLine(
- line: readers.LineAssetRow,
- active_isotopes: []const u8,
- line_strength_threshold: f64,
-) bool {
- // activeLine ---------------------------------------------------------------------------------------------|
- // Apply the line-gas isotope and weak-line threshold controls to one parsed HITRAN row. |
- // --------------------------------------------------------------------------------------------------------|
- if (line.gas_index != 7) return false;
-
- if (line.line_strength_cm2_per_molecule < line_strength_threshold) return false;
-
- if (active_isotopes.len == 0) return true;
-
- for (active_isotopes) |isotope_number| {
- if (line.isotope_number == isotope_number) return true;
- }
-
- return false;
-}
-
-fn runtimeLine(line: readers.LineAssetRow, active_isotopes: []const u8) bool {
- // runtimeLine --------------------------------------------------------------------------------------------|
- // Apply the total-sigma gas/isotope controls without the weak-line threshold filter. |
- // --------------------------------------------------------------------------------------------------------|
- if (line.gas_index != 7) return false;
-
- if (active_isotopes.len == 0) return true;
-
- for (active_isotopes) |isotope_number| {
- if (line.isotope_number == isotope_number) return true;
- }
-
- return false;
-}
-
-fn isotopeOneActive(active_isotopes: []const u8) bool {
- // isotopeOneActive ---------------------------------------------------------------------------------------|
- // LISA strong-line sidecars are isotope-1 O2 data; disable them when that isotope is not requested. |
- // --------------------------------------------------------------------------------------------------------|
- for (active_isotopes) |isotope_number| {
- if (isotope_number == 1) return true;
- }
- return false;
-}
diff --git a/src/internal.zig b/src/internal.zig
index 1a09f658..4df01896 100644
--- a/src/internal.zig
+++ b/src/internal.zig
@@ -1,5 +1,5 @@
// internal.zig -----------------------------------------------------------------------------------------------|
-// Test access router for setup, asset, cache, common, input, and instrumentation modules. |
+// Test access router for setup, asset, cache, common, input, and instrumentation modules. |
// |
// Product callers use src/root.zig. Tests use this file to reach small table builders without widening API. |
// public package surface before later modules add public forward-model entry points. |
@@ -110,6 +110,7 @@ pub const setup = struct {
pub const cache = struct {
pub const forward_worker_pool = @import("cache/forward_worker_pool.zig");
pub const session_memory = @import("cache/session_memory.zig");
+ pub const profile_line_build = @import("cache/profile_line_build.zig");
pub const profile_line_memory = @import("cache/profile_line_memory.zig");
pub const radiance_memory = @import("cache/radiance_memory.zig");
pub const solar_irradiance_memory = @import("cache/solar_irradiance_memory.zig");
@@ -119,7 +120,7 @@ pub const cache = struct {
// ------------------------------------------------------------------------------------------------------------|
// optics -----------------------------------------------------------------------------------------------------|
-// Namespace-only test import wrapper for optical-depth fill modules. |
+// Namespace-only test import wrapper for optical-depth fill modules. |
// |
// layout(64-bit) |
// size: 0 B (0.000 KiB), align: 1 B |
@@ -149,15 +150,12 @@ pub const optics = struct {
// footprint: per instance = 0 B; this wrapper is used as a comptime namespace only |
pub const output = struct {
pub const atmospheric_budget = @import("output/atmospheric_budget.zig");
- pub const instrument_response = @import("output/instrument_response.zig");
- pub const line_contributions = @import("output/line_contributions.zig");
- pub const cia_diagnostics = @import("output/cia_diagnostics.zig");
pub const spectrum = @import("output/spectrum.zig");
};
// ------------------------------------------------------------------------------------------------------------|
// retrieval --------------------------------------------------------------------------------------------------|
-// Namespace-only test import wrapper for retrieval state-space and result owners. |
+// Namespace-only test import wrapper for retrieval state-space and result owners. |
// |
// layout(64-bit) |
// size: 0 B (0.000 KiB), align: 1 B |
@@ -173,7 +171,7 @@ pub const retrieval = struct {
// ------------------------------------------------------------------------------------------------------------|
// rtm --------------------------------------------------------------------------------------------------------|
-// Namespace-only test import wrapper for RTM state modules. |
+// Namespace-only test import wrapper for RTM state modules. |
// |
// layout(64-bit) |
// size: 0 B (0.000 KiB), align: 1 B |
@@ -198,7 +196,7 @@ pub const rtm = struct {
// ------------------------------------------------------------------------------------------------------------|
// spectrum ---------------------------------------------------------------------------------------------------|
-// Namespace-only test import wrapper for spectrum sampling and exact wavelength modules. |
+// Namespace-only test import wrapper for spectrum sampling and exact wavelength modules. |
// |
// layout(64-bit) |
// size: 0 B (0.000 KiB), align: 1 B |
@@ -218,7 +216,7 @@ pub const spectrum = struct {
// ------------------------------------------------------------------------------------------------------------|
// validation -------------------------------------------------------------------------------------------------|
-// Namespace-only test import wrapper for validation-only metric helpers. |
+// Namespace-only test import wrapper for validation-only metric helpers. |
// |
// layout(64-bit) |
// size: 0 B (0.000 KiB), align: 1 B |
diff --git a/src/output/atmospheric_budget.zig b/src/output/atmospheric_budget.zig
index fae322cb..15c9d844 100644
--- a/src/output/atmospheric_budget.zig
+++ b/src/output/atmospheric_budget.zig
@@ -4,12 +4,12 @@ const layer_depths = @import("../optics/layer_depths.zig");
const scene_input = @import("../input/scene.zig");
const atmosphere_layers = @import("../setup/atmosphere_layers.zig");
const run_tables = @import("../setup/run_tables.zig");
-const profile_line_memory = @import("../cache/profile_line_memory.zig");
+const profile_line_build = @import("../cache/profile_line_build.zig");
const Allocator = std.mem.Allocator;
// atmospheric_budget.zig -------------------------------------------------------------------------------------|
-// Public atmospheric support-row diagnostic table for the explicit route. |
+// Public atmospheric support-row diagnostic table for the explicit route. |
// |
// boundary |
// The builder projects existing setup/profile-line/optics rows into the fixed Python C ABI row order. It |
@@ -138,7 +138,7 @@ pub fn build(
const rows = try allocator.alloc(AtmosphericBudgetRow, row_count);
errdefer allocator.free(rows);
- var profile_values = try profile_line_memory.buildProfileLineValuesForWavelengths(
+ var profile_values = try profile_line_build.buildProfileLineValuesForWavelengths(
allocator,
scene,
wavelengths_nm,
diff --git a/src/output/cia_diagnostics.zig b/src/output/cia_diagnostics.zig
deleted file mode 100644
index 14d147a8..00000000
--- a/src/output/cia_diagnostics.zig
+++ /dev/null
@@ -1,146 +0,0 @@
-const std = @import("std");
-
-const atmospheric_budget = @import("atmospheric_budget.zig");
-
-const Allocator = std.mem.Allocator;
-
-// cia_diagnostics.zig ----------------------------------------------------------------------------------------|
-// O2-O2 collision-induced absorption diagnostic rows for the explicit route. |
-// |
-// boundary |
-// The builder projects the already-public atmospheric-budget rows into the fixed Python C ABI row order. |
-// It does not parse inputs, own C handles, or call transport. |
-// |
-// the CIA-focused table and budget table keep identical wavelength/support-row ordering. |
-// |
-// math |
-// pair path = oxygen_number_density_cm3^2 * path_length_cm |
-// CIA cross section = cia_optical_depth / pair path |
-// CIA share = cia_optical_depth / total absorption or total optical depth |
-// |
-// memory |
-// `build` allocates one output row table and frees the temporary atmospheric-budget owner before returning. |
-// ------------------------------------------------------------------------------------------------------------|
-
-// CiaRow -----------------------------------------------------------------------------------------------------|
-// One public O2-O2 CIA diagnostic row for one wavelength and support row. |
-// |
-// layout(64-bit) |
-// size: 112 B (0.109 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] wavelength_nm : f64 |
-// [ 8.. 11] layer_index : u32 |
-// [ 12.. 15] sublayer_index : u32 |
-// [ 16.. 19] global_sublayer_index : u32 |
-// [ 20.. 23] interval_index_1based : u32 |
-// [ 24.. 31] altitude_km : f64 |
-// [ 32.. 39] pressure_hpa : f64 |
-// [ 40.. 47] temperature_k : f64 |
-// [ 48.. 55] oxygen_number_density_cm3 : f64 |
-// [ 56.. 63] path_length_cm : f64 |
-// [ 64.. 71] cia_cross_section_cm5_per_molecule2: f64 |
-// [ 72.. 79] cia_optical_depth : f64 |
-// [ 80.. 87] total_absorption_optical_depth : f64 |
-// [ 88.. 95] total_optical_depth : f64 |
-// [ 96..103] cia_share_of_total_absorption : f64 |
-// [104..111] cia_share_of_total_optical_depth : f64 |
-pub const CiaRow = extern struct {
- wavelength_nm: f64,
- layer_index: u32,
- sublayer_index: u32,
- global_sublayer_index: u32,
- interval_index_1based: u32,
- altitude_km: f64,
- pressure_hpa: f64,
- temperature_k: f64,
- oxygen_number_density_cm3: f64,
- path_length_cm: f64,
- cia_cross_section_cm5_per_molecule2: f64,
- cia_optical_depth: f64,
- total_absorption_optical_depth: f64,
- total_optical_depth: f64,
- cia_share_of_total_absorption: f64,
- cia_share_of_total_optical_depth: f64,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// CiaDiagnostics ---------------------------------------------------------------------------------------------|
-// Owned O2-O2 CIA diagnostic table returned through root/API calls. |
-// |
-// layout(64-bit) |
-// size: 16 B (0.016 KiB), align: 8 B |
-// |
-// memory |
-// [0..15] rows : []CiaRow |
-// |
-// referenced storage |
-// rows owns wavelength_count * support_row_count records in wavelength-major order. |
-pub const CiaDiagnostics = struct {
- rows: []CiaRow = &.{},
-
- pub fn deinit(self: *CiaDiagnostics, allocator: Allocator) void {
- // CiaDiagnostics.deinit ------------------------------------------------------------------------------|
- // Release the owned CIA diagnostic row table. |
- // ----------------------------------------------------------------------------------------------------|
- allocator.free(self.rows);
- self.* = .{};
- }
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-pub fn build(
- allocator: Allocator,
- budget: atmospheric_budget.AtmosphericBudget,
-) !CiaDiagnostics {
- // build --------------------------------------------------------------------------------------------------|
- // Build CIA-focused rows from an atmospheric-budget table with the same row order. |
- // --------------------------------------------------------------------------------------------------------|
- const rows = try allocator.alloc(CiaRow, budget.rows.len);
- errdefer allocator.free(rows);
-
- for (budget.rows, rows) |source, *target| {
- target.* = rowFromBudget(source);
- }
-
- return .{ .rows = rows };
-}
-
-fn rowFromBudget(row: atmospheric_budget.AtmosphericBudgetRow) CiaRow {
- // rowFromBudget ------------------------------------------------------------------------------------------|
- // Derive the public CIA-focused columns from one atmospheric-budget row. |
- // --------------------------------------------------------------------------------------------------------|
- const pair_path = row.oxygen_number_density_cm3 * row.oxygen_number_density_cm3 * row.path_length_cm;
-
- return .{
- .wavelength_nm = row.wavelength_nm,
- .layer_index = row.layer_index,
- .sublayer_index = row.sublayer_index,
- .global_sublayer_index = row.global_sublayer_index,
- .interval_index_1based = row.interval_index_1based,
- .altitude_km = row.altitude_km,
- .pressure_hpa = row.pressure_hpa,
- .temperature_k = row.temperature_k,
- .oxygen_number_density_cm3 = row.oxygen_number_density_cm3,
- .path_length_cm = row.path_length_cm,
- .cia_cross_section_cm5_per_molecule2 = safeDivide(row.cia_optical_depth, pair_path),
- .cia_optical_depth = row.cia_optical_depth,
- .total_absorption_optical_depth = row.total_absorption_optical_depth,
- .total_optical_depth = row.total_optical_depth,
- .cia_share_of_total_absorption = safeDivide(row.cia_optical_depth, row.total_absorption_optical_depth),
- .cia_share_of_total_optical_depth = safeDivide(row.cia_optical_depth, row.total_optical_depth),
- };
-}
-
-fn safeDivide(numerator: f64, denominator: f64) f64 {
- // safeDivide ---------------------------------------------------------------------------------------------|
- // Keep diagnostics finite on zero-path boundary rows and malformed denominator guards. |
- // --------------------------------------------------------------------------------------------------------|
- if (denominator <= 0.0 or !std.math.isFinite(denominator)) return 0.0;
- return numerator / denominator;
-}
-
-comptime {
- std.debug.assert(@sizeOf(CiaRow) == 112);
- std.debug.assert(@sizeOf(CiaDiagnostics) == 16);
-}
diff --git a/src/output/instrument_response.zig b/src/output/instrument_response.zig
deleted file mode 100644
index 8a0ced63..00000000
--- a/src/output/instrument_response.zig
+++ /dev/null
@@ -1,241 +0,0 @@
-const std = @import("std");
-
-const scene_input = @import("../input/scene.zig");
-const run_tables = @import("../setup/run_tables.zig");
-const sampling_table = @import("../spectrum/sampling_table.zig");
-const errors = @import("../common/errors.zig");
-
-const Allocator = std.mem.Allocator;
-
-pub const channel_mask_radiance: u32 = 1 << 0;
-pub const channel_mask_irradiance: u32 = 1 << 1;
-const allowed_channel_mask = channel_mask_radiance | channel_mask_irradiance;
-const integration_mode_disamar_hr_grid: u32 = 2;
-
-// instrument_response.zig ------------------------------------------------------------------------------------|
-// Public instrument-response support rows for the explicit route. |
-// |
-// boundary |
-// The builder projects spectrum sampling kernels into the fixed Python C ABI row order. It owns no C |
-// handles and performs no transport; it expands only instrument-kernel support samples. |
-// |
-// requested nominal wavelengths, then rounds only `nominal_index` back to the product grid. |
-// |
-// row order |
-// requested wavelength major, channel order radiance then irradiance, then support sample index. |
-// |
-// memory |
-// `build` allocates one temporary compact sampling table plus one returned row slice. |
-// ------------------------------------------------------------------------------------------------------------|
-
-// InstrumentResponseRow --------------------------------------------------------------------------------------|
-// One support sample from one instrument response kernel. |
-// |
-// layout(64-bit) |
-// size: 96 B (0.094 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 3] nominal_index : i32 |
-// [ 4.. 7] padding : 4 B |
-// [ 8..15] nominal_wavelength_nm : f64 |
-// [16..19] channel : u32 |
-// [20..23] sample_index : u32 |
-// [24..27] support_count : u32 |
-// [28..31] padding : 4 B |
-// [32..39] offset_nm : f64 |
-// [40..47] support_wavelength_nm : f64 |
-// [48..55] weight : f64 |
-// [56..63] support_width_nm : f64 |
-// [64..71] instrument_fwhm_nm : f64 |
-// [72..79] high_resolution_step_nm : f64 |
-// [80..87] high_resolution_half_span_nm : f64 |
-// [88..91] integration_mode : u32 |
-// [92..92] response_enabled : u8 |
-// [93..95] trailing padding : 3 B |
-pub const InstrumentResponseRow = extern struct {
- nominal_index: i32,
- nominal_wavelength_nm: f64,
- channel: u32,
- sample_index: u32,
- support_count: u32,
- offset_nm: f64,
- support_wavelength_nm: f64,
- weight: f64,
- support_width_nm: f64,
- instrument_fwhm_nm: f64,
- high_resolution_step_nm: f64,
- high_resolution_half_span_nm: f64,
- integration_mode: u32,
- response_enabled: u8,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// InstrumentResponse -----------------------------------------------------------------------------------------|
-// Owned instrument-response diagnostic table returned through root/API calls. |
-// |
-// layout(64-bit) |
-// size: 16 B (0.016 KiB), align: 8 B |
-// |
-// memory |
-// [0..15] rows : []InstrumentResponseRow |
-// |
-// referenced storage |
-// rows owns one record per requested wavelength, enabled channel, and support sample. |
-pub const InstrumentResponse = struct {
- rows: []InstrumentResponseRow = &.{},
-
- pub fn deinit(self: *InstrumentResponse, allocator: Allocator) void {
- // InstrumentResponse.deinit --------------------------------------------------------------------------|
- // Release the owned instrument-response row table. |
- // ----------------------------------------------------------------------------------------------------|
- allocator.free(self.rows);
- self.* = .{};
- }
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-pub fn build(
- allocator: Allocator,
- scene: scene_input.Scene,
- tables: *const run_tables.RunTables,
- nominal_wavelengths_nm: []const f64,
- channel_mask: u32,
-) !InstrumentResponse {
- // build --------------------------------------------------------------------------------------------------|
- // Build public instrument-response support rows for caller-selected wavelengths and channels. |
- // --------------------------------------------------------------------------------------------------------|
- if (nominal_wavelengths_nm.len == 0) return errors.Error.InvalidControl;
-
- if ((channel_mask & allowed_channel_mask) == 0) return errors.Error.InvalidControl;
-
- if ((channel_mask & ~allowed_channel_mask) != 0) return errors.Error.InvalidControl;
-
- var owned_sampling = try sampling_table.buildSpectrumSamplingTableForWavelengths(
- allocator,
- scene,
- tables.instrument,
- tables.lines,
- nominal_wavelengths_nm,
- );
- defer owned_sampling.deinit(allocator);
-
- const table = owned_sampling.view();
- const row_count = responseRowCount(table, channel_mask);
- const rows = try allocator.alloc(InstrumentResponseRow, row_count);
- errdefer allocator.free(rows);
-
- var row_index: usize = 0;
- for (table.rows) |row| {
- if ((channel_mask & channel_mask_radiance) != 0) {
- row_index = writeChannelRows(scene, tables, table, row, .radiance, rows, row_index);
- }
- if ((channel_mask & channel_mask_irradiance) != 0) {
- row_index = writeChannelRows(scene, tables, table, row, .irradiance, rows, row_index);
- }
- }
- std.debug.assert(row_index == rows.len);
-
- return .{ .rows = rows };
-}
-
-const Channel = enum {
- radiance,
- irradiance,
-};
-
-fn responseRowCount(table: sampling_table.SpectrumSamplingTable, channel_mask: u32) usize {
- // responseRowCount ---------------------------------------------------------------------------------------|
- // Count expanded support rows before allocation so row append is a single linear fill. |
- // --------------------------------------------------------------------------------------------------------|
- var count: usize = 0;
- for (table.rows) |row| {
- if ((channel_mask & channel_mask_radiance) != 0) {
- count += row.radiance_integration.activeSampleCount();
- }
- if ((channel_mask & channel_mask_irradiance) != 0) {
- count += row.irradiance_integration.activeSampleCount();
- }
- }
- return count;
-}
-
-fn writeChannelRows(
- scene: scene_input.Scene,
- tables: *const run_tables.RunTables,
- table: sampling_table.SpectrumSamplingTable,
- row: sampling_table.SpectrumSamplingRow,
- channel: Channel,
- rows: []InstrumentResponseRow,
- row_start: usize,
-) usize {
- // writeChannelRows ---------------------------------------------------------------------------------------|
- // Expand one compact kernel reference into contiguous fixed-ABI diagnostic rows. |
- // --------------------------------------------------------------------------------------------------------|
- const kernel = switch (channel) {
- .radiance => row.radiance_integration,
- .irradiance => row.irradiance_integration,
- };
- const support_count = kernel.activeSampleCount();
- const support_width_nm = supportWidthNm(kernel, table.kernel_storage, support_count);
- const channel_code: u32 = switch (channel) {
- .radiance => 0,
- .irradiance => 1,
- };
- const nominal_index = nearestNominalIndex(scene, row.nominal_wavelength_nm);
-
- var row_index = row_start;
- for (0..support_count) |sample_index| {
- const offset_nm = kernel.offsetNm(table.kernel_storage, sample_index);
- rows[row_index] = .{
- .nominal_index = nominal_index,
- .nominal_wavelength_nm = row.nominal_wavelength_nm,
- .channel = channel_code,
- .sample_index = @intCast(sample_index),
- .support_count = @intCast(support_count),
- .offset_nm = offset_nm,
- .support_wavelength_nm = row.nominal_wavelength_nm + offset_nm,
- .weight = kernel.weight(table.kernel_storage, sample_index),
- .support_width_nm = support_width_nm,
- .instrument_fwhm_nm = tables.instrument.line_fwhm_nm,
- .high_resolution_step_nm = tables.instrument.high_resolution_step_nm,
- .high_resolution_half_span_nm = tables.instrument.high_resolution_half_span_nm,
- .integration_mode = integration_mode_disamar_hr_grid,
- .response_enabled = if (kernel.enabled()) 1 else 0,
- };
- row_index += 1;
- }
- return row_index;
-}
-
-fn supportWidthNm(
- kernel: sampling_table.IntegrationKernelRef,
- storage: sampling_table.IntegrationKernelStorage,
- support_count: usize,
-) f64 {
- // supportWidthNm -----------------------------------------------------------------------------------------|
- // Match the diagnostic width: last selected offset minus first selected offset. |
- // --------------------------------------------------------------------------------------------------------|
- if (support_count <= 1) return 0.0;
- return kernel.offsetNm(storage, support_count - 1) - kernel.offsetNm(storage, 0);
-}
-
-fn nearestNominalIndex(scene: scene_input.Scene, nominal_wavelength_nm: f64) i32 {
- // nearestNominalIndex ------------------------------------------------------------------------------------|
- // Round diagnostic wavelengths back to the product grid without changing the sampled kernel location. |
- // --------------------------------------------------------------------------------------------------------|
- const sample_count = scene.spectral_grid.sample_count;
- if (sample_count <= 1) return 0;
-
- const step_nm = (scene.spectral_grid.end_nm - scene.spectral_grid.start_nm) /
- @as(f64, @floatFromInt(sample_count - 1));
- if (step_nm <= 0.0) return 0;
-
- const raw = @round((nominal_wavelength_nm - scene.spectral_grid.start_nm) / step_nm);
- const clamped = std.math.clamp(raw, 0.0, @as(f64, @floatFromInt(sample_count - 1)));
- return @intFromFloat(clamped);
-}
-
-comptime {
- std.debug.assert(@sizeOf(InstrumentResponseRow) == 96);
- std.debug.assert(@sizeOf(InstrumentResponse) == 16);
-}
diff --git a/src/output/line_contributions.zig b/src/output/line_contributions.zig
deleted file mode 100644
index e8dab587..00000000
--- a/src/output/line_contributions.zig
+++ /dev/null
@@ -1,651 +0,0 @@
-const std = @import("std");
-
-const run_tables = @import("../setup/run_tables.zig");
-const readers = @import("../assets/readers.zig");
-const line_physics = @import("../spectrum/line_physics.zig");
-
-const Allocator = std.mem.Allocator;
-
-const min_spectroscopy_pressure_atm = 1.0e-12;
-const missing_index = std.math.maxInt(u32);
-const vendor_cutoff_prewindow_margin_cm1 = line_physics.vendor_cutoff_prewindow_margin_cm1;
-
-// line_contributions.zig -------------------------------------------------------------------------------------|
-// Public O2 line-by-line diagnostic rows for selected wavelengths and spectroscopy profile nodes. |
-// |
-// boundary |
-// The builder renders explanation rows from prepared setup tables into the fixed Python C ABI row order. |
-// It does not parse inputs, own C handles, call transport, or feed the forward solve. |
-// |
-// line and ConvTP sidecar math is delegated to `src/spectrum/line_physics.zig`; this file only attaches |
-// diagnostic row metadata and public row-status labels. |
-// |
-// row order |
-// wavelength-major -> spectroscopy profile node -> relevant weak-line window -> strong-line sidecars. |
-// |
-// memory |
-// `rows` owns at most max_rows records. `total_row_count` always reports the full untruncated diagnostic |
-// row count so Python can distinguish a short materialized table from a short route. |
-// ------------------------------------------------------------------------------------------------------------|
-
-// LineRowKind ------------------------------------------------------------------------------------------------|
-// Public row kind labels used by python/zdisamar/output/tables.py. |
-pub const LineRowKind = enum(u32) {
- weak_line = 0,
- strong_line = 1,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// LineStatus -------------------------------------------------------------------------------------------------|
-// Public row status labels used by python/zdisamar/output/tables.py. |
-pub const LineStatus = enum(u32) {
- weak_included = 0,
- weak_excluded_by_strong_line = 1,
- strong_sidecar = 2,
- weak_zero_after_cutoff = 3,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// LineContributionRow ----------------------------------------------------------------------------------------|
-// One public O2 spectroscopy diagnostic row for one wavelength and thermodynamic profile node. |
-// |
-// layout(64-bit) |
-// size: 168 B (0.164 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] wavelength_nm : f64 |
-// [ 8.. 11] profile_node_index : u32 |
-// [ 12.. 15] padding : 4 B |
-// [ 16.. 23] altitude_km : f64 |
-// [ 24.. 27] row_kind : LineRowKind |
-// [ 28.. 31] status : LineStatus |
-// [ 32.. 35] line_index : u32 |
-// [ 36.. 39] strong_line_index : u32 |
-// [ 40.. 43] matched_strong_line_index : u32 |
-// [ 44.. 45] gas_index : u16 |
-// [ 46.. 46] isotope_number : u8 |
-// [ 47.. 47] padding : 1 B |
-// [ 48.. 51] isotopologue_code : i32 |
-// [ 52.. 55] padding : 4 B |
-// [ 56.. 63] center_wavelength_nm : f64 |
-// [ 64.. 71] center_wavenumber_cm1 : f64 |
-// [ 72.. 79] shifted_center_wavenumber_cm1 : f64 |
-// [ 80.. 87] line_strength_cm2_per_molecule : f64 |
-// [ 88.. 95] air_half_width_cm1 : f64 |
-// [ 96..103] pressure_shift_cm1 : f64 |
-// [104..111] lower_state_energy_cm1 : f64 |
-// [112..119] temperature_k : f64 |
-// [120..127] pressure_hpa : f64 |
-// [128..135] weak_line_sigma_cm2_per_molecule : f64 |
-// [136..143] strong_line_sigma_cm2_per_molecule : f64 |
-// [144..151] line_mixing_sigma_cm2_per_molecule : f64 |
-// [152..159] total_sigma_cm2_per_molecule : f64 |
-// [160..167] abs_total_sigma_cm2_per_molecule : f64 |
-pub const LineContributionRow = extern struct {
- wavelength_nm: f64,
- profile_node_index: u32,
- altitude_km: f64,
- row_kind: LineRowKind,
- status: LineStatus,
- line_index: u32,
- strong_line_index: u32,
- matched_strong_line_index: u32,
- gas_index: u16,
- isotope_number: u8,
- isotopologue_code: i32,
- center_wavelength_nm: f64,
- center_wavenumber_cm1: f64,
- shifted_center_wavenumber_cm1: f64,
- line_strength_cm2_per_molecule: f64,
- air_half_width_cm1: f64,
- pressure_shift_cm1: f64,
- lower_state_energy_cm1: f64,
- temperature_k: f64,
- pressure_hpa: f64,
- weak_line_sigma_cm2_per_molecule: f64,
- strong_line_sigma_cm2_per_molecule: f64,
- line_mixing_sigma_cm2_per_molecule: f64,
- total_sigma_cm2_per_molecule: f64,
- abs_total_sigma_cm2_per_molecule: f64,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-// LineContributions ------------------------------------------------------------------------------------------|
-// Owned O2 spectroscopy diagnostic table returned through root/API calls. |
-// |
-// layout(64-bit) |
-// size: 32 B (0.031 KiB), align: 8 B |
-// |
-// memory |
-// [ 0..15] rows : []LineContributionRow |
-// [16..23] total_row_count : usize |
-// [24..24] truncated : bool |
-// [25..31] padding : 7 B |
-// |
-// referenced storage |
-// rows owns min(total_row_count, max_rows) records. |
-pub const LineContributions = struct {
- rows: []LineContributionRow = &.{},
- total_row_count: usize = 0,
- truncated: bool = false,
-
- pub fn deinit(self: *LineContributions, allocator: Allocator) void {
- // LineContributions.deinit ---------------------------------------------------------------------------|
- // Release materialized diagnostic rows. |
- // ----------------------------------------------------------------------------------------------------|
- allocator.free(self.rows);
- self.* = .{};
- }
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-const ThermodynamicState = struct {
- profile_node_index: u32,
- altitude_km: f64,
- temperature_k: f64,
- pressure_hpa: f64,
-};
-
-const RuntimeControls = struct {
- cutoff_cm1: f64,
- line_mixing_factor: f64,
-};
-
-const RuntimeLine = struct {
- line: readers.LineAssetRow,
- line_index: u32,
-};
-
-const LineWindow = struct {
- lines: []const RuntimeLine,
-};
-
-const StrongLinePreparedState = line_physics.StrongLinePreparedState;
-
-const StrongAnchorFields = struct {
- line_index: u32,
- gas_index: u16,
- isotope_number: u8,
- isotopologue_code: i32,
- line_strength_cm2_per_molecule: f64,
-};
-
-const StrongAnchorMatch = struct {
- line: *const readers.LineAssetRow,
- line_index: u32,
-};
-
-pub fn build(
- allocator: Allocator,
- tables: *const run_tables.RunTables,
- wavelengths_nm: []const f64,
- max_rows: usize,
-) !LineContributions {
- // build --------------------------------------------------------------------------------------------------|
- // Build public O2 line-contribution diagnostics for caller-selected wavelengths. |
- // |
- // memory |
- // Allocates one output table capped by max_rows; all spectroscopy rows and profile nodes are borrowed |
- // from RunTables. |
- // --------------------------------------------------------------------------------------------------------|
- if (wavelengths_nm.len == 0) return error.EmptyWavelengths;
- if (max_rows == 0) return error.InvalidRowLimit;
-
- const runtime = RuntimeControls{
- .cutoff_cm1 = tables.lines.cutoff_sim_cm1,
- .line_mixing_factor = tables.lines.line_mixing_factor,
- };
- const runtime_lines = try collectRuntimeLines(allocator, tables.lines.rows, tables.lines.isotopes_sim);
- defer allocator.free(runtime_lines);
- const strong_line_count = @min(tables.lines.strong_lines.len, line_physics.max_strong_line_sidecars);
- var rows = std.ArrayList(LineContributionRow).empty;
- errdefer rows.deinit(allocator);
- try rows.ensureTotalCapacity(allocator, @min(max_rows, runtime_lines.len + strong_line_count));
- var window_rows = std.ArrayList(RuntimeLine).empty;
- defer window_rows.deinit(allocator);
-
- var total_row_count: usize = 0;
- for (wavelengths_nm) |wavelength_nm| {
- const window = try relevantLineWindow(
- allocator,
- &window_rows,
- runtime_lines,
- wavelength_nm,
- runtime.cutoff_cm1,
- );
- for (tables.layers.spectroscopy_profile.rows, 0..) |profile_row, profile_node_index| {
- try appendRowsForState(
- allocator,
- &rows,
- &total_row_count,
- max_rows,
- tables,
- window,
- runtime,
- wavelength_nm,
- .{
- .profile_node_index = @intCast(profile_node_index),
- .altitude_km = profile_row.altitude_km,
- .temperature_k = profile_row.temperature_k,
- .pressure_hpa = profile_row.pressure_hpa,
- },
- );
- }
- }
-
- return .{
- .rows = try rows.toOwnedSlice(allocator),
- .total_row_count = total_row_count,
- .truncated = total_row_count > max_rows,
- };
-}
-
-fn appendRowsForState(
- allocator: Allocator,
- rows: *std.ArrayList(LineContributionRow),
- total_row_count: *usize,
- max_rows: usize,
- tables: *const run_tables.RunTables,
- window: LineWindow,
- runtime: RuntimeControls,
- wavelength_nm: f64,
- thermodynamic_state: ThermodynamicState,
-) !void {
- // appendRowsForState -------------------------------------------------------------------------------------|
- // Append weak-line rows and strong-line sidecar rows for one wavelength/profile-node state. |
- // --------------------------------------------------------------------------------------------------------|
- const pressure_atm = @max(thermodynamic_state.pressure_hpa / 1013.25, min_spectroscopy_pressure_atm);
- const safe_temperature = @max(thermodynamic_state.temperature_k, line_physics.min_hitran_temperature_k);
- const vendor_partition = usesVendorStrongLinePartition(window.lines, tables.lines.strong_lines);
-
- for (window.lines, 0..) |runtime_line, line_index| {
- total_row_count.* += 1;
- if (rows.items.len >= max_rows) continue;
- try rows.append(allocator, weakLineRow(
- tables.lines.strong_lines,
- window,
- wavelength_nm,
- thermodynamic_state,
- safe_temperature,
- pressure_atm,
- runtime_line,
- line_index,
- vendor_partition,
- runtime,
- ));
- }
-
- const strong_state = line_physics.prepareStrongLineState(
- tables.lines.strong_lines,
- tables.lines.relaxation_matrix,
- safe_temperature,
- pressure_atm,
- null,
- );
- for (tables.lines.strong_lines[0..strong_state.line_count], 0..) |strong_line, strong_index| {
- total_row_count.* += 1;
- if (rows.items.len >= max_rows) continue;
- try rows.append(allocator, strongLineRow(
- tables.lines.strong_lines,
- strong_line,
- strong_index,
- window,
- wavelength_nm,
- thermodynamic_state,
- safe_temperature,
- pressure_atm,
- runtime,
- vendor_partition,
- &strong_state,
- ));
- }
-}
-
-fn weakLineRow(
- strong_lines: []const readers.StrongLineAssetRow,
- window: LineWindow,
- wavelength_nm: f64,
- thermodynamic_state: ThermodynamicState,
- temperature_k: f64,
- pressure_atm: f64,
- runtime_line: RuntimeLine,
- line_index: usize,
- vendor_partition: bool,
- runtime: RuntimeControls,
-) LineContributionRow {
- // weakLineRow --------------------------------------------------------------------------------------------|
- // Project one HITRAN weak-line row and its canonical inclusion status into the public diagnostic shape. |
- // --------------------------------------------------------------------------------------------------------|
- const line = runtime_line.line;
- const matched_strong_index = matchedStrongIndexForRelevantLine(strong_lines, line, vendor_partition);
- const excluded = shouldExcludeWeakLine(line, line_index, window.lines, strong_lines, vendor_partition);
-
- var weak_sigma: f64 = 0.0;
- var status: LineStatus = .weak_excluded_by_strong_line;
-
- if (!excluded) {
- weak_sigma = line_physics.weakLineContribution(
- wavelength_nm,
- line,
- temperature_k,
- pressure_atm,
- runtime.cutoff_cm1,
- &.{},
- &.{},
- null,
- );
-
- status = if (weak_sigma == 0.0) .weak_zero_after_cutoff else .weak_included;
- }
-
- const matched_strong_line_index: u32 = if (matched_strong_index) |index|
- @intCast(index)
- else
- missing_index;
-
- return .{
- .wavelength_nm = wavelength_nm,
- .profile_node_index = thermodynamic_state.profile_node_index,
- .altitude_km = thermodynamic_state.altitude_km,
- .row_kind = .weak_line,
- .status = status,
- .line_index = runtime_line.line_index,
- .strong_line_index = missing_index,
- .matched_strong_line_index = matched_strong_line_index,
- .gas_index = line.gas_index,
- .isotope_number = line.isotope_number,
- .isotopologue_code = line_physics.deriveIsotopologueCode(line.gas_index, line.isotope_number),
- .center_wavelength_nm = line.center_wavelength_nm,
- .center_wavenumber_cm1 = line.center_wavenumber_cm1,
- .shifted_center_wavenumber_cm1 = line_physics.shiftedCenterWavenumberCm1(line, pressure_atm),
- .line_strength_cm2_per_molecule = line.line_strength_cm2_per_molecule,
- .air_half_width_cm1 = line.air_half_width_cm1,
- .pressure_shift_cm1 = line.pressure_shift_cm1,
- .lower_state_energy_cm1 = line.lower_state_energy_cm1,
- .temperature_k = temperature_k,
- .pressure_hpa = thermodynamic_state.pressure_hpa,
- .weak_line_sigma_cm2_per_molecule = weak_sigma,
- .strong_line_sigma_cm2_per_molecule = 0.0,
- .line_mixing_sigma_cm2_per_molecule = 0.0,
- .total_sigma_cm2_per_molecule = weak_sigma,
- .abs_total_sigma_cm2_per_molecule = @abs(weak_sigma),
- };
-}
-
-fn strongLineRow(
- strong_lines: []const readers.StrongLineAssetRow,
- strong_line: readers.StrongLineAssetRow,
- strong_index: usize,
- window: LineWindow,
- wavelength_nm: f64,
- thermodynamic_state: ThermodynamicState,
- temperature_k: f64,
- pressure_atm: f64,
- runtime: RuntimeControls,
- vendor_partition: bool,
- strong_state: *const StrongLinePreparedState,
-) LineContributionRow {
- // strongLineRow ------------------------------------------------------------------------------------------|
- // Project one LISA strong-line sidecar row and its weak-line anchor metadata into the public table. |
- // --------------------------------------------------------------------------------------------------------|
- const contribution = line_physics.strongLineContribution(
- wavelength_nm,
- strong_index,
- strong_state,
- temperature_k,
- pressure_atm,
- );
- const line_mixing_sigma = contribution.line_mixing_sigma_cm2_per_molecule * runtime.line_mixing_factor;
- const total_sigma = @max(contribution.strong_line_sigma_cm2_per_molecule + line_mixing_sigma, 0.0);
- const anchor = strongAnchorLine(strong_lines, window, strong_index, vendor_partition);
- const anchor_fields = resolveStrongAnchorFields(anchor);
-
- return .{
- .wavelength_nm = wavelength_nm,
- .profile_node_index = thermodynamic_state.profile_node_index,
- .altitude_km = thermodynamic_state.altitude_km,
- .row_kind = .strong_line,
- .status = .strong_sidecar,
- .line_index = anchor_fields.line_index,
- .strong_line_index = @intCast(strong_index),
- .matched_strong_line_index = @intCast(strong_index),
- .gas_index = anchor_fields.gas_index,
- .isotope_number = anchor_fields.isotope_number,
- .isotopologue_code = anchor_fields.isotopologue_code,
- .center_wavelength_nm = strong_line.center_wavelength_nm,
- .center_wavenumber_cm1 = strong_line.center_wavenumber_cm1,
- .shifted_center_wavenumber_cm1 = strong_state.mod_sig_cm1[strong_index],
- .line_strength_cm2_per_molecule = anchor_fields.line_strength_cm2_per_molecule,
- .air_half_width_cm1 = strong_line.air_half_width_cm1,
- .pressure_shift_cm1 = strong_line.pressure_shift_cm1,
- .lower_state_energy_cm1 = strong_line.lower_state_energy_cm1,
- .temperature_k = temperature_k,
- .pressure_hpa = thermodynamic_state.pressure_hpa,
- .weak_line_sigma_cm2_per_molecule = 0.0,
- .strong_line_sigma_cm2_per_molecule = contribution.strong_line_sigma_cm2_per_molecule,
- .line_mixing_sigma_cm2_per_molecule = line_mixing_sigma,
- .total_sigma_cm2_per_molecule = total_sigma,
- .abs_total_sigma_cm2_per_molecule = @abs(total_sigma),
- };
-}
-
-fn resolveStrongAnchorFields(anchor: ?StrongAnchorMatch) StrongAnchorFields {
- // resolveStrongAnchorFields ------------------------------------------------------------------------------|
- // Copy weak-line metadata for sidecars, or keep the complete-row defaults when no anchor exists. |
- // --------------------------------------------------------------------------------------------------------|
- if (anchor) |owned| {
- return .{
- .line_index = owned.line_index,
- .gas_index = owned.line.gas_index,
- .isotope_number = owned.line.isotope_number,
- .isotopologue_code = line_physics.deriveIsotopologueCode(
- owned.line.gas_index,
- owned.line.isotope_number,
- ),
- .line_strength_cm2_per_molecule = owned.line.line_strength_cm2_per_molecule,
- };
- }
-
- return .{
- .line_index = missing_index,
- .gas_index = 7,
- .isotope_number = 1,
- .isotopologue_code = 66,
- .line_strength_cm2_per_molecule = std.math.nan(f64),
- };
-}
-
-fn strongAnchorLine(
- strong_lines: []const readers.StrongLineAssetRow,
- window: LineWindow,
- strong_index: usize,
- vendor_partition: bool,
-) ?StrongAnchorMatch {
- // strongAnchorLine ---------------------------------------------------------------------------------------|
- // Find the weak-line row attached to one strong sidecar within the current relevant window. |
- // --------------------------------------------------------------------------------------------------------|
- if (vendor_partition) return null;
- if (strong_index >= strong_lines.len) return null;
- const strong_line = strong_lines[strong_index];
- var best_index: ?usize = null;
- var best_delta = std.math.inf(f64);
- var best_strength: f64 = -std.math.inf(f64);
-
- for (window.lines, 0..) |runtime_line, line_index| {
- const line = runtime_line.line;
- const delta = @abs(strong_line.center_wavelength_nm - line.center_wavelength_nm);
- const tolerance_nm = @max(0.01, strong_line.air_half_width_nm * 4.0);
-
- if (delta > tolerance_nm) continue;
- if (delta > best_delta) continue;
- if (delta == best_delta and line.line_strength_cm2_per_molecule < best_strength) continue;
-
- best_index = line_index;
- best_delta = delta;
- best_strength = line.line_strength_cm2_per_molecule;
- }
-
- const index = best_index orelse return null;
- return .{ .line = &window.lines[index].line, .line_index = window.lines[index].line_index };
-}
-
-fn relevantLineWindow(
- allocator: Allocator,
- rows: *std.ArrayList(RuntimeLine),
- lines: []const RuntimeLine,
- wavelength_nm: f64,
- cutoff_cm1: f64,
-) !LineWindow {
- // relevantLineWindow -------------------------------------------------------------------------------------|
- // Fill a wavelength-local weak-line prewindow, preserving filtered source order for equal centers. |
- // --------------------------------------------------------------------------------------------------------|
- rows.clearRetainingCapacity();
- if (lines.len == 0) return .{ .lines = rows.items };
-
- const evaluation_wavenumber_cm1 = line_physics.wavelengthToWavenumberCm1(wavelength_nm);
- const prewindow_cm1 = cutoff_cm1 + vendor_cutoff_prewindow_margin_cm1;
- const lower_wavenumber_cm1 = @max(evaluation_wavenumber_cm1 - prewindow_cm1, 1.0);
- const upper_wavenumber_cm1 = evaluation_wavenumber_cm1 + prewindow_cm1;
- const lower_wavelength_nm = line_physics.wavenumberToWavelengthNm(upper_wavenumber_cm1);
- const upper_wavelength_nm = line_physics.wavenumberToWavelengthNm(lower_wavenumber_cm1);
- for (lines) |line| {
- const center_wavelength_nm = line.line.center_wavelength_nm;
- if (center_wavelength_nm < lower_wavelength_nm or center_wavelength_nm >= upper_wavelength_nm) continue;
- try rows.append(allocator, line);
- }
- return .{ .lines = rows.items };
-}
-
-fn collectRuntimeLines(
- allocator: Allocator,
- lines: []const readers.LineAssetRow,
- active_isotopes: []const u8,
-) ![]RuntimeLine {
- // collectRuntimeLines ------------------------------------------------------------------------------------|
- // Copy O2 rows that participate in diagnostic evaluation, without applying the weak-line threshold. |
- // --------------------------------------------------------------------------------------------------------|
- var active_count: usize = 0;
- for (lines) |line| {
- if (runtimeLine(line, active_isotopes)) active_count += 1;
- }
-
- const active_lines = try allocator.alloc(RuntimeLine, active_count);
- errdefer allocator.free(active_lines);
-
- var active_index: usize = 0;
- for (lines) |line| {
- if (!runtimeLine(line, active_isotopes)) continue;
- active_lines[active_index] = .{ .line = line, .line_index = @intCast(active_index) };
- active_index += 1;
- }
-
- std.sort.pdq(RuntimeLine, active_lines, {}, lessRuntimeLineByCenter);
- for (active_lines, 0..) |*line, sorted_index| {
- line.line_index = @intCast(sorted_index);
- }
- return active_lines;
-}
-
-fn lessRuntimeLineByCenter(_: void, lhs: RuntimeLine, rhs: RuntimeLine) bool {
- // lessRuntimeLineByCenter --------------------------------------------------------------------------------|
- // Match sortLineList: pdq sort by center wavelength only. Equal-center ordering is the pdq result. |
- // --------------------------------------------------------------------------------------------------------|
- return lhs.line.center_wavelength_nm < rhs.line.center_wavelength_nm;
-}
-
-fn runtimeLine(line: readers.LineAssetRow, active_isotopes: []const u8) bool {
- // runtimeLine --------------------------------------------------------------------------------------------|
- // Apply the total-sigma gas/isotope controls without the weak-line threshold filter. |
- // --------------------------------------------------------------------------------------------------------|
- if (line.gas_index != 7) return false;
- if (active_isotopes.len == 0) return true;
- for (active_isotopes) |isotope_number| {
- if (line.isotope_number == isotope_number) return true;
- }
- return false;
-}
-
-fn usesVendorStrongLinePartition(
- lines: []const RuntimeLine,
- strong_lines: []const readers.StrongLineAssetRow,
-) bool {
- // usesVendorStrongLinePartition --------------------------------------------------------------------------|
- // Detect the sidecar partition from retained HITRAN branch metadata. |
- // --------------------------------------------------------------------------------------------------------|
- if (strong_lines.len == 0) return false;
-
- for (lines) |runtime_line| {
- const line = runtime_line.line;
- if (line.gas_index == 7 and
- line.branch_ic1 != null and
- line.branch_ic2 != null and
- line.rotational_nf != null)
- {
- return true;
- }
- }
- return false;
-}
-
-fn matchedStrongIndexForRelevantLine(
- strong_lines: []const readers.StrongLineAssetRow,
- line: readers.LineAssetRow,
- vendor_partition: bool,
-) ?usize {
- // matchedStrongIndexForRelevantLine ----------------------------------------------------------------------|
- // Report the sidecar index associated with a weak line when the partition rules assign one. |
- // --------------------------------------------------------------------------------------------------------|
- if (vendor_partition and !line_physics.isVendorStrongCandidateFromSource(line)) return null;
- return line_physics.findStrongLineMatch(strong_lines, line.center_wavelength_nm);
-}
-
-fn shouldExcludeWeakLine(
- line: readers.LineAssetRow,
- line_index: usize,
- window: []const RuntimeLine,
- strong_lines: []const readers.StrongLineAssetRow,
- vendor_partition: bool,
-) bool {
- // shouldExcludeWeakLine ----------------------------------------------------------------------------------|
- // Keep lines covered by O2 strong-line sidecars out of the weak-line contribution. |
- // --------------------------------------------------------------------------------------------------------|
- if (vendor_partition) {
- if (!line_physics.isVendorStrongCandidateFromSource(line)) return false;
- return line_physics.findStrongLineMatch(strong_lines, line.center_wavelength_nm) != null;
- }
-
- const strong_index = line_physics.findStrongLineMatch(strong_lines, line.center_wavelength_nm) orelse return false;
- return strongestWindowAnchorForSidecar(window, strong_lines[strong_index]) == line_index;
-}
-
-fn strongestWindowAnchorForSidecar(
- window: []const RuntimeLine,
- strong_line: readers.StrongLineAssetRow,
-) usize {
- // strongestWindowAnchorForSidecar ------------------------------------------------------------------------|
- // Select the generic sidecar anchor: closest line center, then strongest line on an equal delta. |
- // --------------------------------------------------------------------------------------------------------|
- var best_index: usize = 0;
- var best_delta = std.math.inf(f64);
- var best_strength: f64 = -std.math.inf(f64);
-
- for (window, 0..) |runtime_line, line_index| {
- const line = runtime_line.line;
- const delta = @abs(strong_line.center_wavelength_nm - line.center_wavelength_nm);
- const tolerance_nm = @max(0.01, strong_line.air_half_width_nm * 4.0);
-
- if (delta > tolerance_nm) continue;
- if (delta > best_delta) continue;
- if (delta == best_delta and line.line_strength_cm2_per_molecule < best_strength) continue;
-
- best_index = line_index;
- best_delta = delta;
- best_strength = line.line_strength_cm2_per_molecule;
- }
- return best_index;
-}
-
-comptime {
- std.debug.assert(@sizeOf(LineContributionRow) == 168);
- std.debug.assert(@sizeOf(LineContributions) == 32);
-}
diff --git a/src/root.zig b/src/root.zig
index 4c297723..da64eacb 100644
--- a/src/root.zig
+++ b/src/root.zig
@@ -8,10 +8,7 @@ const jacobian_states = @import("rtm/jacobian_states.zig");
const scene_input = @import("input/scene.zig");
const session_memory = @import("cache/session_memory.zig");
const atmospheric_budget = @import("output/atmospheric_budget.zig");
-const instrument_response = @import("output/instrument_response.zig");
const instrument_tables = @import("setup/instrument_tables.zig");
-const line_contributions = @import("output/line_contributions.zig");
-const cia_diagnostics = @import("output/cia_diagnostics.zig");
const output_spectrum = @import("output/spectrum.zig");
const retrieval = @import("retrieval/root.zig");
const retrieval_algebra = @import("retrieval/algebra.zig");
@@ -25,6 +22,7 @@ const phase_table = @import("setup/phase_table.zig");
const setup_tables = @import("setup/run_tables.zig");
const line_tables = @import("setup/line_tables.zig");
const solar_table = @import("setup/solar_table.zig");
+const profile_line_build = @import("cache/profile_line_build.zig");
const profile_lines = @import("cache/profile_line_memory.zig");
const sampling_table = @import("spectrum/sampling_table.zig");
const solve = @import("rtm/solve.zig");
@@ -35,7 +33,7 @@ const Allocator = std.mem.Allocator;
pub const geometry_direction_cosine_floor: f64 = 0.05;
// root.zig ---------------------------------------------------------------------------------------------------|
-// Public explicit row surface for the forward model. |
+// Public explicit row surface for the forward model. |
// |
// public flow |
// Scene -> prepare -> warmSessionMemory -> runForwardWithSessionMemory |
@@ -52,12 +50,6 @@ pub const ProfileLineValues = profile_lines.ProfileLineValues;
pub const SessionMemory = session_memory.SessionMemory;
pub const AtmosphericBudget = atmospheric_budget.AtmosphericBudget;
pub const AtmosphericBudgetRow = atmospheric_budget.AtmosphericBudgetRow;
-pub const InstrumentResponse = instrument_response.InstrumentResponse;
-pub const InstrumentResponseRow = instrument_response.InstrumentResponseRow;
-pub const LineContributions = line_contributions.LineContributions;
-pub const LineContributionRow = line_contributions.LineContributionRow;
-pub const CiaDiagnostics = cia_diagnostics.CiaDiagnostics;
-pub const CiaRow = cia_diagnostics.CiaRow;
pub const Spectrum = output_spectrum.Spectrum;
pub const SpectrumRunResult = output_spectrum.SpectrumRunResult;
pub const SpectrumRunSummary = output_spectrum.SpectrumRunSummary;
@@ -78,10 +70,10 @@ pub const jacobian_state_count = jacobian_states.state_count;
pub const parseSceneJson = input_json.parseSceneJson;
pub const buildRunTables = setup_tables.buildRunTables;
-pub const buildProfileLineValues = profile_lines.buildProfileLineValues;
+pub const buildProfileLineValues = profile_line_build.buildProfileLineValues;
// Prepared ---------------------------------------------------------------------------------------------------|
-// Public owner for parsed or caller-provided controls and setup tables. |
+// Public owner for parsed or caller-provided controls and setup tables. |
// |
// layout(64-bit) |
// size: 2728 B (2.664 KiB), align: 8 |
@@ -91,14 +83,14 @@ pub const buildProfileLineValues = profile_lines.buildProfileLineValues;
// [ 704..2727] tables: RunTables |
// |
// referenced storage |
-// scene borrows control strings/slices. tables owns loaded physical setup arrays and scalar tables. |
+// scene borrows control strings/slices. tables owns loaded physical setup arrays and scalar tables. |
pub const Prepared = struct {
scene: Scene,
tables: RunTables,
pub fn deinit(self: *Prepared, allocator: Allocator) void {
// Prepared.deinit ------------------------------------------------------------------------------------|
- // Release setup tables; scene strings and slices are borrowed from caller-owned storage. |
+ // Release setup tables; scene strings and slices are borrowed from caller-owned storage. |
// ----------------------------------------------------------------------------------------------------|
self.tables.deinit(allocator);
self.* = undefined;
@@ -108,14 +100,14 @@ pub const Prepared = struct {
pub fn initSessionMemory(allocator: Allocator) SessionMemory {
// initSessionMemory --------------------------------------------------------------------------------------|
- // Create an empty reusable session cache. |
+ // Create an empty reusable session cache. |
// --------------------------------------------------------------------------------------------------------|
return SessionMemory.init(allocator);
}
pub fn prepare(allocator: Allocator, scene: Scene) !Prepared {
// prepare ------------------------------------------------------------------------------------------------|
- // Build the setup tables retained across forward runs. |
+ // Build the setup tables retained across forward runs. |
// --------------------------------------------------------------------------------------------------------|
return .{
.scene = scene,
@@ -130,7 +122,7 @@ pub fn warmSessionMemory(
solve_config: SolveConfig,
) !void {
// warmSessionMemory --------------------------------------------------------------------------------------|
- // Materialize reusable spectrum, radiance, profile-line, solar, and transport memory for one scene. |
+ // Materialize reusable spectrum, radiance, profile-line, solar, and transport memory for one scene. |
// --------------------------------------------------------------------------------------------------------|
const prepared_solve_config = try controls.prepareSolveConfig(solve_config);
_ = try prepareSessionRows(allocator, session, prepared, prepared_solve_config);
@@ -143,7 +135,7 @@ pub fn runForwardWithSessionMemory(
solve_config: SolveConfig,
) !SpectrumRunResult {
// runForwardWithSessionMemory ----------------------------------------------------------------------------|
- // Run the product-grid spectrum through caller-retained session memory and return owned arrays. |
+ // Run the product-grid spectrum through caller-retained session memory and return owned arrays. |
// |
// Root-level orchestration follows the integrated transport route in |
// `tests/unit/spectrum/spectrum_run_test.zig` |
@@ -181,7 +173,7 @@ pub fn runForwardWithSessionMemory(
// runForward sampling policy -----------------------------------------------------------------------------|
// Canonical expected values owned by this repository. |
- // product row using integrated radiance and integrated irradiance sampling. |
+ // product row using integrated radiance and integrated irradiance sampling. |
// `evidence/python-reference-case-native.json` exposes no Python-native key for calibration arrays, slit |
// kernels, or per-channel integration overrides. Keep this policy fixed here; |
// user-configurable controls enter through Scene JSON and `solveConfig`. |
@@ -230,7 +222,7 @@ pub fn runForwardWithSessionMemory(
pub fn runForward(allocator: Allocator, prepared: *const Prepared, solve_config: SolveConfig) !SpectrumRunResult {
// runForward ---------------------------------------------------------------------------------------------|
- // Run one spectrum with a short-lived session memory owner. |
+ // Run one spectrum with a short-lived session memory owner. |
// --------------------------------------------------------------------------------------------------------|
var session = initSessionMemory(allocator);
defer session.deinit(allocator);
@@ -248,7 +240,7 @@ pub fn runOptimalEstimation(
retrieval_controls: retrieval.Controls,
) !retrieval.Result {
// runOptimalEstimation -----------------------------------------------------------------------------------|
- // Run one full-physics optimal-estimation solve through the explicit forward path. |
+ // Run one full-physics optimal-estimation solve through the explicit forward path. |
// |
// steps |
// 1. copy measurement rows into dense retrieval SoA storage |
@@ -403,7 +395,7 @@ pub fn runOptimalEstimationCorrection(
retrieval_controls: retrieval.Controls,
) !retrieval.Result {
// runOptimalEstimationCorrection --------------------------------------------------------------------- |
- // Apply one full-physics correction step to a prepared scene already written at the fast-stage state. |
+ // Apply one full-physics correction step to a prepared scene already written at the fast-stage state. |
// |
// contract |
// The correction path returns exactly one Rodgers update. Controls still validate solver thresholds, |
@@ -540,8 +532,8 @@ pub fn runOptimalEstimationBatch(
// Run a correctness-first full-physics OE batch over caller-provided start/prior rows. |
// |
// boundary |
- // This is the single-worker batch slice. Each start reuses the same public measurement rows and |
- // prepared scene, then copies compact result rows into the run-major BatchResult owner. |
+ // This is the single-worker batch slice. Each start reuses the same public measurement rows and |
+ // prepared scene, then copies compact result rows into the run-major BatchResult owner. |
// |
// failure model |
// OutOfMemory aborts the whole batch. Numerical or control failures mark only that start as failed, |
@@ -634,7 +626,7 @@ pub fn runFastmodeOptimalEstimationBatch(
// Run fast-stage starts, then run a full-physics correction batch seeded from the fast-stage states. |
// |
// boundary |
- // Python owns the fast and correction scene construction. Zig receives two prepared scenes, two |
+ // Python owns the fast and correction scene construction. Zig receives two prepared scenes, two |
// measurement grids, and config-driven controls; no wavelength counts are hardcoded here. |
// |
// converged flags keep fast-stage convergence when the correction stage returns ok. |
@@ -767,57 +759,14 @@ pub fn buildAtmosphericBudget(
wavelengths_nm: []const f64,
) !AtmosphericBudget {
// buildAtmosphericBudget ---------------------------------------------------------------------------------|
- // Build public atmospheric support-row diagnostic rows for the prepared scene. |
+ // Build public atmospheric support-row diagnostic rows for the prepared scene. |
// --------------------------------------------------------------------------------------------------------|
return atmospheric_budget.build(allocator, prepared.scene, &prepared.tables, wavelengths_nm);
}
-pub fn buildCiaDiagnostics(
- allocator: Allocator,
- prepared: *const Prepared,
- wavelengths_nm: []const f64,
-) !CiaDiagnostics {
- // buildCiaDiagnostics ------------------------------------------------------------------------------------|
- // Build public O2-O2 CIA diagnostic rows from atmospheric-budget support rows. |
- // --------------------------------------------------------------------------------------------------------|
- var budget = try buildAtmosphericBudget(allocator, prepared, wavelengths_nm);
- defer budget.deinit(allocator);
- return cia_diagnostics.build(allocator, budget);
-}
-
-pub fn buildLineContributions(
- allocator: Allocator,
- prepared: *const Prepared,
- wavelengths_nm: []const f64,
- max_rows: usize,
-) !LineContributions {
- // buildLineContributions ---------------------------------------------------------------------------------|
- // Build public O2 line-by-line diagnostic rows for caller-selected wavelengths. |
- // --------------------------------------------------------------------------------------------------------|
- return line_contributions.build(allocator, &prepared.tables, wavelengths_nm, max_rows);
-}
-
-pub fn buildInstrumentResponse(
- allocator: Allocator,
- prepared: *const Prepared,
- wavelengths_nm: []const f64,
- channel_mask: u32,
-) !InstrumentResponse {
- // buildInstrumentResponse --------------------------------------------------------------------------------|
- // Build public instrument-response support rows for caller-selected wavelengths and channels. |
- // --------------------------------------------------------------------------------------------------------|
- return instrument_response.build(
- allocator,
- prepared.scene,
- &prepared.tables,
- wavelengths_nm,
- channel_mask,
- );
-}
-
pub fn solveConfig(scene: Scene) SolveConfig {
// solveConfig --------------------------------------------------------------------------------------------|
- // Build the exercised transport controls used by integrated transport evidence. |
+ // Build the exercised transport controls used by integrated transport evidence. |
// --------------------------------------------------------------------------------------------------------|
const performance_thresholds = performanceThresholdsWithFourierLimit(scene.rtm);
return .{
@@ -879,7 +828,7 @@ fn evaluateRetrievalState(
// |
// boundary |
// The retrieval driver owns one retained LayerGrid. This function refills its computed rows for the |
- // current state while borrowing line, CIA, phase, instrument, and solar tables from the prepared scene. |
+ // current state while borrowing line, CIA, phase, instrument, and solar tables from the prepared scene. |
// |
// refresh |
// aerosol optical depth : rebuild inline AerosolLayerTable |
@@ -920,7 +869,7 @@ fn writeRetrievalStateToScene(
state: retrieval.StateVector,
) !void {
// writeRetrievalStateToScene -----------------------------------------------------------------------------|
- // Write active OE scalar values into the scene copy used for this iteration. |
+ // Write active OE scalar values into the scene copy used for this iteration. |
// |
// pressure placement |
// target interval takes the new aerosol top/bottom pressures, the adjacent interval boundaries move |
@@ -1045,7 +994,7 @@ fn prepareSessionRows(
// Both mode bits remain in the reuse stamp so future diagnostic or temperature-profile paths split caches.
const build_layer_values = false;
const needs_temperature_derivatives = false;
- const profile_stamp = profile_lines.profileLineReuseStamp(
+ const profile_stamp = profile_line_build.profileLineReuseStamp(
prepared.scene.id,
prepared.tables.lines,
prepared.tables.layers.spectroscopy_profile.rows,
@@ -1088,7 +1037,7 @@ fn prepareSessionRows(
session.profile_lines.deinit(allocator);
session.profile_lines =
- try profile_lines.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ try profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
allocator,
prepared.scene,
exact_wavelengths,
diff --git a/tests/unit/cache/profile_line_memory_test.zig b/tests/unit/cache/profile_line_memory_test.zig
index b35944a3..ea597dac 100644
--- a/tests/unit/cache/profile_line_memory_test.zig
+++ b/tests/unit/cache/profile_line_memory_test.zig
@@ -29,11 +29,29 @@ fn supportProfileLineSigma(
return values.supportProfileTotalSigmaAt(wavelength_index, profile_node_index);
}
+test "BENCH profile-line build same-boundary timing" {
+ if (builtin.mode == .Debug) return error.SkipZigTest;
+ const allocator = std.testing.allocator;
+ var best_ns: u64 = std.math.maxInt(u64);
+ var iteration: usize = 0;
+ while (iteration < 9) : (iteration += 1) {
+ var timer = try std.time.Timer.start();
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
+ allocator,
+ o2a_scene.reference(),
+ );
+ const elapsed_ns = timer.read();
+ values.deinit(allocator);
+ if (elapsed_ns < best_ns) best_ns = elapsed_ns;
+ }
+ std.debug.print("\n[BENCH] buildProfileLineValues reference best = {d} us\n", .{best_ns / 1000});
+}
+
test "ProfileLineValues keep wavelength-major line values for each layer node" {
var scene = o2a_scene.reference();
scene.spectral_grid.sample_count = profile_line_test_sample_count;
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
scene,
);
@@ -76,7 +94,7 @@ test "ProfileLineValues contain computed finite weak-line sigma rows" {
var scene = o2a_scene.reference();
scene.spectral_grid.sample_count = profile_line_test_sample_count;
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
scene,
);
@@ -99,7 +117,7 @@ test "ProfileLineValues contain computed finite weak-line sigma rows" {
test "ProfileLineValues build the full reference wavelength route in optimized mode" {
if (builtin.mode == .Debug) return error.SkipZigTest;
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
o2a_scene.reference(),
);
@@ -128,7 +146,7 @@ test "ProfileLineValues match profile-node line math evidence" {
.sample_count = 1,
};
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
scene,
);
@@ -172,7 +190,7 @@ test "ProfileLineValues match profile-node total line sidecar evidence" {
.sample_count = 1,
};
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
scene,
);
@@ -208,7 +226,7 @@ test "ProfileLineValues preserve caller-provided exact wavelength order" {
if (builtin.mode == .Debug) return error.SkipZigTest;
const wavelengths_nm = [_]f64{ 760.0, 758.0, 776.0 };
- var values = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengths(
+ var values = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengths(
std.testing.allocator,
o2a_scene.reference(),
wavelengths_nm[0..],
@@ -260,7 +278,7 @@ test "ProfileLineValues reuse stamp distinguishes line assets" {
defer tables.deinit(allocator);
const wavelengths_nm = [_]f64{760.0};
- const baseline = internal.cache.profile_line_memory.profileLineReuseStamp(
+ const baseline = internal.cache.profile_line_build.profileLineReuseStamp(
scene.id,
tables.lines,
tables.layers.spectroscopy_profile.rows,
@@ -274,7 +292,7 @@ test "ProfileLineValues reuse stamp distinguishes line assets" {
isotope_scene.line_gas.isotopes_sim = isotope_two[0..];
var isotope_tables = try internal.setup.run_tables.buildRunTables(allocator, isotope_scene);
defer isotope_tables.deinit(allocator);
- const isotope_stamp = internal.cache.profile_line_memory.profileLineReuseStamp(
+ const isotope_stamp = internal.cache.profile_line_build.profileLineReuseStamp(
scene.id,
isotope_tables.lines,
isotope_tables.layers.spectroscopy_profile.rows,
@@ -294,7 +312,7 @@ test "ProfileLineValues reuse stamp distinguishes spectroscopy profile thermodyn
defer tables.deinit(allocator);
const wavelengths_nm = [_]f64{760.0};
- const baseline = internal.cache.profile_line_memory.profileLineReuseStamp(
+ const baseline = internal.cache.profile_line_build.profileLineReuseStamp(
scene.id,
tables.lines,
tables.layers.spectroscopy_profile.rows,
@@ -310,7 +328,7 @@ test "ProfileLineValues reuse stamp distinguishes spectroscopy profile thermodyn
defer allocator.free(changed_rows);
changed_rows[0].temperature_k += 1.0;
- const changed = internal.cache.profile_line_memory.profileLineReuseStamp(
+ const changed = internal.cache.profile_line_build.profileLineReuseStamp(
scene.id,
tables.lines,
changed_rows,
@@ -327,7 +345,7 @@ test "ProfileLineValues disable strong-line sidecars when isotope one is inactiv
var scene = o2a_scene.reference();
scene.line_gas.isotopes_sim = isotope_two[0..];
- var values = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengths(
+ var values = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengths(
std.testing.allocator,
scene,
wavelengths_nm[0..],
@@ -350,7 +368,7 @@ test "ProfileLineValues parallel wavelength build matches serial rows" {
const allocator = std.testing.allocator;
const wavelengths_nm = [_]f64{ 758.0, 760.0, 765.0, 767.0, 776.0 };
- var serial = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ var serial = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
allocator,
o2a_scene.reference(),
wavelengths_nm[0..],
@@ -363,7 +381,7 @@ test "ProfileLineValues parallel wavelength build matches serial rows" {
);
defer serial.deinit(allocator);
- var parallel = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ var parallel = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
allocator,
o2a_scene.reference(),
wavelengths_nm[0..],
@@ -391,7 +409,7 @@ test "ProfileLineValues retain only output rows on the caller allocator" {
if (builtin.mode == .Debug) return error.SkipZigTest;
const wavelengths_nm = [_]f64{ 758.0, 760.0 };
- var expected = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ var expected = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
std.testing.allocator,
o2a_scene.reference(),
wavelengths_nm[0..],
@@ -406,7 +424,7 @@ test "ProfileLineValues retain only output rows on the caller allocator" {
var counter = CountingAllocator.init(std.testing.allocator);
const counting_allocator = counter.allocator();
- var actual = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ var actual = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
counting_allocator,
o2a_scene.reference(),
wavelengths_nm[0..],
@@ -453,7 +471,7 @@ test "ProfileLineValues fill support-row sigma from atmospheric-budget evidence"
.end_nm = expected.wavelength_nm,
.sample_count = 1,
};
- var values = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var values = try internal.cache.profile_line_build.buildProfileLineValues(
std.testing.allocator,
scene,
);
diff --git a/tests/unit/output/cia_diagnostics_test.zig b/tests/unit/output/cia_diagnostics_test.zig
deleted file mode 100644
index 98726f45..00000000
--- a/tests/unit/output/cia_diagnostics_test.zig
+++ /dev/null
@@ -1,194 +0,0 @@
-const std = @import("std");
-const internal = @import("internal");
-const o2a_scene = @import("../support/o2a_scene.zig");
-
-const cia_diagnostics = internal.output.cia_diagnostics;
-
-const allocator = std.testing.allocator;
-
-// RowEvidence ------------------------------------------------------------------------------------------------|
-// Test-local public O2-O2 CIA evidence from baseline artifact: |
-// Canonical expected values owned by this repository. |
-// public-python-baseline.json .diagnostics.collision_induced_absorption.rows. |
-// |
-// layout(64-bit) |
-// size: 120 B (0.117 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] row_index: usize |
-// [ 8..119] row : CiaRow |
-const RowEvidence = struct {
- row_index: usize,
- row: cia_diagnostics.CiaRow,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-const expected_cia_rows = [_]RowEvidence{
- .{
- .row_index = 0,
- .row = .{
- .wavelength_nm = 758.0,
- .layer_index = 0,
- .sublayer_index = 0,
- .global_sublayer_index = 0,
- .interval_index_1based = 1,
- .altitude_km = -3.636916272619225e-05,
- .pressure_hpa = 1013.2499974119982,
- .temperature_k = 294.20205620757804,
- .oxygen_number_density_cm3 = 5.224995167106018e18,
- .path_length_cm = 0.0,
- .cia_cross_section_cm5_per_molecule2 = 0.0,
- .cia_optical_depth = 0.0,
- .total_absorption_optical_depth = 0.0,
- .total_optical_depth = 0.0,
- .cia_share_of_total_absorption = 0.0,
- .cia_share_of_total_optical_depth = 0.0,
- },
- },
- .{
- .row_index = 1,
- .row = .{
- .wavelength_nm = 758.0,
- .layer_index = 0,
- .sublayer_index = 0,
- .global_sublayer_index = 1,
- .interval_index_1based = 1,
- .altitude_km = 0.007506658210811699,
- .pressure_hpa = 1012.3624132494756,
- .temperature_k = 294.19449473014873,
- .oxygen_number_density_cm3 = 5.220552365887917e18,
- .path_length_cm = 1889.5354494827166,
- .cia_cross_section_cm5_per_molecule2 = 2.4616634207532467e-46,
- .cia_optical_depth = 1.26770040534369e-05,
- .total_absorption_optical_depth = 2.7504873904415636e-05,
- .total_optical_depth = 8.526373793855298e-05,
- .cia_share_of_total_absorption = 0.46090027889208873,
- .cia_share_of_total_optical_depth = 0.14867990027100192,
- },
- },
- .{
- .row_index = 227,
- .row = .{
- .wavelength_nm = 760.0,
- .layer_index = 0,
- .sublayer_index = 0,
- .global_sublayer_index = 1,
- .interval_index_1based = 1,
- .altitude_km = 0.007506658210811699,
- .pressure_hpa = 1012.3624132494756,
- .temperature_k = 294.19449473014873,
- .oxygen_number_density_cm3 = 5.220552365887917e18,
- .path_length_cm = 1889.5354494827166,
- .cia_cross_section_cm5_per_molecule2 = 4.501936434839764e-46,
- .cia_optical_depth = 2.318394381280432e-05,
- .total_absorption_optical_depth = 0.0038742877697214785,
- .total_optical_depth = 0.00393143472191721,
- .cia_share_of_total_absorption = 0.005984053119128785,
- .cia_share_of_total_optical_depth = 0.005897069505836383,
- },
- },
- .{
- .row_index = 905,
- .row = .{
- .wavelength_nm = 776.0,
- .layer_index = 0,
- .sublayer_index = 0,
- .global_sublayer_index = 1,
- .interval_index_1based = 1,
- .altitude_km = 0.007506658210811699,
- .pressure_hpa = 1012.3624132494756,
- .temperature_k = 294.19449473014873,
- .oxygen_number_density_cm3 = 5.220552365887917e18,
- .path_length_cm = 1889.5354494827166,
- .cia_cross_section_cm5_per_molecule2 = 1.082325063786383e-47,
- .cia_optical_depth = 5.57372673497254e-07,
- .total_absorption_optical_depth = 1.092679720354939e-06,
- .total_optical_depth = 5.3625399544667935e-05,
- .cia_share_of_total_absorption = 0.5100970239625209,
- .cia_share_of_total_optical_depth = 0.010393818567878148,
- },
- },
- .{
- .row_index = 1129,
- .row = .{
- .wavelength_nm = 776.0,
- .layer_index = 44,
- .sublayer_index = 4,
- .global_sublayer_index = 225,
- .interval_index_1based = 3,
- .altitude_km = 59.30427248486187,
- .pressure_hpa = 0.3000000002509619,
- .temperature_k = 259.2405337021464,
- .oxygen_number_density_cm3 = 1755631375691464.0,
- .path_length_cm = 0.0,
- .cia_cross_section_cm5_per_molecule2 = 0.0,
- .cia_optical_depth = 0.0,
- .total_absorption_optical_depth = 0.0,
- .total_optical_depth = 0.0,
- .cia_share_of_total_absorption = 0.0,
- .cia_share_of_total_optical_depth = 0.0,
- },
- },
-};
-
-test "O2-O2 CIA diagnostics match public Python evidence at probe wavelengths" {
- var prepared = try internal.public.prepare(allocator, o2a_scene.reference());
- defer prepared.deinit(allocator);
-
- const wavelengths_nm = [_]f64{ 758.0, 760.0, 765.0, 767.0, 776.0 };
- var diagnostics = try internal.public.buildCiaDiagnostics(
- allocator,
- &prepared,
- wavelengths_nm[0..],
- );
- defer diagnostics.deinit(allocator);
-
- try std.testing.expectEqual(@as(usize, 1130), diagnostics.rows.len);
- try std.testing.expectEqual(@as(usize, 226), diagnostics.rows.len / wavelengths_nm.len);
-
- for (expected_cia_rows) |expected| {
- try expectRowEqual(expected.row, diagnostics.rows[expected.row_index]);
- }
-}
-
-fn expectRowEqual(expected: cia_diagnostics.CiaRow, actual: cia_diagnostics.CiaRow) !void {
- // expectRowEqual -----------------------------------------------------------------------------------------|
- // Compare one CIA diagnostic row field-by-field so drift points at the first diverging column. |
- // --------------------------------------------------------------------------------------------------------|
- try expectF64Bits(expected.wavelength_nm, actual.wavelength_nm);
- try std.testing.expectEqual(expected.layer_index, actual.layer_index);
- try std.testing.expectEqual(expected.sublayer_index, actual.sublayer_index);
- try std.testing.expectEqual(expected.global_sublayer_index, actual.global_sublayer_index);
- try std.testing.expectEqual(expected.interval_index_1based, actual.interval_index_1based);
- try expectF64Bits(expected.altitude_km, actual.altitude_km);
- try expectF64Bits(expected.pressure_hpa, actual.pressure_hpa);
- try expectF64Bits(expected.temperature_k, actual.temperature_k);
- try expectF64Bits(expected.oxygen_number_density_cm3, actual.oxygen_number_density_cm3);
- try expectF64Bits(expected.path_length_cm, actual.path_length_cm);
- try expectOpticalDepthUlp(expected.cia_cross_section_cm5_per_molecule2, actual.cia_cross_section_cm5_per_molecule2);
- try expectOpticalDepthUlp(expected.cia_optical_depth, actual.cia_optical_depth);
- try expectOpticalDepthUlp(expected.total_absorption_optical_depth, actual.total_absorption_optical_depth);
- try expectOpticalDepthUlp(expected.total_optical_depth, actual.total_optical_depth);
- try expectOpticalDepthUlp(expected.cia_share_of_total_absorption, actual.cia_share_of_total_absorption);
- try expectOpticalDepthUlp(expected.cia_share_of_total_optical_depth, actual.cia_share_of_total_optical_depth);
-}
-
-fn expectF64Bits(expected: f64, actual: f64) !void {
- // expectF64Bits ------------------------------------------------------------------------------------------|
- // Enforce exact f64 bits for JSON values that round-trip without local derived math. |
- // --------------------------------------------------------------------------------------------------------|
- const expected_bits: u64 = @bitCast(expected);
- const actual_bits: u64 = @bitCast(actual);
- try std.testing.expectEqual(expected_bits, actual_bits);
-}
-
-fn expectOpticalDepthUlp(expected: f64, actual: f64) !void {
- // expectOpticalDepthUlp ----------------------------------------------------------------------------------|
- // CIA optical columns inherit the atmospheric-budget one-ULP product-order exception and add only the |
- // canonical ratio projection. Larger drift still fails deterministically. |
- // --------------------------------------------------------------------------------------------------------|
- const expected_bits: u64 = @bitCast(expected);
- const actual_bits: u64 = @bitCast(actual);
- const ulp_delta = if (expected_bits > actual_bits) expected_bits - actual_bits else actual_bits - expected_bits;
- try std.testing.expect(ulp_delta <= 1);
-}
diff --git a/tests/unit/output/instrument_response_test.zig b/tests/unit/output/instrument_response_test.zig
deleted file mode 100644
index e7bf91c4..00000000
--- a/tests/unit/output/instrument_response_test.zig
+++ /dev/null
@@ -1,271 +0,0 @@
-const std = @import("std");
-const internal = @import("internal");
-const o2a_scene = @import("../support/o2a_scene.zig");
-
-const instrument_response = internal.output.instrument_response;
-
-const allocator = std.testing.allocator;
-
-// RowEvidence ------------------------------------------------------------------------------------------------|
-// `rtm.instrument_response(o2a.reference_scene(), [758.0, 760.0, 765.0, 767.0, 776.0])`. |
-// The public artifact records no instrument-response rows, so this canonical probe is the source oracle. |
-// |
-// layout(64-bit) |
-// size: 104 B (0.102 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] row_index: usize |
-// [ 8..103] row : InstrumentResponseRow |
-const RowEvidence = struct {
- row_index: usize,
- row: instrument_response.InstrumentResponseRow,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-const expected_rows = [_]RowEvidence{
- .{
- .row_index = 0,
- .row = .{
- .nominal_index = 100,
- .nominal_wavelength_nm = 758.0,
- .channel = 0,
- .sample_index = 0,
- .support_count = 242,
- .offset_nm = -1.1421219262290379,
- .support_wavelength_nm = 756.857878073771,
- .weight = 0.0,
- .support_width_nm = 2.288798250087325,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 1,
- .row = .{
- .nominal_index = 100,
- .nominal_wavelength_nm = 758.0,
- .channel = 0,
- .sample_index = 1,
- .support_count = 242,
- .offset_nm = -1.1333236761416856,
- .support_wavelength_nm = 756.8666763238583,
- .weight = 0.0,
- .support_width_nm = 2.288798250087325,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 241,
- .row = .{
- .nominal_index = 100,
- .nominal_wavelength_nm = 758.0,
- .channel = 0,
- .sample_index = 241,
- .support_count = 242,
- .offset_nm = 1.146676323858287,
- .support_wavelength_nm = 759.1466763238583,
- .weight = 0.0,
- .support_width_nm = 2.288798250087325,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 242,
- .row = .{
- .nominal_index = 100,
- .nominal_wavelength_nm = 758.0,
- .channel = 1,
- .sample_index = 0,
- .support_count = 242,
- .offset_nm = -1.1421219262290379,
- .support_wavelength_nm = 756.857878073771,
- .weight = 0.0,
- .support_width_nm = 2.288798250087325,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 2231,
- .row = .{
- .nominal_index = 167,
- .nominal_wavelength_nm = 760.0,
- .channel = 1,
- .sample_index = 690,
- .support_count = 1057,
- .offset_nm = 0.20390806762350167,
- .support_wavelength_nm = 760.2039080676235,
- .weight = 0.0013061505693578597,
- .support_width_nm = 2.2917266303010138,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 2232,
- .row = .{
- .nominal_index = 167,
- .nominal_wavelength_nm = 760.0,
- .channel = 1,
- .sample_index = 691,
- .support_count = 1057,
- .offset_nm = 0.20515891946615739,
- .support_wavelength_nm = 760.2051589194662,
- .weight = 0.001276744801798927,
- .support_width_nm = 2.2917266303010138,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 4456,
- .row = .{
- .nominal_index = 700,
- .nominal_wavelength_nm = 776.0,
- .channel = 1,
- .sample_index = 199,
- .support_count = 207,
- .offset_nm = 0.7916922621676576,
- .support_wavelength_nm = 776.7916922621677,
- .weight = 3.975190843504198e-93,
- .support_width_nm = 1.9669824805035887,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
- .{
- .row_index = 4463,
- .row = .{
- .nominal_index = 700,
- .nominal_wavelength_nm = 776.0,
- .channel = 1,
- .sample_index = 206,
- .support_count = 207,
- .offset_nm = 0.8246811031543757,
- .support_wavelength_nm = 776.8246811031544,
- .weight = 3.27853995787421e-110,
- .support_width_nm = 1.9669824805035887,
- .instrument_fwhm_nm = 0.38,
- .high_resolution_step_nm = 0.01,
- .high_resolution_half_span_nm = 1.14,
- .integration_mode = 2,
- .response_enabled = 1,
- },
- },
-};
-
-test "instrument response rows match public route at probe wavelengths" {
- var prepared = try internal.public.prepare(allocator, o2a_scene.reference());
- defer prepared.deinit(allocator);
-
- const wavelengths_nm = [_]f64{ 758.0, 760.0, 765.0, 767.0, 776.0 };
- var response = try internal.public.buildInstrumentResponse(
- allocator,
- &prepared,
- wavelengths_nm[0..],
- instrument_response.channel_mask_radiance | instrument_response.channel_mask_irradiance,
- );
- defer response.deinit(allocator);
-
- try std.testing.expectEqual(@as(usize, 4464), response.rows.len);
-
- for (expected_rows) |expected| {
- try expectRowEqual(expected.row, response.rows[expected.row_index]);
- }
-}
-
-test "instrument response channel mask selects one public channel" {
- var prepared = try internal.public.prepare(allocator, o2a_scene.reference());
- defer prepared.deinit(allocator);
-
- const wavelengths_nm = [_]f64{ 758.0, 760.0, 765.0, 767.0, 776.0 };
- var radiance = try internal.public.buildInstrumentResponse(
- allocator,
- &prepared,
- wavelengths_nm[0..],
- instrument_response.channel_mask_radiance,
- );
- defer radiance.deinit(allocator);
- var irradiance = try internal.public.buildInstrumentResponse(
- allocator,
- &prepared,
- wavelengths_nm[0..],
- instrument_response.channel_mask_irradiance,
- );
- defer irradiance.deinit(allocator);
-
- try std.testing.expectEqual(@as(usize, 2232), radiance.rows.len);
- try std.testing.expectEqual(@as(usize, 2232), irradiance.rows.len);
- try std.testing.expectEqual(@as(u32, 0), radiance.rows[0].channel);
- try std.testing.expectEqual(@as(u32, 1), irradiance.rows[0].channel);
-}
-
-test "instrument response rejects empty and unsupported channel masks" {
- var prepared = try internal.public.prepare(allocator, o2a_scene.reference());
- defer prepared.deinit(allocator);
-
- const wavelengths_nm = [_]f64{758.0};
- try std.testing.expectError(
- error.InvalidControl,
- internal.public.buildInstrumentResponse(allocator, &prepared, wavelengths_nm[0..], 0),
- );
- try std.testing.expectError(
- error.InvalidControl,
- internal.public.buildInstrumentResponse(allocator, &prepared, wavelengths_nm[0..], 4),
- );
-}
-
-fn expectRowEqual(
- expected: instrument_response.InstrumentResponseRow,
- actual: instrument_response.InstrumentResponseRow,
-) !void {
- // expectRowEqual -----------------------------------------------------------------------------------------|
- // Compare one public instrument-response row field-by-field so drift names the first diverging column. |
- // --------------------------------------------------------------------------------------------------------|
- try std.testing.expectEqual(expected.nominal_index, actual.nominal_index);
- try expectF64Bits(expected.nominal_wavelength_nm, actual.nominal_wavelength_nm);
- try std.testing.expectEqual(expected.channel, actual.channel);
- try std.testing.expectEqual(expected.sample_index, actual.sample_index);
- try std.testing.expectEqual(expected.support_count, actual.support_count);
- try expectF64Bits(expected.offset_nm, actual.offset_nm);
- try expectF64Bits(expected.support_wavelength_nm, actual.support_wavelength_nm);
- try expectF64Bits(expected.weight, actual.weight);
- try expectF64Bits(expected.support_width_nm, actual.support_width_nm);
- try expectF64Bits(expected.instrument_fwhm_nm, actual.instrument_fwhm_nm);
- try expectF64Bits(expected.high_resolution_step_nm, actual.high_resolution_step_nm);
- try expectF64Bits(expected.high_resolution_half_span_nm, actual.high_resolution_half_span_nm);
- try std.testing.expectEqual(expected.integration_mode, actual.integration_mode);
- try std.testing.expectEqual(expected.response_enabled, actual.response_enabled);
-}
-
-fn expectF64Bits(expected: f64, actual: f64) !void {
- // expectF64Bits ------------------------------------------------------------------------------------------|
- // Enforce exact f64 bits for canonical probe values that are copied from kernel rows without new math. |
- // --------------------------------------------------------------------------------------------------------|
- const expected_bits: u64 = @bitCast(expected);
- const actual_bits: u64 = @bitCast(actual);
- try std.testing.expectEqual(expected_bits, actual_bits);
-}
diff --git a/tests/unit/output/line_contributions_test.zig b/tests/unit/output/line_contributions_test.zig
deleted file mode 100644
index 4e824df3..00000000
--- a/tests/unit/output/line_contributions_test.zig
+++ /dev/null
@@ -1,246 +0,0 @@
-const std = @import("std");
-const internal = @import("internal");
-const o2a_scene = @import("../support/o2a_scene.zig");
-
-const line_contributions = internal.output.line_contributions;
-
-const allocator = std.testing.allocator;
-
-// RowEvidence ------------------------------------------------------------------------------------------------|
-// Test-local O2 line-contribution evidence from baseline artifact: |
-// Canonical expected values owned by this repository. |
-// public-python-baseline.json .diagnostics.line_contributions.rows. |
-// |
-// layout(64-bit) |
-// size: 176 B (0.172 KiB), align: 8 B |
-// |
-// memory |
-// [ 0.. 7] row_index: usize |
-// [ 8..175] row : LineContributionRow |
-const RowEvidence = struct {
- row_index: usize,
- row: line_contributions.LineContributionRow,
-};
-// ------------------------------------------------------------------------------------------------------------|
-
-const expected_rows = [_]RowEvidence{
- .{
- .row_index = 0,
- .row = .{
- .wavelength_nm = 758.0,
- .profile_node_index = 0,
- .altitude_km = -0.30687417130268846,
- .row_kind = .weak_line,
- .status = .weak_included,
- .line_index = 326,
- .strong_line_index = 4294967295,
- .matched_strong_line_index = 4294967295,
- .gas_index = 7,
- .isotope_number = 1,
- .isotopologue_code = 66,
- .center_wavelength_nm = 759.5754324317322,
- .center_wavenumber_cm1 = 13165.249392,
- .shifted_center_wavenumber_cm1 = 13165.2397546943,
- .line_strength_cm2_per_molecule = 1.098e-25,
- .air_half_width_cm1 = 0.0312,
- .pressure_shift_cm1 = -0.0093,
- .lower_state_energy_cm1 = 1245.9725,
- .temperature_k = 294.2,
- .pressure_hpa = 1050.0,
- .weak_line_sigma_cm2_per_molecule = 1.470550359845784e-30,
- .strong_line_sigma_cm2_per_molecule = 0.0,
- .line_mixing_sigma_cm2_per_molecule = 0.0,
- .total_sigma_cm2_per_molecule = 1.470550359845784e-30,
- .abs_total_sigma_cm2_per_molecule = 1.470550359845784e-30,
- },
- },
- .{
- .row_index = 1,
- .row = .{
- .wavelength_nm = 758.0,
- .profile_node_index = 0,
- .altitude_km = -0.30687417130268846,
- .row_kind = .weak_line,
- .status = .weak_included,
- .line_index = 327,
- .strong_line_index = 4294967295,
- .matched_strong_line_index = 4294967295,
- .gas_index = 7,
- .isotope_number = 3,
- .isotopologue_code = 67,
- .center_wavelength_nm = 759.5802034166419,
- .center_wavenumber_cm1 = 13165.1667,
- .shifted_center_wavenumber_cm1 = 13165.1570626943,
- .line_strength_cm2_per_molecule = 5.256e-29,
- .air_half_width_cm1 = 0.0312,
- .pressure_shift_cm1 = -0.0093,
- .lower_state_energy_cm1 = 1210.0667,
- .temperature_k = 294.2,
- .pressure_hpa = 1050.0,
- .weak_line_sigma_cm2_per_molecule = 7.0050107314408325e-34,
- .strong_line_sigma_cm2_per_molecule = 0.0,
- .line_mixing_sigma_cm2_per_molecule = 0.0,
- .total_sigma_cm2_per_molecule = 7.0050107314408325e-34,
- .abs_total_sigma_cm2_per_molecule = 7.0050107314408325e-34,
- },
- },
- .{
- .row_index = 500,
- .row = .{
- .wavelength_nm = 758.0,
- .profile_node_index = 1,
- .altitude_km = 0.0020875793966169676,
- .row_kind = .weak_line,
- .status = .weak_included,
- .line_index = 394,
- .strong_line_index = 4294967295,
- .matched_strong_line_index = 4294967295,
- .gas_index = 7,
- .isotope_number = 1,
- .isotopologue_code = 66,
- .center_wavelength_nm = 759.7951775403164,
- .center_wavenumber_cm1 = 13161.441788,
- .shifted_center_wavenumber_cm1 = 13161.431790467308,
- .line_strength_cm2_per_molecule = 3.378e-27,
- .air_half_width_cm1 = 0.0255,
- .pressure_shift_cm1 = -0.01,
- .lower_state_energy_cm1 = 2010.5953,
- .temperature_k = 294.2,
- .pressure_hpa = 1013.0,
- .weak_line_sigma_cm2_per_molecule = 2.688168394826091e-32,
- .strong_line_sigma_cm2_per_molecule = 0.0,
- .line_mixing_sigma_cm2_per_molecule = 0.0,
- .total_sigma_cm2_per_molecule = 2.688168394826091e-32,
- .abs_total_sigma_cm2_per_molecule = 2.688168394826091e-32,
- },
- },
- .{
- .row_index = 10000,
- .row = .{
- .wavelength_nm = 758.0,
- .profile_node_index = 23,
- .altitude_km = 21.987872492065627,
- .row_kind = .weak_line,
- .status = .weak_included,
- .line_index = 390,
- .strong_line_index = 4294967295,
- .matched_strong_line_index = 4294967295,
- .gas_index = 7,
- .isotope_number = 2,
- .isotopologue_code = 68,
- .center_wavelength_nm = 759.782962192108,
- .center_wavenumber_cm1 = 13161.65339,
- .shifted_center_wavenumber_cm1 = 13161.653006155933,
- .line_strength_cm2_per_molecule = 3.991e-27,
- .air_half_width_cm1 = 0.0408,
- .pressure_shift_cm1 = -0.0089,
- .lower_state_energy_cm1 = 569.2054,
- .temperature_k = 221.6,
- .pressure_hpa = 43.7,
- .weak_line_sigma_cm2_per_molecule = 1.4843335201653664e-33,
- .strong_line_sigma_cm2_per_molecule = 0.0,
- .line_mixing_sigma_cm2_per_molecule = 0.0,
- .total_sigma_cm2_per_molecule = 1.4843335201653664e-33,
- .abs_total_sigma_cm2_per_molecule = 1.4843335201653664e-33,
- },
- },
- .{
- .row_index = 50000,
- .row = .{
- .wavelength_nm = 765.0,
- .profile_node_index = 15,
- .altitude_km = 13.95938647915891,
- .row_kind = .strong_line,
- .status = .strong_sidecar,
- .line_index = 4294967295,
- .strong_line_index = 54,
- .matched_strong_line_index = 54,
- .gas_index = 7,
- .isotope_number = 1,
- .isotopologue_code = 66,
- .center_wavelength_nm = 759.8585667043321,
- .center_wavenumber_cm1 = 13160.34383,
- .shifted_center_wavenumber_cm1 = 13160.342471006661,
- .line_strength_cm2_per_molecule = std.math.nan(f64),
- .air_half_width_cm1 = 0.039425769443963796,
- .pressure_shift_cm1 = -0.009,
- .lower_state_energy_cm1 = 664.2595,
- .temperature_k = 215.7,
- .pressure_hpa = 153.0,
- .weak_line_sigma_cm2_per_molecule = 0.0,
- .strong_line_sigma_cm2_per_molecule = 1.5731618461428351e-31,
- .line_mixing_sigma_cm2_per_molecule = 1.6810984283119425e-30,
- .total_sigma_cm2_per_molecule = 1.838414612926226e-30,
- .abs_total_sigma_cm2_per_molecule = 1.838414612926226e-30,
- },
- },
-};
-
-test "O2 line contributions match public Python evidence at probe wavelengths" {
- var prepared = try internal.public.prepare(allocator, o2a_scene.reference());
- defer prepared.deinit(allocator);
-
- const wavelengths_nm = [_]f64{ 758.0, 760.0, 765.0, 767.0, 776.0 };
- var contributions = try internal.public.buildLineContributions(
- allocator,
- &prepared,
- wavelengths_nm[0..],
- 50_001,
- );
- defer contributions.deinit(allocator);
-
- try std.testing.expectEqual(@as(usize, 50_001), contributions.rows.len);
- try std.testing.expectEqual(@as(usize, 101_144), contributions.total_row_count);
- try std.testing.expect(contributions.truncated);
-
- for (expected_rows) |expected| {
- try expectRowEqual(expected.row, contributions.rows[expected.row_index]);
- }
-}
-
-fn expectRowEqual(
- expected: line_contributions.LineContributionRow,
- actual: line_contributions.LineContributionRow,
-) !void {
- // expectRowEqual -----------------------------------------------------------------------------------------|
- // Compare one public O2 line-contribution row field-by-field so drift names the first bad column. |
- // --------------------------------------------------------------------------------------------------------|
- try expectF64Bits(expected.wavelength_nm, actual.wavelength_nm);
- try std.testing.expectEqual(expected.profile_node_index, actual.profile_node_index);
- try expectF64Bits(expected.altitude_km, actual.altitude_km);
- try std.testing.expectEqual(expected.row_kind, actual.row_kind);
- try std.testing.expectEqual(expected.status, actual.status);
- try std.testing.expectEqual(expected.line_index, actual.line_index);
- try std.testing.expectEqual(expected.strong_line_index, actual.strong_line_index);
- try std.testing.expectEqual(expected.matched_strong_line_index, actual.matched_strong_line_index);
- try std.testing.expectEqual(expected.gas_index, actual.gas_index);
- try std.testing.expectEqual(expected.isotope_number, actual.isotope_number);
- try std.testing.expectEqual(expected.isotopologue_code, actual.isotopologue_code);
- try expectF64Bits(expected.center_wavelength_nm, actual.center_wavelength_nm);
- try expectF64Bits(expected.center_wavenumber_cm1, actual.center_wavenumber_cm1);
- try expectF64Bits(expected.shifted_center_wavenumber_cm1, actual.shifted_center_wavenumber_cm1);
- if (std.math.isNan(expected.line_strength_cm2_per_molecule)) {
- try std.testing.expect(std.math.isNan(actual.line_strength_cm2_per_molecule));
- } else {
- try expectF64Bits(expected.line_strength_cm2_per_molecule, actual.line_strength_cm2_per_molecule);
- }
- try expectF64Bits(expected.air_half_width_cm1, actual.air_half_width_cm1);
- try expectF64Bits(expected.pressure_shift_cm1, actual.pressure_shift_cm1);
- try expectF64Bits(expected.lower_state_energy_cm1, actual.lower_state_energy_cm1);
- try expectF64Bits(expected.temperature_k, actual.temperature_k);
- try expectF64Bits(expected.pressure_hpa, actual.pressure_hpa);
- try expectF64Bits(expected.weak_line_sigma_cm2_per_molecule, actual.weak_line_sigma_cm2_per_molecule);
- try expectF64Bits(expected.strong_line_sigma_cm2_per_molecule, actual.strong_line_sigma_cm2_per_molecule);
- try expectF64Bits(expected.line_mixing_sigma_cm2_per_molecule, actual.line_mixing_sigma_cm2_per_molecule);
- try expectF64Bits(expected.total_sigma_cm2_per_molecule, actual.total_sigma_cm2_per_molecule);
- try expectF64Bits(expected.abs_total_sigma_cm2_per_molecule, actual.abs_total_sigma_cm2_per_molecule);
-}
-
-fn expectF64Bits(expected: f64, actual: f64) !void {
- // expectF64Bits ------------------------------------------------------------------------------------------|
- // Enforce exact f64 bits for JSON values that round-trip without local derived-math exceptions. |
- // --------------------------------------------------------------------------------------------------------|
- const expected_bits: u64 = @bitCast(expected);
- const actual_bits: u64 = @bitCast(actual);
- try std.testing.expectEqual(expected_bits, actual_bits);
-}
diff --git a/tests/unit/public_surface_test.zig b/tests/unit/public_surface_test.zig
index 9c75e13c..3c655ad6 100644
--- a/tests/unit/public_surface_test.zig
+++ b/tests/unit/public_surface_test.zig
@@ -11,12 +11,6 @@ test "public root exposes setup session and spectrum surface" {
try std.testing.expect(@hasDecl(zdisamar, "SessionMemory"));
try std.testing.expect(@hasDecl(zdisamar, "AtmosphericBudget"));
try std.testing.expect(@hasDecl(zdisamar, "AtmosphericBudgetRow"));
- try std.testing.expect(@hasDecl(zdisamar, "InstrumentResponse"));
- try std.testing.expect(@hasDecl(zdisamar, "InstrumentResponseRow"));
- try std.testing.expect(@hasDecl(zdisamar, "LineContributions"));
- try std.testing.expect(@hasDecl(zdisamar, "LineContributionRow"));
- try std.testing.expect(@hasDecl(zdisamar, "CiaDiagnostics"));
- try std.testing.expect(@hasDecl(zdisamar, "CiaRow"));
try std.testing.expect(@hasDecl(zdisamar, "Spectrum"));
try std.testing.expect(@hasDecl(zdisamar, "SpectrumRunResult"));
try std.testing.expect(@hasDecl(zdisamar, "optimal_estimation"));
@@ -35,9 +29,6 @@ test "public root exposes setup session and spectrum surface" {
try std.testing.expect(@hasDecl(zdisamar, "buildRunTables"));
try std.testing.expect(@hasDecl(zdisamar, "buildProfileLineValues"));
try std.testing.expect(@hasDecl(zdisamar, "buildAtmosphericBudget"));
- try std.testing.expect(@hasDecl(zdisamar, "buildInstrumentResponse"));
- try std.testing.expect(@hasDecl(zdisamar, "buildLineContributions"));
- try std.testing.expect(@hasDecl(zdisamar, "buildCiaDiagnostics"));
try std.testing.expect(!@hasDecl(zdisamar, "Case"));
try std.testing.expect(!@hasDecl(zdisamar, "PreparedOpticalState"));
diff --git a/tests/unit/root.zig b/tests/unit/root.zig
index e3f57e39..00209cac 100644
--- a/tests/unit/root.zig
+++ b/tests/unit/root.zig
@@ -41,9 +41,6 @@ test {
_ = @import("spectrum/solar_lookup_test.zig");
_ = @import("spectrum/spectrum_run_test.zig");
_ = @import("output/atmospheric_budget_test.zig");
- _ = @import("output/instrument_response_test.zig");
- _ = @import("output/line_contributions_test.zig");
- _ = @import("output/cia_diagnostics_test.zig");
_ = @import("retrieval/state_layout_test.zig");
_ = @import("retrieval/pressure_profile_test.zig");
_ = @import("retrieval/measured_reflectance_rows_test.zig");
diff --git a/tests/unit/spectrum/spectrum_run_test.zig b/tests/unit/spectrum/spectrum_run_test.zig
index 3fe10766..e0b2da33 100644
--- a/tests/unit/spectrum/spectrum_run_test.zig
+++ b/tests/unit/spectrum/spectrum_run_test.zig
@@ -65,7 +65,7 @@ test "radianceAtWavelength wires optics direct transport and radiance scaling" {
.end_nm = wavelength_nm,
.sample_count = 1,
};
- var profile_lines = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var profile_lines = try internal.cache.profile_line_build.buildProfileLineValues(
allocator,
scene,
);
@@ -181,7 +181,7 @@ test "radianceAtWavelength checks caller-owned row shapes" {
.end_nm = 760.0,
.sample_count = 1,
};
- var profile_lines = try internal.cache.profile_line_memory.buildProfileLineValues(
+ var profile_lines = try internal.cache.profile_line_build.buildProfileLineValues(
allocator,
scene,
);
@@ -240,7 +240,7 @@ test "radianceAtWavelength matches canonical transport probes" {
const support_count = tables.layers.support_mid_altitudes_km.len;
const layer_count = tables.layers.layer_pressures_hpa.len;
- var profile_lines = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengths(
+ var profile_lines = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengths(
allocator,
o2a_scene.reference(),
transport_probe_wavelengths_nm[0..],
@@ -400,7 +400,7 @@ test "runForwardSpectrum matches canonical full spectrum" {
}
var profile_lines =
- try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengthsWithCutoffGrid(
+ try internal.cache.profile_line_build.buildProfileLineValuesForWavelengthsWithCutoffGrid(
allocator,
scene,
exact_wavelengths_nm,
@@ -607,7 +607,7 @@ test "runForwardSpectrum assembles direct-route product reflectance across worke
const exact_wavelengths_nm = [_]f64{ 758.0, 760.0 };
const scene = o2a_scene.reference();
- var profile_lines = try internal.cache.profile_line_memory.buildProfileLineValuesForWavelengths(
+ var profile_lines = try internal.cache.profile_line_build.buildProfileLineValuesForWavelengths(
allocator,
scene,
exact_wavelengths_nm[0..],
@@ -1060,7 +1060,7 @@ test "postprocessAndAssembleProductRows rejects inconsistent product shapes" {
}
// TransportProbeEvidence -------------------------------------------------------------------------------------|
-// One-wavelength transport result at one probe wavelength. |
+// One-wavelength transport result at one probe wavelength. |
// |
// layout(64-bit) |
// size: 64 B (0.062 KiB), align: 8 B |
@@ -1168,7 +1168,7 @@ fn expectFullSpectrumApproxAbs(
}
// PublicPythonBaselineEvidence -------------------------------------------------------------------------------|
-// Minimal typed view of the public Python baseline JSON used by the full-spectrum canonical test. |
+// Minimal typed view of the public Python baseline JSON used by the full-spectrum canonical test. |
// |
// layout(64-bit) |
// size: 312 B (0.305 KiB), align: 8 B |
@@ -1181,7 +1181,7 @@ const PublicPythonBaselineEvidence = struct {
// ------------------------------------------------------------------------------------------------------------|
// PublicPythonSpectrumEvidence -------------------------------------------------------------------------------|
-// Spectrum run variants recorded by the public Python baseline. |
+// Spectrum run variants recorded by the public Python baseline. |
// |
// layout(64-bit) |
// size: 312 B (0.305 KiB), align: 8 B |
@@ -1221,7 +1221,7 @@ const FullSpectrumEvidence = struct {
// ------------------------------------------------------------------------------------------------------------|
// FullSpectrumReflectanceJacobianEvidence --------------------------------------------------------------------|
-// Two reflectance-space Jacobian columns requested by the public baseline capture. |
+// Two reflectance-space Jacobian columns requested by the public baseline capture. |
// |
// layout(64-bit) |
// size: 32 B (0.031 KiB), align: 8 B |
From ae7be1abd19b24823a080183263dbd49e78cf573 Mon Sep 17 00:00:00 2001
From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com>
Date: Thu, 18 Jun 2026 13:26:38 +0200
Subject: [PATCH 2/4] remove diagnostic output bindings
---
benchmark/suite/layout.py | 167 ------------
benchmark/suite/report.py | 25 --
python/zdisamar/bindings/handles.py | 96 -------
python/zdisamar/bindings/signatures.py | 56 ----
python/zdisamar/bindings/structures.py | 93 -------
python/zdisamar/output/tables.py | 69 -----
.../plot/collision_induced_absorption.py | 41 ---
python/zdisamar/plot/fields.py | 3 -
python/zdisamar/plot/instrument_response.py | 97 -------
python/zdisamar/plot/properties.py | 1 -
python/zdisamar/rtm/__init__.py | 6 -
python/zdisamar/rtm/run.py | 55 ----
python/zdisamar/rtm/session_cache.py | 20 --
scripts/demo/o2a_plot_bundle.ipynb | 251 +-----------------
14 files changed, 2 insertions(+), 978 deletions(-)
delete mode 100644 python/zdisamar/plot/collision_induced_absorption.py
delete mode 100644 python/zdisamar/plot/instrument_response.py
diff --git a/benchmark/suite/layout.py b/benchmark/suite/layout.py
index ee1575c2..0dce74a2 100644
--- a/benchmark/suite/layout.py
+++ b/benchmark/suite/layout.py
@@ -2,105 +2,20 @@
from typing import Any
-import numpy as np
-from zdisamar import rtm
-
-from . import cases, config
-
-INLINE_INTEGRATION_SAMPLE_COUNT = 5
-WAVELENGTH_SAMPLING_ROW_BYTES = 200
-OLD_WAVELENGTH_SAMPLING_ROW_BYTES = 65_592
-OWNED_WAVELENGTH_SAMPLING_HEADER_BYTES = 48
-INTEGRATION_KERNEL_BYTES = 32_784
-SIDE_SAMPLE_BYTES = 16
-MIN_PARALLEL_WAVELENGTH_SAMPLE_COUNT = 64
-MAX_WORKERS = 64
COLLISION_COMPLEX_PROFILE_CACHE_OLD_BYTES = 2_072
COLLISION_COMPLEX_PROFILE_CACHE_BYTES = 1_048
COLLISION_COMPLEX_PROFILE_CACHE_CAPACITY = 64
COLLISION_COMPLEX_PROFILE_NODE_COUNT = 47
SPLINE_ENDPOINT_SECANT_SAMPLE_SCRATCH_BYTES = 10_240
-INTEGRATION_MODE_LABELS = {
- 0: "auto",
- 1: "explicit_hr_grid",
- 2: "disamar_hr_grid",
- 3: "adaptive",
-}
-CHANNEL_LABELS = {
- 0: "radiance",
- 1: "irradiance",
-}
-
def memory_layout_diagnostics() -> dict[str, Any]:
return {
- "instrument_sampling": instrument_sampling_layout(),
"optical_accumulation": optical_accumulation_layout(),
}
-def instrument_sampling_layout() -> dict[str, Any]:
-
- case = cases.forward_case()
- nominal_wavelengths_nm = rtm.nominal_wavelengths(case)
- response = rtm.instrument_response(case, nominal_wavelengths_nm)
- rows = response.to_rows()
- kernel_rows = [row for row in rows if int(row["sample_index"]) == 0]
- support_counts = np.asarray([row["support_count"] for row in kernel_rows], dtype=np.int64)
- nominal_sample_count = len(nominal_wavelengths_nm)
- kernel_count = int(support_counts.size)
- side_sample_count = int(support_counts[support_counts > INLINE_INTEGRATION_SAMPLE_COUNT].sum())
- side_kernel_count = int(np.count_nonzero(support_counts > INLINE_INTEGRATION_SAMPLE_COUNT))
- current_row_payload_bytes = nominal_sample_count * WAVELENGTH_SAMPLING_ROW_BYTES
- current_side_storage_bytes = side_sample_count * SIDE_SAMPLE_BYTES
- current_owned_plan_bytes = (
- OWNED_WAVELENGTH_SAMPLING_HEADER_BYTES
- + current_row_payload_bytes
- + current_side_storage_bytes
- )
- previous_row_payload_bytes = nominal_sample_count * OLD_WAVELENGTH_SAMPLING_ROW_BYTES
- saved_row_payload_bytes = previous_row_payload_bytes - (
- current_row_payload_bytes + current_side_storage_bytes
- )
- sampling_worker_count = preferred_sampling_worker_count(nominal_sample_count)
-
- return {
- "boundary": "post-timing diagnostic; excluded from benchmark wall time and peak RSS",
- "source": "instrument_response_sampling over benchmark forward_case nominal grid",
- "nominal_sample_count": nominal_sample_count,
- "kernel_count": kernel_count,
- "support_sample_count": len(rows),
- "support_count_stats": int_stats(support_counts),
- "support_count_histogram": int_histogram(support_counts),
- "integration_mode_histogram": labeled_histogram(
- np.asarray([row["integration_mode"] for row in kernel_rows], dtype=np.int64),
- INTEGRATION_MODE_LABELS,
- ),
- "channel_stats": channel_stats(kernel_rows),
- "inline_threshold_samples": INLINE_INTEGRATION_SAMPLE_COUNT,
- "inline_kernel_count": int(kernel_count - side_kernel_count),
- "side_kernel_count": side_kernel_count,
- "side_sample_count": side_sample_count,
- "estimated_sampling_worker_count": sampling_worker_count,
- "integration_kernel_scratch_bytes_per_worker": INTEGRATION_KERNEL_BYTES,
- "integration_kernel_scratch_bytes_at_worker_count": (
- sampling_worker_count * INTEGRATION_KERNEL_BYTES
- ),
- "current_owned_wavelength_sampling_bytes": current_owned_plan_bytes,
- "current_row_payload_bytes": current_row_payload_bytes,
- "current_side_storage_bytes": current_side_storage_bytes,
- "previous_full_kernel_row_payload_bytes": previous_row_payload_bytes,
- "estimated_row_payload_saved_bytes": saved_row_payload_bytes,
- "estimated_row_payload_saved_mib": mib(saved_row_payload_bytes),
- "estimated_row_payload_reduction_ratio": ratio(
- saved_row_payload_bytes,
- previous_row_payload_bytes,
- ),
- }
-
-
def optical_accumulation_layout() -> dict[str, Any]:
cache_saved_bytes = (
@@ -131,88 +46,6 @@ def optical_accumulation_layout() -> dict[str, Any]:
}
-def channel_stats(kernel_rows: list[dict[str, Any]]) -> dict[str, Any]:
-
- result = {}
-
- for channel_code, label in CHANNEL_LABELS.items():
- rows = [row for row in kernel_rows if int(row["channel"]) == channel_code]
- counts = np.asarray([row["support_count"] for row in rows], dtype=np.int64)
- side_counts = counts[counts > INLINE_INTEGRATION_SAMPLE_COUNT]
- result[label] = {
- "kernel_count": int(counts.size),
- "support_count_stats": int_stats(counts),
- "support_count_histogram": int_histogram(counts),
- "side_kernel_count": int(side_counts.size),
- "side_sample_count": int(side_counts.sum()),
- }
-
- return result
-
-
-def preferred_sampling_worker_count(sample_count: int) -> int:
-
- if sample_count < MIN_PARALLEL_WAVELENGTH_SAMPLE_COUNT:
- return 1
-
- count_from_work = max(1, sample_count // MIN_PARALLEL_WAVELENGTH_SAMPLE_COUNT)
- worker_cap = config.EFFECTIVE_WORKER_CAP or 1
-
- return min(MAX_WORKERS, worker_cap, count_from_work)
-
-
-def int_stats(values: np.ndarray) -> dict[str, float | int]:
-
- if values.size == 0:
- return {
- "count": 0,
- "min": 0,
- "median": 0.0,
- "mean": 0.0,
- "p90": 0.0,
- "max": 0,
- }
-
- return {
- "count": int(values.size),
- "min": int(np.min(values)),
- "median": float(np.median(values)),
- "mean": float(np.mean(values)),
- "p90": float(np.percentile(values, 90.0)),
- "max": int(np.max(values)),
- }
-
-
-def int_histogram(values: np.ndarray) -> dict[str, int]:
-
- if values.size == 0:
- return {}
-
- unique, counts = np.unique(values.astype(np.int64), return_counts=True)
-
- return {str(int(value)): int(count) for value, count in zip(unique, counts, strict=True)}
-
-
-def labeled_histogram(values: np.ndarray, labels: dict[int, str]) -> dict[str, int]:
-
- histogram = int_histogram(values)
-
- return {labels.get(int(key), str(key)): count for key, count in histogram.items()}
-
-
-def ratio(numerator: int, denominator: int) -> float:
-
- if denominator == 0:
- return 0.0
-
- return numerator / denominator
-
-
-def mib(byte_count: int) -> float:
-
- return byte_count / (1024 * 1024)
-
-
def kib(byte_count: int) -> float:
return byte_count / 1024
diff --git a/benchmark/suite/report.py b/benchmark/suite/report.py
index 841b8e9f..e67f0f55 100644
--- a/benchmark/suite/report.py
+++ b/benchmark/suite/report.py
@@ -213,34 +213,9 @@ def build_compact_report(
def memory_layout_rows(memory_layout: dict[str, Any]) -> list[dict[str, str]]:
- instrument = memory_layout["instrument_sampling"]
optical = memory_layout["optical_accumulation"]
- support = instrument["support_count_stats"]
return [
- {
- "area": "Instrument sampling plan",
- "shape": (
- f"{instrument['nominal_sample_count']} wavelengths, "
- f"{instrument['kernel_count']} channel kernels, "
- f"support count median {support['median']:.1f}, max {support['max']}"
- ),
- "memory": (
- f"current owned plan {instrument['current_owned_wavelength_sampling_bytes']} B; "
- "estimated row payload saved "
- f"{instrument['estimated_row_payload_saved_mib']:.2f} MiB"
- ),
- },
- {
- "area": "Integration kernel scratch",
- "shape": (
- f"{instrument['estimated_sampling_worker_count']} sampling worker(s), "
- f"{instrument['integration_kernel_scratch_bytes_per_worker']} B each"
- ),
- "memory": (
- f"{instrument['integration_kernel_scratch_bytes_at_worker_count']} B transient"
- ),
- },
{
"area": "Collision-complex profile cache",
"shape": (
diff --git a/python/zdisamar/bindings/handles.py b/python/zdisamar/bindings/handles.py
index 175ea5e3..1eee831c 100644
--- a/python/zdisamar/bindings/handles.py
+++ b/python/zdisamar/bindings/handles.py
@@ -18,16 +18,12 @@
)
from ..output.tables import (
AtmosphericBudget,
- InstrumentResponseTable,
- O2LineContributions,
- OxygenCollisionInducedAbsorptionDiagnosticTable,
)
from .loader import load_library
from .signatures import configure
from .structures import (
CAtmosphericBudget,
CDiagnosticReport,
- CInstrumentResponse,
COptimalEstimationBatchRequest,
COptimalEstimationBatchResult,
COptimalEstimationControls,
@@ -37,8 +33,6 @@
COptimalEstimationResult,
COptimalEstimationScalarSpec,
CSpectrum,
- O2LineContributionsRaw,
- OxygenCollisionInducedAbsorptionDiagnosticsRaw,
)
_MAX_OPTIMAL_ESTIMATION_ITERATIONS = 1000
@@ -54,24 +48,6 @@ def contiguous_wavelengths(wavelengths_nm):
return double_array(wavelengths_nm, "wavelengths_nm")
-def channel_mask(channels: tuple[str, ...]) -> int:
- """Translate requested spectral channels into model channel selection."""
-
- masks = {"radiance": 1, "irradiance": 2}
- mask = 0
-
- for channel in channels:
- try:
- mask |= masks[channel]
- except KeyError as exc:
- raise ValueError(f"unsupported spectral channel: {channel}") from exc
-
- if mask == 0:
- raise ValueError("channels must not be empty")
-
- return mask
-
-
def batch_run_status(value: int) -> str:
"""Translate native per-start batch status into a stable Python label."""
@@ -229,78 +205,6 @@ def atmospheric_budget(self, wavelengths_nm) -> AtmosphericBudget:
return AtmosphericBudget(self._copied_rows(raw, self._lib.zds_atmospheric_budget_free))
- def o2_line_contributions(self, wavelengths_nm, max_rows: int = 50_000) -> O2LineContributions:
- """Return copied line-by-line evidence rows."""
-
- wavelengths = contiguous_wavelengths(wavelengths_nm)
-
- if max_rows <= 0:
- raise ValueError("max_rows must be positive")
-
- raw = O2LineContributionsRaw()
- self._check(
- self._lib.zds_o2_line_contributions(
- self._ctx,
- wavelengths,
- len(wavelengths),
- max_rows,
- ctypes.byref(raw),
- )
- )
- total_row_count = int(raw.total_row_count)
- truncated = bool(raw.truncated)
- rows = self._copied_rows(raw, self._lib.zds_o2_line_contributions_free)
-
- return O2LineContributions(
- rows,
- total_row_count=total_row_count,
- truncated=truncated,
- )
-
- def instrument_response_sampling(
- self,
- wavelengths_nm,
- channels: tuple[str, ...] = ("radiance", "irradiance"),
- ) -> InstrumentResponseTable:
- """Return copied instrument response support rows."""
-
- wavelengths = contiguous_wavelengths(wavelengths_nm)
- raw = CInstrumentResponse()
- self._check(
- self._lib.zds_instrument_response_sampling(
- self._ctx,
- wavelengths,
- len(wavelengths),
- channel_mask(channels),
- ctypes.byref(raw),
- )
- )
-
- return InstrumentResponseTable(
- self._copied_rows(raw, self._lib.zds_instrument_response_free)
- )
-
- def collision_induced_absorption(
- self,
- wavelengths_nm,
- ) -> OxygenCollisionInducedAbsorptionDiagnosticTable:
- """Return copied O2-O2 CIA rows on the atmospheric layer grid."""
-
- wavelengths = contiguous_wavelengths(wavelengths_nm)
- raw = OxygenCollisionInducedAbsorptionDiagnosticsRaw()
- self._check(
- self._lib.zds_o2_o2_cia_diagnostics(
- self._ctx,
- wavelengths,
- len(wavelengths),
- ctypes.byref(raw),
- )
- )
-
- return OxygenCollisionInducedAbsorptionDiagnosticTable(
- self._copied_rows(raw, self._lib.zds_o2_o2_cia_diagnostics_free)
- )
-
def optimal_estimation(self, *, measurement, state_vector, controls):
"""Run native optimal estimation for the loaded scene."""
diff --git a/python/zdisamar/bindings/signatures.py b/python/zdisamar/bindings/signatures.py
index 1cbf93c8..d8db5a14 100644
--- a/python/zdisamar/bindings/signatures.py
+++ b/python/zdisamar/bindings/signatures.py
@@ -5,15 +5,12 @@
from .structures import (
CAtmosphericBudget,
CDiagnosticReport,
- CInstrumentResponse,
COptimalEstimationBatchRequest,
COptimalEstimationBatchResult,
COptimalEstimationFastmodeBatchResult,
COptimalEstimationRequest,
COptimalEstimationResult,
CSpectrum,
- O2LineContributionsRaw,
- OxygenCollisionInducedAbsorptionDiagnosticsRaw,
)
@@ -67,41 +64,6 @@ def configure(lib: ctypes.CDLL) -> ctypes.CDLL:
],
ctypes.c_int,
)
- bind(
- lib,
- "zds_o2_line_contributions",
- [
- ctypes.c_void_p,
- ctypes.POINTER(ctypes.c_double),
- ctypes.c_size_t,
- ctypes.c_size_t,
- ctypes.POINTER(O2LineContributionsRaw),
- ],
- ctypes.c_int,
- )
- bind(
- lib,
- "zds_instrument_response_sampling",
- [
- ctypes.c_void_p,
- ctypes.POINTER(ctypes.c_double),
- ctypes.c_size_t,
- ctypes.c_uint32,
- ctypes.POINTER(CInstrumentResponse),
- ],
- ctypes.c_int,
- )
- bind(
- lib,
- "zds_o2_o2_cia_diagnostics",
- [
- ctypes.c_void_p,
- ctypes.POINTER(ctypes.c_double),
- ctypes.c_size_t,
- ctypes.POINTER(OxygenCollisionInducedAbsorptionDiagnosticsRaw),
- ],
- ctypes.c_int,
- )
bind(
lib,
"zds_run_o2a_optimal_estimation",
@@ -169,24 +131,6 @@ def configure(lib: ctypes.CDLL) -> ctypes.CDLL:
[ctypes.c_void_p, ctypes.POINTER(CAtmosphericBudget)],
None,
)
- bind(
- lib,
- "zds_o2_line_contributions_free",
- [ctypes.c_void_p, ctypes.POINTER(O2LineContributionsRaw)],
- None,
- )
- bind(
- lib,
- "zds_instrument_response_free",
- [ctypes.c_void_p, ctypes.POINTER(CInstrumentResponse)],
- None,
- )
- bind(
- lib,
- "zds_o2_o2_cia_diagnostics_free",
- [ctypes.c_void_p, ctypes.POINTER(OxygenCollisionInducedAbsorptionDiagnosticsRaw)],
- None,
- )
bind(lib, "zds_last_error", [ctypes.c_void_p], ctypes.c_char_p)
return lib
diff --git a/python/zdisamar/bindings/structures.py b/python/zdisamar/bindings/structures.py
index aec7463d..f6dcf960 100644
--- a/python/zdisamar/bindings/structures.py
+++ b/python/zdisamar/bindings/structures.py
@@ -67,99 +67,6 @@ class CAtmosphericBudget(ctypes.Structure):
]
-class O2LineContributionRow(ctypes.Structure):
- _fields_ = [
- ("wavelength_nm", ctypes.c_double),
- ("profile_node_index", ctypes.c_uint32),
- ("altitude_km", ctypes.c_double),
- ("row_kind", ctypes.c_uint32),
- ("status", ctypes.c_uint32),
- ("line_index", ctypes.c_uint32),
- ("strong_line_index", ctypes.c_uint32),
- ("matched_strong_line_index", ctypes.c_uint32),
- ("gas_index", ctypes.c_uint16),
- ("isotope_number", ctypes.c_uint8),
- ("isotopologue_code", ctypes.c_int32),
- ("center_wavelength_nm", ctypes.c_double),
- ("center_wavenumber_cm1", ctypes.c_double),
- ("shifted_center_wavenumber_cm1", ctypes.c_double),
- ("line_strength_cm2_per_molecule", ctypes.c_double),
- ("air_half_width_cm1", ctypes.c_double),
- ("pressure_shift_cm1", ctypes.c_double),
- ("lower_state_energy_cm1", ctypes.c_double),
- ("temperature_k", ctypes.c_double),
- ("pressure_hpa", ctypes.c_double),
- ("weak_line_sigma_cm2_per_molecule", ctypes.c_double),
- ("strong_line_sigma_cm2_per_molecule", ctypes.c_double),
- ("line_mixing_sigma_cm2_per_molecule", ctypes.c_double),
- ("total_sigma_cm2_per_molecule", ctypes.c_double),
- ("abs_total_sigma_cm2_per_molecule", ctypes.c_double),
- ]
-
-
-class O2LineContributionsRaw(ctypes.Structure):
- _fields_ = [
- ("len", ctypes.c_size_t),
- ("total_row_count", ctypes.c_size_t),
- ("truncated", ctypes.c_uint8),
- ("rows", ctypes.POINTER(O2LineContributionRow)),
- ]
-
-
-class CInstrumentResponseRow(ctypes.Structure):
- _fields_ = [
- ("nominal_index", ctypes.c_int32),
- ("nominal_wavelength_nm", ctypes.c_double),
- ("channel", ctypes.c_uint32),
- ("sample_index", ctypes.c_uint32),
- ("support_count", ctypes.c_uint32),
- ("offset_nm", ctypes.c_double),
- ("support_wavelength_nm", ctypes.c_double),
- ("weight", ctypes.c_double),
- ("support_width_nm", ctypes.c_double),
- ("instrument_fwhm_nm", ctypes.c_double),
- ("high_resolution_step_nm", ctypes.c_double),
- ("high_resolution_half_span_nm", ctypes.c_double),
- ("integration_mode", ctypes.c_uint32),
- ("response_enabled", ctypes.c_uint8),
- ]
-
-
-class CInstrumentResponse(ctypes.Structure):
- _fields_ = [
- ("len", ctypes.c_size_t),
- ("rows", ctypes.POINTER(CInstrumentResponseRow)),
- ]
-
-
-class OxygenCollisionInducedAbsorptionRow(ctypes.Structure):
- _fields_ = [
- ("wavelength_nm", ctypes.c_double),
- ("layer_index", ctypes.c_uint32),
- ("sublayer_index", ctypes.c_uint32),
- ("global_sublayer_index", ctypes.c_uint32),
- ("interval_index_1based", ctypes.c_uint32),
- ("altitude_km", ctypes.c_double),
- ("pressure_hpa", ctypes.c_double),
- ("temperature_k", ctypes.c_double),
- ("oxygen_number_density_cm3", ctypes.c_double),
- ("path_length_cm", ctypes.c_double),
- ("cia_cross_section_cm5_per_molecule2", ctypes.c_double),
- ("cia_optical_depth", ctypes.c_double),
- ("total_absorption_optical_depth", ctypes.c_double),
- ("total_optical_depth", ctypes.c_double),
- ("cia_share_of_total_absorption", ctypes.c_double),
- ("cia_share_of_total_optical_depth", ctypes.c_double),
- ]
-
-
-class OxygenCollisionInducedAbsorptionDiagnosticsRaw(ctypes.Structure):
- _fields_ = [
- ("len", ctypes.c_size_t),
- ("rows", ctypes.POINTER(OxygenCollisionInducedAbsorptionRow)),
- ]
-
-
class COptimalEstimationScalarSpec(ctypes.Structure):
_fields_ = [
("has_lower", ctypes.c_uint8),
diff --git a/python/zdisamar/output/tables.py b/python/zdisamar/output/tables.py
index b1d6a6a4..f7ab2827 100644
--- a/python/zdisamar/output/tables.py
+++ b/python/zdisamar/output/tables.py
@@ -6,9 +6,6 @@
from ..bindings.structures import (
CAtmosphericBudgetRow,
- CInstrumentResponseRow,
- O2LineContributionRow,
- OxygenCollisionInducedAbsorptionRow,
)
@@ -88,69 +85,3 @@ def plot(self):
from ..plot.atmosphere import BudgetPlot
return BudgetPlot(self)
-
-
-@dataclass(frozen=True)
-class O2LineContributions(RtmTable):
- """O2 line-by-line contribution table for selected wavelengths."""
-
- total_row_count: int = 0
- truncated: bool = False
-
- row_kind_labels = {
- 0: "weak_line",
- 1: "strong_line",
- }
- status_labels = {
- 0: "weak_included",
- 1: "weak_excluded_by_strong_line",
- 2: "strong_sidecar",
- 3: "weak_zero_after_cutoff",
- }
- columns = field_names(O2LineContributionRow)
- label_columns = {
- "row_kind_label": ("row_kind", row_kind_labels),
- "status_label": ("status", status_labels),
- }
-
-
-@dataclass(frozen=True)
-class InstrumentResponseTable(RtmTable):
- """Instrument response support-weight table."""
-
- channel_labels = {
- 0: "radiance",
- 1: "irradiance",
- }
- integration_mode_labels = {
- 0: "auto",
- 1: "explicit_hr_grid",
- 2: "disamar_hr_grid",
- 3: "adaptive",
- }
- columns = field_names(CInstrumentResponseRow)
- label_columns = {
- "channel_label": ("channel", channel_labels),
- "integration_mode_label": ("integration_mode", integration_mode_labels),
- }
-
- @property
- def plot(self):
-
- from ..plot.instrument_response import InstrumentResponsePlot
-
- return InstrumentResponsePlot(self)
-
-
-@dataclass(frozen=True)
-class OxygenCollisionInducedAbsorptionDiagnosticTable(RtmTable):
- """O2-O2 collision-induced absorption diagnostic table."""
-
- columns = field_names(OxygenCollisionInducedAbsorptionRow)
-
- @property
- def plot(self):
-
- from ..plot.collision_induced_absorption import CollisionInducedAbsorptionPlot
-
- return CollisionInducedAbsorptionPlot(self)
diff --git a/python/zdisamar/plot/collision_induced_absorption.py b/python/zdisamar/plot/collision_induced_absorption.py
deleted file mode 100644
index d6758f1d..00000000
--- a/python/zdisamar/plot/collision_induced_absorption.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""O2-O2 collision-induced absorption plot accessor."""
-
-from pathlib import Path
-
-from . import fields
-from .profiles import profile_figure
-from .properties import PlotAccessor
-
-
-class CollisionInducedAbsorptionPlot(PlotAccessor):
- """Plots for O2-O2 collision-induced absorption by layer."""
-
- def __init__(self, table):
-
- super().__init__(table)
-
- def optical_depth(
- self,
- wavelength_nm: float | None = None,
- save: str | Path | None = None,
- ):
-
- return self.finish_plot(
- optical_depth_profile(self.target, wavelength_nm=wavelength_nm),
- save=save,
- )
-
-
-def optical_depth_profile(
- table,
- *,
- wavelength_nm: float | None,
-):
-
- return profile_figure(
- table,
- quantity=fields.COLLISION_INDUCED_ABSORPTION_OPTICAL_DEPTH,
- title="O2-O2 collision-induced absorption optical depth profile",
- color_key="collision_induced_absorption",
- wavelength_nm=wavelength_nm,
- )
diff --git a/python/zdisamar/plot/fields.py b/python/zdisamar/plot/fields.py
index 730db789..f4244171 100644
--- a/python/zdisamar/plot/fields.py
+++ b/python/zdisamar/plot/fields.py
@@ -10,7 +10,6 @@
SNR = "snr"
TOTAL_OPTICAL_DEPTH = "total_optical_depth"
-COLLISION_INDUCED_ABSORPTION_OPTICAL_DEPTH = "cia_optical_depth"
SPECTRUM_FIELDS = (WAVELENGTH_NM, REFLECTANCE, RADIANCE, IRRADIANCE)
@@ -26,6 +25,4 @@
"altitude_km": "Altitude (km)",
"pressure_hpa": "Pressure (hPa)",
TOTAL_OPTICAL_DEPTH: "Total optical depth",
- COLLISION_INDUCED_ABSORPTION_OPTICAL_DEPTH: "O2-O2 collision-induced absorption optical depth",
- "offset_nm": "Offset (nm)",
}
diff --git a/python/zdisamar/plot/instrument_response.py b/python/zdisamar/plot/instrument_response.py
deleted file mode 100644
index cb308b75..00000000
--- a/python/zdisamar/plot/instrument_response.py
+++ /dev/null
@@ -1,97 +0,0 @@
-"""Instrument-response plot accessor."""
-
-from pathlib import Path
-
-from . import fields
-from .data import as_float, with_channel_labels
-from .properties import PLOT, PlotAccessor
-from .svg import SvgFigure, SvgPanel, line_panel
-
-
-class InstrumentResponsePlot(PlotAccessor):
- """Plots for instrument response support weights."""
-
- def __init__(self, response):
-
- super().__init__(response)
-
- def curve(self, save: str | Path | None = None):
-
- return self.finish_plot(curve_figure(self.target), save=save)
-
-
-def curve_figure(response):
-
- data = with_channel_labels(response)
- required = [
- "nominal_wavelength_nm",
- "channel_label",
- "offset_nm",
- "support_wavelength_nm",
- "weight",
- "instrument_fwhm_nm",
- ]
- present = set[str]()
-
- for row in data:
- present.update(row)
-
- missing = [column for column in required if column not in present]
-
- if missing:
- raise ValueError(f"missing required plotting columns: {', '.join(missing)}")
-
- data = [row for row in data if row["channel_label"] == "radiance"]
-
- if not data:
- raise ValueError("no radiance instrument response rows")
-
- nominal_wavelengths = [as_float(row["nominal_wavelength_nm"]) for row in data]
- selected = min(nominal_wavelengths, key=lambda value: abs(value - 760.76))
- data = [row for row in data if as_float(row["nominal_wavelength_nm"]) == selected]
- data = sorted(data, key=lambda row: as_float(row["offset_nm"]))
- max_weight = max(as_float(row["weight"]) for row in data)
-
- if max_weight <= 0.0:
- raise ValueError("instrument response weights are not positive")
-
- data = [{**row, "normalized_response": as_float(row["weight"]) / max_weight} for row in data]
- plot_data = [row for row in data if as_float(row["weight"]) >= max_weight * 1.0e-4]
-
- if not plot_data:
- plot_data = data
-
- offsets = [as_float(row["offset_nm"]) for row in plot_data]
- x_min = min(offsets)
- x_max = max(offsets)
- x_pad = max((x_max - x_min) * 0.04, 0.01)
- title = f"Instrument spectral response function (ISRF), {selected:.5f} nm"
-
- panel = line_panel(
- title=title,
- x_title=fields.QUANTITY_LABELS["offset_nm"],
- y_title="Normalized ISRF",
- x=[as_float(row["offset_nm"]) for row in plot_data],
- y=[as_float(row["normalized_response"]) for row in plot_data],
- name=title,
- color=PLOT.colors["black"],
- marker_x=False,
- show_legend=False,
- )
-
- panel = SvgPanel(
- title=panel.title,
- x_title=panel.x_title,
- y_title=panel.y_title,
- series=panel.series,
- width=panel.width,
- height=panel.height,
- x_domain=(x_min - x_pad, x_max + x_pad),
- y_domain=(0.0, 1.05),
- show_legend=panel.show_legend,
- )
-
- return SvgFigure(
- title=title,
- panels=(panel,),
- )
diff --git a/python/zdisamar/plot/properties.py b/python/zdisamar/plot/properties.py
index ca8f138a..77d0dc75 100644
--- a/python/zdisamar/plot/properties.py
+++ b/python/zdisamar/plot/properties.py
@@ -48,7 +48,6 @@ class PlotProperties:
"irradiance": "#2F6B3F",
"sun_normalized_radiance": "#2A5C7A",
"total_optical_depth": "#222222",
- "collision_induced_absorption": "#8C3F2D",
"residual_positive": "#8B1A1A",
"residual_negative": "#1F4E79",
}
diff --git a/python/zdisamar/rtm/__init__.py b/python/zdisamar/rtm/__init__.py
index df804736..4a195216 100644
--- a/python/zdisamar/rtm/__init__.py
+++ b/python/zdisamar/rtm/__init__.py
@@ -7,10 +7,7 @@
"AerosolProfileLayer": "..input.aerosol",
"SessionCache": ".session_cache",
"atmospheric_budget": ".run",
- "collision_induced_absorption": ".run",
- "instrument_response": ".run",
"nominal_wavelengths": ".run",
- "o2_line_contributions": ".run",
"reference_scene": ".run",
"reflectance_from_radiance": ".reflectance",
"reflectance_jacobian_from_radiance_jacobian": ".reflectance",
@@ -39,10 +36,7 @@ def __getattr__(name: str):
"AerosolProfileLayer",
"SessionCache",
"atmospheric_budget",
- "collision_induced_absorption",
- "instrument_response",
"nominal_wavelengths",
- "o2_line_contributions",
"reference_scene",
"reflectance_from_radiance",
"reflectance_jacobian_from_radiance_jacobian",
diff --git a/python/zdisamar/rtm/run.py b/python/zdisamar/rtm/run.py
index 18dfa0dd..cbcca6f9 100644
--- a/python/zdisamar/rtm/run.py
+++ b/python/zdisamar/rtm/run.py
@@ -61,61 +61,6 @@ def atmospheric_budget(
return temporary.atmospheric_budget(wavelengths_nm)
-def instrument_response(
- scene: Scene,
- wavelengths_nm=None,
- *,
- channels: tuple[str, ...] = ("radiance", "irradiance"),
- cache: SessionCache | None = None,
-):
- """Return instrument response support rows."""
-
- grid = nominal_wavelengths(scene) if wavelengths_nm is None else wavelengths_nm
-
- if cache is not None:
- cache.load(scene)
-
- return cache.instrument_response(grid, channels=channels)
-
- with _temporary_cache(scene) as temporary:
- return temporary.instrument_response(grid, channels=channels)
-
-
-def collision_induced_absorption(
- scene: Scene,
- wavelengths_nm,
- *,
- cache: SessionCache | None = None,
-):
- """Return O2-O2 collision-induced absorption rows."""
-
- if cache is not None:
- cache.load(scene)
-
- return cache.collision_induced_absorption(wavelengths_nm)
-
- with _temporary_cache(scene) as temporary:
- return temporary.collision_induced_absorption(wavelengths_nm)
-
-
-def o2_line_contributions(
- scene: Scene,
- wavelengths_nm,
- *,
- max_rows: int = 50_000,
- cache: SessionCache | None = None,
-):
- """Return line-by-line evidence rows."""
-
- if cache is not None:
- cache.load(scene)
-
- return cache._handle.o2_line_contributions(wavelengths_nm, max_rows=max_rows)
-
- with _temporary_cache(scene) as temporary:
- return temporary._handle.o2_line_contributions(wavelengths_nm, max_rows=max_rows)
-
-
def nominal_wavelengths(scene: Scene):
"""Recreate the nominal spectrum grid from a wavelength-band scene."""
diff --git a/python/zdisamar/rtm/session_cache.py b/python/zdisamar/rtm/session_cache.py
index c6a7638d..6c3ce5d6 100644
--- a/python/zdisamar/rtm/session_cache.py
+++ b/python/zdisamar/rtm/session_cache.py
@@ -96,26 +96,6 @@ def atmospheric_budget(self, wavelengths_nm):
return self._handle.atmospheric_budget(wavelengths_nm)
- def instrument_response(
- self,
- wavelengths_nm,
- channels: tuple[str, ...] = ("radiance", "irradiance"),
- ):
- """Return instrument response rows for the loaded scene."""
-
- if not self._loaded:
- raise RuntimeError("SessionCache has no loaded wavelength-band scene")
-
- return self._handle.instrument_response_sampling(wavelengths_nm, channels=channels)
-
- def collision_induced_absorption(self, wavelengths_nm):
- """Return O2-O2 collision-induced absorption rows for the loaded scene."""
-
- if not self._loaded:
- raise RuntimeError("SessionCache has no loaded wavelength-band scene")
-
- return self._handle.collision_induced_absorption(wavelengths_nm)
-
def close(self) -> None:
"""Release cached RTM storage."""
diff --git a/scripts/demo/o2a_plot_bundle.ipynb b/scripts/demo/o2a_plot_bundle.ipynb
index 63b5c0b0..59e68e63 100644
--- a/scripts/demo/o2a_plot_bundle.ipynb
+++ b/scripts/demo/o2a_plot_bundle.ipynb
@@ -72,12 +72,7 @@
" nearest = min(grid, key=lambda value: abs(value - target))\n",
" if nearest not in values:\n",
" values.append(nearest)\n",
- " return values\n",
- "\n",
- "\n",
- "def response_grid_values(grid: list[float]) -> list[float]:\n",
- " values = grid[::35] + nearest_grid_values(grid, O2A_MARKERS_NM)\n",
- " return sorted(set(values))"
+ " return values"
]
},
{
@@ -98,8 +93,6 @@
"case = o2a.reference_scene()\n",
"oe_baseline.configure_case(case)\n",
"grid = spectral_grid(case)\n",
- "profile_wavelengths_nm = nearest_grid_values(grid, O2A_MARKERS_NM)\n",
- "response_wavelengths_nm = response_grid_values(grid)\n",
"\n",
"spectrum = rtm.spectrum(case, jacobian=True)\n",
"noise = components_from_spectrum(\n",
@@ -109,9 +102,7 @@
" reflectance=spectrum.reflectance.copy(),\n",
")\n",
"noise_table = noise.snr_table()\n",
- "budget = rtm.atmospheric_budget(case, wavelengths_nm=grid)\n",
- "cia = rtm.collision_induced_absorption(case, wavelengths_nm=grid)\n",
- "response = rtm.instrument_response(case, wavelengths_nm=response_wavelengths_nm)"
+ "budget = rtm.atmospheric_budget(case, wavelengths_nm=grid)"
]
},
{
@@ -715,184 +706,6 @@
"## O2-O2 CIA Optical Depth\n"
]
},
- {
- "cell_type": "code",
- "execution_count": 11,
- "id": "6c0e023a",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "cia.plot.optical_depth(): 302.26 ms\n"
- ]
- },
- {
- "data": {
- "image/svg+xml": [
- ""
- ],
- "text/plain": [
- "SvgFigure(title='O2-O2 collision-induced absorption optical depth profile, 760.76 nm', panels=(SvgPanel(title='O2-O2 collision-induced absorption optical depth profile, 760.76 nm', x_title='O2-O2 collision-induced absorption optical depth', y_title='Altitude (km)', series=(SvgSeries(name='O2-O2 collision-induced absorption optical depth profile, 760.76 nm', kind='points', x=(4.2625252429425384e-05, 7.905965262044865e-05, 7.798198991728409e-05, 4.116823692238184e-05, 0.00016313728622152404, 0.00029400501124191247, 0.00027976175114536336, 0.00014370181666257352, 0.0002181201261890337, 0.0003846575345125608, 0.0003544866700841479, 0.00017767305033384453, 0.0001938137196374152, 0.0003395038291053643, 0.0003108969043527366, 0.00015487645291988267, 0.00013246495233230543, 0.00023350708058655697, 0.00021536166755019447, 0.0001079318171955036, 6.740917578371608e-05, 0.00012137756819217887, 0.00011511248780142751, 5.894101000313478e-05, 1.4474463480504834e-05, 2.686011964210277e-05, 2.6503774245179532e-05, 1.399302395654382e-05, 1.5879329746365176e-06, 2.973524557773562e-06, 2.968999776729754e-06, 1.5818197752800815e-06, 5.927797373130026e-06, 1.1064618300010227e-05, 1.1001517117802685e-05, 5.8425445766674114e-06, 7.6023859424949945e-06, 1.4171324498867857e-05, 1.40658586822649e-05, 7.459896330165183e-06, 5.702224845298265e-06, 1.064347952856689e-05, 1.0582668525652411e-05, 5.6200661405897714e-06, 1.4952686690385775e-06, 2.799994098581632e-06, 2.7957212128293842e-06, 1.4894957920728619e-06, 2.0002413861536583e-05, 3.691214568270794e-05, 3.615920064693962e-05, 1.898511580368614e-05, 7.854480714642844e-05, 0.000137728833473655, 0.0001261695188465697, 6.288959155920508e-05, 0.00010533070679130462, 0.00017490269213209318, 0.00014894971005335558, 7.007081493519765e-05, 9.018842366827387e-05, 0.00014125812030804606, 0.00011098405762007297, 4.910998164129691e-05, 5.4787453042273e-05, 7.772847697960998e-05, 5.215560516564534e-05, 2.057356543979757e-05, 2.0102405734376647e-05, 2.6426530908114242e-05, 1.6504060242194306e-05, 6.1458118607353855e-06, 5.5529344623348665e-06, 7.040287390883348e-06, 4.243931915156696e-06, 1.5410882259203825e-06, 1.3210256502480964e-06, 1.6542102518015896e-06, 9.755795342923936e-07, 3.472366903598515e-07, 2.8506112437087643e-07, 3.569545519389642e-07, 2.1196869595998925e-07, 7.615148348490247e-08, 6.082557325105099e-08, 7.801342625919192e-08, 4.7884827353729485e-08, 1.7662640627947034e-08, 1.381879001857921e-08, 1.8402659395879416e-08, 1.1851072079879576e-08, 4.531767432975826e-09, 3.472767708728616e-09, 4.842828178782317e-09, 3.3284708089636336e-09, 1.3468151017979625e-09, 1.013121949187601e-09, 1.5084901030147491e-09, 1.1199657729648912e-09, 4.762087136958805e-10, 3.42439091175547e-10, 5.358331723131902e-10, 4.2344160339820414e-10, 1.8859555370825234e-10, 1.2437757373747272e-10, 2.049097305976378e-10, 1.7308644982473555e-10, 8.11072241353601e-11, 4.3625897134178026e-11, 7.597777603731307e-11, 6.900480569509116e-11, 3.418588772941141e-11, 7.888699594638655e-12, 1.4533013959757727e-11, 1.42050081872923e-11, 7.445497615677016e-12), y=(0.012791192210410855, 0.060933015764590134, 0.12374465143912032, 0.17188647499329957, 0.23624048246809212, 0.4296191773301218, 0.6819243712790015, 0.875303066141031, 1.0071014319117895, 1.308361857073967, 1.7014225820617734, 2.002683007223951, 2.173607384614099, 2.513825957529956, 2.9577161859017527, 3.2979347588176093, 3.4688591362077577, 3.770119561369935, 4.163180286357742, 4.464440711519919, 4.596239077290678, 4.789617772152708, 5.041922966101588, 5.235301660963618, 5.299655668438411, 5.34779749199259, 5.4106091276671195, 5.458750951221298, 5.4730386241044835, 5.478518420976346, 5.485668025608554, 5.491147822480416, 5.498087730862327, 5.518653402422506, 5.545485860648485, 5.566051532208664, 5.578680933712734, 5.605513391938714, 5.640522256703578, 5.667354714929558, 5.679984116433628, 5.700549787993807, 5.727382246219786, 5.747947917779965, 5.754887826161876, 5.760367623033738, 5.767517227665946, 5.772997024537808, 5.79415376965579, 5.868075213660746, 5.964522054818497, 6.038443498823453, 6.141441778614612, 6.454072764011704, 6.86196889476861, 7.174599880165702, 7.40460574133636, 7.9551864968780635, 8.673540627929478, 9.224121383471182, 9.57566411715167, 10.344422849787446, 11.347438312664321, 12.116197045300098, 12.57660922582157, 13.535776894167734, 14.787222844634178, 15.74639051298034, 16.299036920637146, 17.413950605039837, 18.86860161289869, 19.98351529730138, 20.608423252242194, 21.83878842710615, 23.44407138908642, 24.674436563950376, 25.349020789608574, 26.650369462715677, 28.348266067083934, 29.649614740191033, 30.34949426267631, 31.67479265837441, 33.40393696253532, 34.729235358233424, 35.42911488071869, 36.73046355382579, 38.428360158194046, 39.729708831301146, 40.40429305695935, 41.6346582318233, 43.23994119380357, 44.47030636866752, 45.09521432360834, 46.21012800801103, 47.66477901586988, 48.77969270027258, 49.33233910792938, 50.29150677627555, 51.54295272674199, 52.502120395088156, 52.96253257560962, 53.7312913082454, 54.734306771122284, 55.503065503758066, 55.85460823743855, 56.40518899298025, 57.12354312403167, 57.67412387957337, 57.904129740744025, 58.216760726141125, 58.62465685689803, 58.93728784229513, 59.040286122086286, 59.11420756609124, 59.21065440724898, 59.28457585125393), color='#8C3F2D', stroke_width=1.4, opacity=0.9, dash=(), y2=(), point_size=26),), width=1311, height=465, x_domain=None, y_domain=None, marker_x=(), rule_y=(), y_axis_multiplier=None),), columns=1, panel_spacing=44, y_independent=False, margin_left=138, margin_right=28, margin_top=68, margin_bottom=94)"
- ]
- },
- "execution_count": 11,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "cia_optical_depth_chart = show_timed_plot(\n",
- " \"cia.plot.optical_depth()\",\n",
- " cia.plot.optical_depth,\n",
- ")\n",
- "cia_optical_depth_chart"
- ]
- },
{
"cell_type": "markdown",
"id": "d62108ca",
@@ -901,66 +714,6 @@
"## ISRF Curve\n"
]
},
- {
- "cell_type": "code",
- "execution_count": 12,
- "id": "7186191b",
- "metadata": {},
- "outputs": [
- {
- "name": "stdout",
- "output_type": "stream",
- "text": [
- "response.plot.curve(): 45.84 ms\n"
- ]
- },
- {
- "data": {
- "image/svg+xml": [
- ""
- ],
- "text/plain": [
- "SvgFigure(title='Instrument spectral response function (ISRF), 760.76000 nm', panels=(SvgPanel(title='Instrument spectral response function (ISRF), 760.76000 nm', x_title='Offset (nm)', y_title='Normalized ISRF', series=(SvgSeries(name='Instrument spectral response function (ISRF), 760.76000 nm', kind='line', x=(-0.11123316919247372, -0.10918791543542739, -0.1073768619967268, -0.10621434903100635, -0.10559186707837398, -0.10418322318002993, -0.10170243190191286, -0.09823858043296241, -0.0939167897383868, -0.08889324302651858, -0.08334949823711213, -0.07748591799838778, -0.07151442577344369, -0.06565084553471934, -0.06010710074531289, -0.05508355403344467, -0.05076176333886906, -0.04729791186991861, -0.04481712059180154, -0.04340847669345749, -0.04269422098946052, -0.041163066228023126, -0.03877771382553874, -0.03608389438386439, -0.033698541981380004, -0.03216738721994261, -0.031356262355984654, -0.029629246653598784, -0.026938766789157853, -0.023900361663436342, -0.02120988179899541, -0.01948286609660954, -0.01877172785577841, -0.017642204799699357, -0.01588254561715985, -0.013895332276547379, -0.01213567309400787, -0.011006150037928819, -0.009953949379337246, -0.006856935610130677, -0.0020321658209923044, 0.0034165295775210325, 0.008241299366659405, 0.011338313135865974, 0.012498249848817977, 0.014138595628878647, 0.017009142608912953, 0.020976341139089527, 0.025854773877313164, 0.031416351701182066, 0.03740102900371767, 0.04352897274782208, 0.04951365005035768, 0.055075227874226584, 0.05995366061245022, 0.0639208591426268, 0.0667914061226611, 0.06843175190272177, 0.06900592640965897, 0.06975001869557218, 0.07090922373902231, 0.0722183339065623, 0.07337753895001242, 0.07412163123592563, 0.07506547250625317, 0.0781126531622931, 0.08285978903802516, 0.08822081112441538, 0.09296794700014743, 0.09601512765618736, 0.09750260395173882, 0.1004755222378435, 0.10528322474465313, 0.11109574761655949), y=(0.00010096034504737405, 0.00018136434114670975, 0.00022869401455274374, 0.00014703393558372808, 0.00020015294304954144, 0.0006497477103985699, 0.001772189743410239, 0.0048676763588153295, 0.013271581872885203, 0.03411927949121363, 0.0786257254765997, 0.15676605728185786, 0.26609355939071855, 0.3846876803933454, 0.4788377810348634, 0.520191185262718, 0.49714148498660266, 0.4142742607574451, 0.2854565272674991, 0.1277681882211773, 0.14658501705239582, 0.3162132441354592, 0.4237315937851543, 0.4367505840794878, 0.34412213377510176, 0.16534682288657307, 0.18753752384533318, 0.39906350562322723, 0.524392679378388, 0.5300415866702758, 0.41137962601868794, 0.19597285271681908, 0.12830903769634963, 0.2705777417499938, 0.3515684650229959, 0.3520643285991631, 0.2716679719362232, 0.12906265297965863, 0.35396590682127144, 0.74565671118636, 0.9672429878195931, 0.9672368215873234, 0.7455609110027938, 0.3538389015340052, 0.18006695304387016, 0.4106502722564049, 0.6210855517755038, 0.798761688913723, 0.9300524693742136, 1.0, 0.9953446929549046, 0.9120804858307766, 0.7638724498877094, 0.5823366617706254, 0.4044017379034467, 0.2554609766845575, 0.1419512595853987, 0.055799226616911134, 0.02530396988104951, 0.050524543519785796, 0.06011451218008437, 0.054249727651035584, 0.03801002305097736, 0.016934223711511143, 0.06377317722154911, 0.10018333407924272, 0.07648503551136569, 0.037282681420604355, 0.013501804493636336, 0.003698875773214571, 0.0026715248904412565, 0.0031135624138736804, 0.001386015657656311, 0.0003137983943439322), color='#111111', stroke_width=1.4, opacity=1.0, dash=(), y2=(), point_size=28),), width=1311, height=465, x_domain=(-0.12123316919247372, 0.12109574761655949), y_domain=(0.0, 1.05), marker_x=(), rule_y=(), y_axis_multiplier=None),), columns=1, panel_spacing=44, y_independent=False, margin_left=138, margin_right=28, margin_top=68, margin_bottom=94)"
- ]
- },
- "execution_count": 12,
- "metadata": {},
- "output_type": "execute_result"
- }
- ],
- "source": [
- "isrf_chart = show_timed_plot(\"response.plot.curve()\", response.plot.curve)\n",
- "isrf_chart"
- ]
- },
{
"cell_type": "code",
"execution_count": null,
From 6ac4f3bc6d2fbf7eba5602f7062fb98efd58d94f Mon Sep 17 00:00:00 2001
From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com>
Date: Thu, 18 Jun 2026 13:26:42 +0200
Subject: [PATCH 3/4] document current pipeline boundaries
---
src/README.md | 191 +++++++++++++++-----------
src/cache/README.md | 32 +++--
src/cache/transport_worker_memory.zig | 26 ++--
src/instrumentation/trace.zig | 2 +-
src/optics/README.md | 5 +-
src/output/README.md | 82 +++++++++++
src/retrieval/README.md | 146 ++++++++++++++++++++
src/setup/README.md | 4 +-
src/spectrum/README.md | 13 +-
9 files changed, 385 insertions(+), 116 deletions(-)
create mode 100644 src/output/README.md
create mode 100644 src/retrieval/README.md
diff --git a/src/README.md b/src/README.md
index 36df74b9..06fb83e8 100644
--- a/src/README.md
+++ b/src/README.md
@@ -1,9 +1,18 @@
# `src/` — forward model and retrieval
-`zdisamar` computes the top-of-atmosphere reflectance spectrum across the oxygen
-A band (about 755–775 nm) and fits it for aerosol parameters, specifically the
-aerosol optical thickness and the aerosol layer height. This directory holds the
-whole model.
+`zdisamar` fits a measured spectrum of the oxygen A band (about 755-775 nm) for
+two aerosol properties: the aerosol optical thickness, how much aerosol is in the
+air, and the height of the aerosol layer, given as its mid-layer pressure. Oxygen
+is evenly mixed through the atmosphere at a known concentration, so its absorption
+near 760 nm measures how far light travelled before it reached the instrument.
+Aerosol shortens that path by scattering light back out at its own altitude, so
+the depth and shape of the band in a measured spectrum pin down how much aerosol
+there is and how high it sits.
+
+The model has two halves. The forward pass takes a described scene and computes
+the top-of-atmosphere reflectance spectrum it would produce. The retrieval runs
+that forward pass over and over, adjusting the aerosol state until the computed
+spectrum matches a measured one. This directory holds both.
Each subdirectory has its own README with the physics and the data flow for that
stage. This file shows how the stages fit together.
@@ -19,101 +28,127 @@ Scene -> prepare -> warmSessionMemory -> runForwardWithSessionMemory
Prepared SessionMemory SpectrumRunResult
```
-- A caller-provided or parsed JSON scene gives a `Scene` — atmosphere, geometry,
+- A caller-provided or parsed JSON scene gives a `Scene`: atmosphere, geometry,
surface, spectroscopy, instrument, aerosol.
- `prepare` builds the physics tables that stay fixed across runs of the same
scene.
- `warmSessionMemory` sets up the memory a run reuses.
-- `runForwardWithSessionMemory` runs one spectrum and returns the arrays: radiance,
- reflectance, irradiance, and the Jacobian. `runForward` does the same with
- throwaway memory, for a single run.
-
-A retrieval runs this forward pass many times — see below.
+- `runForwardWithSessionMemory` runs one spectrum and returns the arrays:
+ radiance, reflectance, irradiance, and the Jacobian. `runForward` does the same
+ with throwaway memory, for a one-off run.
## The forward pass
-Inside one forward pass, data flows one way:
+One forward pass is six stages, numbered the way each subdirectory README refers
+to itself. Data flows one way through them:
+
+1. [`input/`](input/README.md) reads and validates the scene into a `Scene`.
+2. [`setup/`](setup/README.md) builds the physics tables fixed for that scene:
+ absorption lines, CIA, aerosol, solar, phase, layers.
+3. [`optics/`](optics/README.md) turns those tables into per-layer optical depths
+ at one wavelength: Rayleigh, gas absorption, the O2-O2 continuum, aerosol.
+4. [`rtm/`](rtm/README.md) does the radiative transfer for one wavelength and
+ returns its reflectance and aerosol Jacobian.
+5. [`spectrum/`](spectrum/README.md) picks the wavelengths to evaluate, loops
+ `optics/` and `rtm/` over them, and averages the result through the instrument
+ slit.
+6. [`output/`](output) writes the spectrum and the diagnostics.
+
+`input/` and `setup/` run once per scene. Then `spectrum/` drives the rest: it
+loops the dense wavelength grid, calls `optics/` and `rtm/` at each wavelength,
+and gathers the per-wavelength results into the band.
```
-+-------+ +-------+ +--------+ +-----+ +----------+ +--------+
-| input | -> | setup | -> | optics | -> | rtm | -> | spectrum | -> | output |
-+-------+ +-------+ +--------+ +-----+ +----------+ +--------+
++----------+ +----------+
+| input/ | -> | setup/ | once per scene: validate the Scene
++----------+ +----------+ and build the fixed physics tables
+ |
+ v
++------------------------------------------------------+
+| spectrum/ loop the dense wavelength grid |
+| |
+| optics/ -> per-layer optical depths |
+| rtm/ -> reflectance + aerosol Jacobian |
+| |
++--------------------------+---------------------------+
+ |
+ v
+gather onto the product grid -> slit average -> output/
```
-- `input/` — read and check the scene into a `Scene`.
-- `setup/` — build the physics tables: absorption lines, CIA, aerosol, solar, phase, layers.
-- `optics/` — optical properties at each wavelength sample: Rayleigh, CIA, curved
- sun path, source levels.
-- `rtm/` — the radiative transfer for each wavelength sample, with Jacobians.
-- `spectrum/` — combine samples into radiance, average through the instrument slit.
-- `output/` — diagnostics and the written-out spectrum.
+`rtm/` works one wavelength at a time and has no notion of the band; `spectrum/`
+is the loop that turns those single wavelengths into a spectrum.
+
+## The retrieval loop
-`input/` and `setup/` run once. Then `spectrum/` loops the wavelengths, calling
-`optics/` and `rtm/` for each one, and averages the per-wavelength results into the
-spectrum:
+A retrieval inverts a measured spectrum. It is the Rodgers optimal-estimation
+loop in [`retrieval/`](retrieval), driven from `runOptimalEstimation` in
+`root.zig`. Each iteration runs one forward pass, compares the computed
+reflectance to the measurement, and updates the aerosol state from that mismatch
+and the Jacobian, then feeds the new state back in.
```
-input -> setup (once per scene)
+measurement + prior aerosol state
|
v
-spectrum/
+repeat until converged:
|
- | for each wavelength:
- | optics/ -> layer optical depths
- | rtm/ -> reflectance at that wavelength
- |
- v
-gather -> slit average -> product spectrum
+ | forward pass -> reflectance + Jacobian
+ | compare to the measurement
+ | Rodgers update -> new aerosol state
+ | write the state into the Scene, re-validate
|
v
-output
+retrieved aerosol optical depth + layer pressure,
+with posterior uncertainty
```
-`rtm/` works one wavelength at a time and has no notion of the band; `spectrum/` is
-the loop that turns those single wavelengths into a spectrum.
-
-A retrieval wraps the whole pass and repeats it. The rest of the directories support
-the flow:
-
-- `cache/` — the reused memory (see below).
-- `common/` — shared helpers: errors, hashing, math, units, memory, workers.
-- `assets/` — readers for the packaged reference data: HITRAN O2 line lists,
- O2–O2 CIA tables, solar spectra, and atmosphere profiles.
-- `api/` — the C entry points the Python package calls.
-- `validation/` — band metrics.
-- `instrumentation/` — tracing and telemetry hooks, off by default.
-
-## Reused memory
-
-A retrieval runs the forward model many times, and between runs only the aerosol
-state changes. Everything that does not depend on that state — the wavelength
-grid, the gas spectroscopy, the solar spectrum — is built once and passed back
-in, so each iteration stays cheap. `SessionMemory`
-(`cache/session_memory.zig`) holds these:
-
-- `SpectrumSamplingTable` (`cache/spectrum_memory.zig`) — the high-resolution
- wavelengths the model evaluates, placed densely around strong O2 lines and
- sparsely elsewhere. It depends on the spectral grid, instrument, and line
- positions, none of which change during a retrieval, so it is built once.
-- `RadianceWavelengthList` (`cache/radiance_memory.zig`) — the dense grid the
- radiative transfer loops over, derived from the sampling table. `rtm/`
- computes a radiance at each entry before the spectrum step gathers them onto
- the output grid.
-- `ProfileLineValues` (`cache/profile_line_memory.zig`) — the O2 absorption from
- summing every line at each wavelength, the most expensive part of setup.
- Aerosol changes do not touch the gas spectroscopy, so this is reused across
- iterations as long as the wavelengths match, which is what makes retrieval fast.
-- `SolarIrradianceMemory` (`cache/solar_irradiance_memory.zig`) — the incoming
- solar flux at each wavelength, needed to turn radiance into reflectance. It
- depends only on the grid, so it is looked up once and kept.
-- `TransportWorkArrays` (`cache/transport_worker_memory.zig`) — scratch space for
- the radiative transfer: layer matrices, scattering orders, and Fourier terms.
- These are large and overwritten on every sample, so they are sized once per
- worker thread and reused instead of reallocated each sample.
-
-A function in `optics/`, `rtm/`, `spectrum/`, or `retrieval/` takes the arrays it
-reads and the one it writes as arguments, so its inputs and outputs are visible
-in its signature.
+The state vector is fixed at two elements, the aerosol optical depth and the
+aerosol layer's mid-pressure, set in `rtm/jacobian_states.zig` and shared by
+every stage. Each iteration writes the updated state back into the `Scene` and
+re-runs the same validation as the first, so a retrieval step can never run an
+invalid scene.
+
+## Reused memory across iterations
+
+Between iterations only the aerosol state changes. Everything that does not depend
+on it, the wavelength grid, the gas spectroscopy, the solar spectrum, is built
+once and passed back in, so each iteration stays cheap. `SessionMemory`
+([`cache/session_memory.zig`](cache/session_memory.zig)) holds these:
+
+- [`SpectrumSamplingTable`](cache/spectrum_memory.zig) — the high-resolution
+ wavelengths to evaluate, dense around strong O2 lines and sparse elsewhere, with
+ the offsets and weights that average them down to each product wavelength.
+- [`RadianceWavelengthList`](cache/radiance_memory.zig) — the dense grid `rtm/`
+ loops over, the radiance at each entry, and the Jacobian column.
+- [`ProfileLineValues`](cache/profile_line_memory.zig) — the O2 absorption summed
+ from every line at each wavelength, the most expensive part of setup and the
+ largest table.
+- [`SolarIrradianceMemory`](cache/solar_irradiance_memory.zig) — the incoming
+ solar flux at each wavelength, needed to turn radiance into reflectance.
+- [`TransportWorkArrays`](cache/transport_worker_memory.zig) — per-worker scratch
+ for the radiative transfer: layer matrices, scattering orders, Fourier terms,
+ sized once per thread.
+
+Each table is rebuilt only when the inputs it depends on change; `prepareSessionRows`
+in `root.zig` hashes those inputs per table and reuses the table on a match.
+[`cache/`](cache/README.md) covers where each buffer lives and when it is touched.
+
+Because the tables are reused, the per-wavelength stages allocate nothing. A
+function in `optics/`, `rtm/`, `spectrum/`, or `retrieval/` takes the arrays it
+reads and the one it writes as arguments, so its inputs and outputs are visible in
+its signature.
+
+## Supporting directories
+
+- [`common/`](common) — shared helpers: errors, hashing, math, units, memory,
+ worker partition.
+- [`assets/`](assets) — readers for the packaged reference data: HITRAN O2 line
+ lists, O2-O2 CIA tables, solar spectra, atmosphere profiles.
+- [`api/`](api) — the C entry points the Python package calls.
+- [`validation/`](validation) — band metrics.
+- [`instrumentation/`](instrumentation) — tracing and telemetry hooks, off by
+ default.
## Where to start
diff --git a/src/cache/README.md b/src/cache/README.md
index ba1bbeeb..81c4ac2c 100644
--- a/src/cache/README.md
+++ b/src/cache/README.md
@@ -1,4 +1,4 @@
-# `cache/` — the memory a retrieval reuses
+# `cache/` — the memory system of zdisamar
A retrieval runs the forward model many times, and between runs only the aerosol
state changes. Everything that does not depend on that state, the wavelength grid,
@@ -88,20 +88,20 @@ a run wants no derivatives. A second stamp, `result_stamp`, records whether the
dense values are still valid and resets whenever the list changes or a column
grows, so stale values are never read against fresh storage.
-## Line cross-sections (`profile_line_memory.zig`)
+## Line cross-sections (`profile_line_memory.zig`, `profile_line_build.zig`)
-`profile_line_memory.zig` holds the largest table and the only arena in the
-subsystem. `ProfileLineValues` keeps the O2 line cross-section data: a dense
-support-profile sigma column, and, when diagnostics ask for it, a
-wavelength-by-layer-node grid. Building it sums Voigt and line-mixing terms over
-every line at every wavelength, the most expensive step in setup.
+`profile_line_memory.zig` holds the largest retained table in the subsystem.
+`ProfileLineValues` keeps the O2 line cross-section data: a dense support-profile
+sigma column, and, when diagnostics ask for it, a wavelength-by-layer-node grid.
+`profile_line_build.zig` builds that owner by summing Voigt and line-mixing terms
+over every line at every wavelength, the most expensive step in setup.
-The build runs only on a stamp miss. `profileLineReuseStamp` hashes the scene id,
-the exact wavelengths, the parsed line assets, and the fixed-node spectroscopy
-profile, and excludes the dynamic layer grid. This is possible because the line
-values sit at fixed spectroscopy nodes and are resampled per pressure state
-downstream, so the cache survives the pressure changes a retrieval makes each
-iteration.
+The build runs only on a stamp miss. `profile_line_build.profileLineReuseStamp`
+hashes the scene id, the exact wavelengths, the parsed line assets, and the
+fixed-node spectroscopy profile, and excludes the dynamic layer grid. This is
+possible because the line values sit at fixed spectroscopy nodes and are resampled
+per pressure state downstream, so the cache survives the pressure changes a
+retrieval makes each iteration.
The build's scratch runs through one `std.heap.ArenaAllocator`. The layer grid,
the line tables, the cutoff grid, the collected line lists, and every prepared
@@ -174,8 +174,10 @@ for nearly all of it:
the map of the directory.
- `common/memory.zig` — `ensureSliceCapacity`, the reuse primitive everything here
is built on.
-- `profile_line_memory.zig` — the line cross-section cache, the one arena, and the
- reuse stamp; read `profileLineReuseStamp` and the build function.
+- `profile_line_memory.zig` — the retained line cross-section owner and hot-path
+ support-row reader.
+- `profile_line_build.zig` — the build arena, reuse stamp, and line-summation
+ pipeline.
- `transport_worker_memory.zig` — the per-worker scratch and the geometry and
Fourier-basis caches.
- `prepareSessionRows` in `root.zig` — where the stamps are checked and the
diff --git a/src/cache/transport_worker_memory.zig b/src/cache/transport_worker_memory.zig
index 91d342d3..31d6d1d3 100644
--- a/src/cache/transport_worker_memory.zig
+++ b/src/cache/transport_worker_memory.zig
@@ -63,18 +63,20 @@ pub const Error = error{
// [ 272.. 287] layer_phase_row_valid : []bool |
// [ 288.. 303] plm_basis_cache : []FourierPlmBasis |
// [ 304.. 319] plm_basis_cache_valid : []bool |
-// [ 320.. 335] line_sigma_cm2_per_molecule : []f64 |
-// [ 336.. 351] support_optics : []SupportOptics |
-// [ 352.. 367] layer_optics : []LayerOptics |
-// [ 368.. 383] source_level_rows : []SourceLevel |
-// [ 384.. 399] curved_samples : []CurvedSunPathSample |
-// [ 400.. 415] curved_level_starts : []usize |
-// [ 416.. 431] curved_level_altitudes_km : []f64 |
-// [ 432..3263] cached_geometry : GaussGeometry |
-// [3264..3264] cached_geometry_valid : bool |
-// [3265..3271] normal build trailing padding : 7 B |
-// [3272..3727] cost build: stage_cost : WorkerStageCost |
-// [3728..3735] cost build: cost_timing_state : WorkspaceState |
+// [ 320.. 335] line_sigma_cm2_per_molecule : []f64 |
+// [ 336.. 351] support_optics : []SupportOptics |
+// [ 352.. 367] layer_optics : []LayerOptics |
+// [ 368.. 383] source_level_rows : []SourceLevel |
+// [ 384.. 399] curved_samples : []CurvedSunPathSample |
+// [ 400.. 415] curved_level_starts : []usize |
+// [ 416.. 431] curved_level_altitudes_km : []f64 |
+// [ 432..3263] cached_geometry : GaussGeometry |
+// ------------------------------------------------------:
+// [3264..3264] cached_geometry_valid : bool |
+// [3265..3271] normal build trailing padding : 7 B |
+// ------------------------------------------------------:
+// [3272..3727] cost build: stage_cost : WorkerStageCost |
+// [3728..3735] cost build: cost_timing_state : WorkspaceState |
// |
// referenced storage |
// Every slice owns heap storage and is released by deinit. Optics rows are borrowed by spectrum prefetch; |
diff --git a/src/instrumentation/trace.zig b/src/instrumentation/trace.zig
index 195e7f13..9bcfca43 100644
--- a/src/instrumentation/trace.zig
+++ b/src/instrumentation/trace.zig
@@ -14,7 +14,7 @@ const SourceLocation = std.builtin.SourceLocation;
// matrix builds, doubling decisions, q-series work, and fixed-kernel product gates. |
// rtm/scattering_orders.zig marks initial source setup, order transport, local source passes, retained |
// order accumulation, convergence exits, and paired Gaussian dot products. |
-// cache/profile_line_memory.zig and setup refresh code may add retained setup zones as land. |
+// cache/profile_line_build.zig and setup refresh code may add retained setup zones as land. |
// |
// primary paths |
// enabled is true only when build_options.enable_ztracy is present and true. build.zig supplies either the |
diff --git a/src/optics/README.md b/src/optics/README.md
index 042b980e..6a3b3536 100644
--- a/src/optics/README.md
+++ b/src/optics/README.md
@@ -49,8 +49,9 @@ layer into one `LayerOptics` row per RTM layer. That coarse row is what `rtm/`
reads.
The O2 line cross-sections come in as an argument, already worked out in
-`cache/profile_line_memory.zig`, so the slow line-by-line summation happens once
-upstream and this loop just reads the result. Both `SupportOptics` and
+`cache/profile_line_build.zig` and retained by `cache/profile_line_memory.zig`, so
+the slow line-by-line summation happens once upstream and this loop just reads the
+result. Both `SupportOptics` and
`LayerOptics` also carry Jacobian vectors, and `fillLayerAerosolJacobians` fills
the aerosol derivative lanes the retrieval needs.
diff --git a/src/output/README.md b/src/output/README.md
new file mode 100644
index 00000000..bdf1a4eb
--- /dev/null
+++ b/src/output/README.md
@@ -0,0 +1,82 @@
+# `output/` — results in the structures the caller reads
+
+This is where computed results become the stable structures the Python package
+reads across the C boundary. The radiative-transfer math is done upstream in
+`optics/`, `spectrum/`, and `rtm/`; `output/` shapes those results and owns the
+memory they sit in.
+
+One forward run returns a product spectrum, the arrays `runForward` hands back.
+Separately, one builder produces the atmospheric-budget diagnostic table on
+demand by re-deriving part of a prepared scene at wavelengths the caller chooses
+and laying it out as fixed rows. The diagnostic never runs during a forward pass;
+it explains a scene after the fact.
+
+```
+the forward run
+ |
+ v
+spectrum.zig -> the product arrays and an assembly summary
+ |
+ v
+SpectrumRunResult -> C ABI -> Python
+
+
+a prepared scene, at chosen wavelengths
+ |
+ v
+atmospheric_budget -> per-support-row optical depths and state
+ |
+ v
+fixed extern-struct rows -> C ABI -> Python
+```
+
+## The forward result (`spectrum.zig`)
+
+`Spectrum` is the output of one forward run: five arrays of equal length, one
+entry per product wavelength. They are the wavelength, the radiance, the
+irradiance, the reflectance, and the Jacobian, a two-element vector per sample.
+`SpectrumRunResult` pairs that with `SpectrumRunSummary`, a few scalars from the
+reflectance assembly. The file holds no physics. `root.zig` allocates these
+arrays, `spectrum/` fills them, and `Spectrum.deinit` frees them. The C layer
+keeps the result alive behind a handle and hands Python borrowed pointers into
+it.
+
+The Jacobian here is the derivative of reflectance, `dR/dx`, in the two retrieval
+lanes (aerosol optical depth, then aerosol layer mid-pressure in hPa). The
+retrieval reads it directly. The C boundary rescales it to a radiance derivative
+for the Python spectrum view, since reflectance is `pi * radiance / (mu0 * E0)`.
+
+## The diagnostic table
+
+The atmospheric-budget builder takes a prepared scene and a list of diagnostic
+wavelengths, re-derives one slice of the model at those wavelengths, and returns
+a flat table of `extern struct` rows whose byte layout is fixed and mirrored
+field-for-field by the Python `ctypes` struct. Rows are wavelength-major: the
+outer loop is the wavelength, then the support row. The table owns one heap
+allocation, freed by its `deinit` or the matching C free hook.
+
+These inspection paths re-derive from the setup tables rather than the warm
+session caches, so calling one does not share the forward run's cached
+spectroscopy, and they are meant to be called on demand rather than inside a loop.
+
+### Atmospheric budget (`atmospheric_budget.zig`)
+
+For each chosen wavelength and each support row of the column, one row carries the
+full optical-depth breakdown and the local atmospheric state: gas absorption, gas
+(Rayleigh) scattering, the O2-O2 continuum, aerosol scattering and absorption, the
+totals and the single-scatter albedo, alongside altitude, pressure, temperature,
+air and O2 number densities, and path length. It first builds a temporary line
+cross-section table for the requested wavelengths, then runs the same support-row
+optics build the forward model uses, `optics/`'s `fillSupportOpticsAtWavelength`,
+and reshapes each `SupportOptics` into a row. The aerosol absorption is the
+aerosol total minus its scattering, floored at zero. The absorbing gas is O2, so
+the absorber density equals the O2 density.
+
+## Where to start
+
+- `spectrum.zig` — the forward run's output type, the shortest file here.
+- `atmospheric_budget.zig` — the per-support-row diagnostic table.
+- `src/api/c.zig` — the C export (`zds_atmospheric_budget`) and how the table is
+ freed.
+- the parent `src/README.md` for how a forward run produces the spectrum these
+ types carry.
diff --git a/src/retrieval/README.md b/src/retrieval/README.md
new file mode 100644
index 00000000..75bfba2d
--- /dev/null
+++ b/src/retrieval/README.md
@@ -0,0 +1,146 @@
+# `retrieval/` — fitting the aerosol state to a measurement
+
+The forward pass computes the spectrum a scene would produce. The retrieval runs
+it backwards: given a measured reflectance spectrum, find the aerosol state that
+reproduces it. It is Rodgers optimal estimation over a fixed two-element state,
+the aerosol optical depth and the aerosol layer's mid-pressure in hPa. Each
+iteration runs one forward pass at the current state, weighs the mismatch against
+the measurement, and steps the state toward a better fit.
+
+The loop that calls the forward model lives in `src/root.zig`
+(`runOptimalEstimation`). This folder holds what that loop needs: the state
+vocabulary, the linear algebra of one step, the convergence test, and the owners
+that carry the result back to Python.
+
+```
+root.zig drives the loop:
+
+measurement (reflectance, variance) + prior aerosol state
+ |
+ v
+each iteration:
+ |
+ | forward pass (root.zig) -> reflectance + Jacobian
+ | accumulateNormalSystem -> whitened normal system G, b
+ | solveStep -> damped Rodgers step, new state
+ | clamp to bounds, record the iteration
+ | quadraticForm -> converged?
+ |
+ v
+invertSymmetric, multiply -> posterior covariance + averaging_kernel
+ |
+ v
+Result: retrieved state, its uncertainty, and the iteration history
+```
+
+## The state space (`retrieval/root.zig`)
+
+The state is two lanes, fixed for the whole model and shared with the forward
+Jacobian in `rtm/jacobian_states.zig`: lane 0 is the aerosol optical depth, lane 1
+is the aerosol layer's mid-pressure. Each lane is a `StateScalar` carrying its
+initial guess, its prior mean, its prior variance, and optional physical bounds.
+The variances form the diagonal prior covariance `Sa`. The measurement variances
+are the diagonal `Se`; their reciprocal is stored once as `inv_variance`, the
+`1/variance` the normal system reads.
+
+The radiative transfer returns the pressure lane's derivative in altitude, but the
+state is a pressure, so that lane also carries a `PressureAltitudeProfile`, a
+spline of `log(pressure)` against altitude built by `buildPressureProfile`.
+`altitudeDerivativeAtPressure` inverts that curve to get the altitude-versus-
+pressure slope (`d altitude / d pressure`, in km per hPa) at the current pressure,
+and scales the pressure-lane Jacobian by it, turning a per-altitude derivative
+into a per-pressure one.
+
+## One iteration (`retrieval/root.zig`)
+
+`accumulateNormalSystem` builds the normal system from one forward pass. It works
+in prior-whitened coordinates, where the prior covariance is the identity, so the
+update stays stable even though the two lanes have very different magnitudes (an
+optical depth near one, a pressure in the hundreds). Streaming every product
+wavelength, with residual `r = measured - modeled` weighted by `inv_variance`, it
+accumulates the whitened curvature `G = sqrt(Sa) * J^T Se^-1 J * sqrt(Sa)` and
+gradient `b = sqrt(Sa) * J^T Se^-1 r`, plus the reflectance chi-square and the
+unwhitened `J^T Se^-1 J` it keeps for the covariance step. The pressure-lane
+Jacobian column is scaled by the altitude-pressure slope above before it enters
+the sum.
+
+`solveStep` takes one damped step. It diagonalizes the symmetric 2x2 `G` with a
+closed-form Jacobi rotation (`algebra.jacobiEigenSymmetric`), rotates `b` and the
+current offset into the eigenbasis, and updates each eigenmode with
+Levenberg-Marquardt damping, `dx_k = (s * b_k + lambda_k * dx_k) / (lambda_k + 1)`
+with `lambda_k = s * eigenvalue_k`. The damping scale `s` is a step factor
+squared; if the resulting step is too large, the factor is cut by 0.75 and the
+step retried, so `s` drops by 0.5625 each try and the step is flagged as outside
+the trusted linear range, up to ten retries. The new physical state is
+`prior + sqrt(Sa) * V * dx`, and the same eigensystem gives the posterior
+precision in physical units.
+
+The driver loop in `src/root.zig` then clamps the state to the lane bounds and
+records the iteration. It converges when `quadraticForm(posterior_precision, dx)`
+divided by the two lanes falls below the threshold and the last step stayed in the
+linear range. After the loop, `invertSymmetric` turns the final precision into the
+posterior covariance, and `multiply` forms the `averaging_kernel`, the covariance
+times `J^T Se^-1 J`, which says how much of the answer comes from the measurement
+rather than the prior.
+
+## The 2x2 linear algebra (`algebra.zig`)
+
+Because the state is fixed at two lanes, every matrix is 2x2 and every vector has
+two entries. `algebra.zig` is that arithmetic, with no allocator and no dynamic
+dispatch:
+
+1. `choleskyLowerDiagonal` reduces to a per-lane square root, since `Sa` is
+ diagonal: it returns `sqrt(Sa)` and `1/sqrt(Sa)`, rejecting a non-positive
+ variance.
+2. `jacobiEigenSymmetric` diagonalizes the symmetric `G` in one closed-form
+ rotation, clamps the eigenvalues non-negative, and sorts them descending.
+3. `invertSymmetric` inverts a 2x2 by cofactors, rejecting a near-singular
+ determinant.
+4. `matrixVector`, `transposeMatrixVector`, and `multiply` are the products the
+ step, the covariance, and the averaging matrix need.
+
+Everything is a fixed stack value passed and returned by value; the file is held
+to that, with no allocator added unless the state count itself changes.
+
+## Results and the run variants
+
+`runOptimalEstimation` (in `src/root.zig`) returns a `Result`: the retrieved
+state, its posterior covariance and `averaging_kernel`, and a per-iteration
+history (the state, the chi-square split into its reflectance and state parts, the
+convergence metric, and the linear-range flag). Three more entry points wrap it:
+
+- `runOptimalEstimationCorrection` — exactly one step, for refining a state that
+ is already close.
+- `runOptimalEstimationBatch` — a loop over many starts that share one
+ measurement and scene, each with its own initial and prior state. An
+ out-of-memory error stops the batch; any other per-run error marks that run
+ failed and the loop goes on.
+- `runFastmodeOptimalEstimationBatch` — two stages, a cheap fit followed by a
+ full-physics correction seeded with the cheap result.
+
+`MeasuredReflectanceRows` holds the dense copy of one measurement, and the
+`Result`, `BatchResult`, and `FastmodeBatchResult` owners, all defined here in
+`retrieval/root.zig`, hold the arrays the C layer borrows back to Python. The
+retrieval math itself allocates nothing.
+
+## Warm memory across iterations
+
+The forward pass is the cost; the optimal-estimation arithmetic is a handful of
+2x2 operations. So a retrieval keeps the `SessionMemory` warm across every
+iteration (the sampling table, the line cross-sections, the solar flux, the
+per-worker scratch) and refreshes only what the new aerosol state changes.
+
+Writing the state back into the scene moves the target interval's pressure
+boundaries (the mid-pressure plus or minus half the layer thickness) and rebuilds
+the small aerosol table. The vertical grid is refilled in place by
+`refillFromPreparedProfiles` with no allocation, and the line cross-sections are
+reused when the wavelengths and the mode bits match, so each iteration pays for
+one forward pass and little else.
+
+## Where to start
+
+1. `root.zig`: `accumulateNormalSystem` and `solveStep`, one iteration end to end.
+2. `algebra.zig`: the 2x2 eigensolve and inverse the step is built on.
+3. `runOptimalEstimation` in `src/root.zig`: the loop that drives this folder.
+4. the parent `src/README.md` for how the forward pass feeds each iteration, and
+ `rtm/README.md` for where the Jacobians come from.
diff --git a/src/setup/README.md b/src/setup/README.md
index 8626c401..b13742ca 100644
--- a/src/setup/README.md
+++ b/src/setup/README.md
@@ -70,7 +70,7 @@ line parameters together with the LISA line-mixing relaxation matrix and the
strong-line sidecar, and carries the runtime line-filter controls (isotopes,
strength threshold, wavenumber cutoff, mixing factor). It does not compute
absorption here; the per-wavelength cross-section summation over every line is
-the expensive step and lives downstream in `cache/profile_line_memory.zig`. This
+the expensive step and lives downstream in `cache/profile_line_build.zig`. This
module's job is to load and organize the rows that summation reads.
`cia_table.zig` loads the O2-O2 collision-induced absorption coefficients from
@@ -121,7 +121,7 @@ here and then reused, so the rest of the model stays fast.
- `run_tables.zig` — the entry point and the full list of tables setup builds.
- `atmosphere_layers.zig` — the vertical discretization, the core of this stage.
-- `line_tables.zig` with `cache/profile_line_memory.zig` — where the line data is
+- `line_tables.zig` with `cache/profile_line_build.zig` — where the line data is
loaded and where it is later turned into absorption.
- the parent `src/README.md` for how these tables feed optics, rtm, and the
reused session memory.
diff --git a/src/spectrum/README.md b/src/spectrum/README.md
index 4354835a..48939df3 100644
--- a/src/spectrum/README.md
+++ b/src/spectrum/README.md
@@ -82,10 +82,10 @@ of the output; Performance covers how that stays race-free.
`line_physics.zig` is the oxygen A-band line-by-line spectroscopy: given a
temperature, a pressure, and a wavelength, it returns the O2 absorption cross-section
-per molecule. `cache/profile_line_memory.zig` sums it over the lines at every fine
-wavelength and caches the result; the per-wavelength loop reads that cache rather than
-calling this file, and `output/` calls it for per-line diagnostics. That is why it
-sits in the folder but not in the dataflow above.
+per molecule. `cache/profile_line_build.zig` sums it over the lines at every fine
+wavelength into `ProfileLineValues`; the per-wavelength loop reads that retained
+cache rather than calling this file. That is why it sits in the folder but not in
+the dataflow above.
Each oxygen line absorbs in a profile around its center wavelength, and its width
comes from two effects. Thermal motion of the molecules spreads it into a Gaussian
@@ -164,7 +164,7 @@ it down.
of calls is set by the sampling table, and the adaptive grid is the gate: by
sampling densely only where absorption varies fast, it keeps that count far below a
uniform fine grid.
-2. The O2 line summation (`line_physics.zig`, through `cache/profile_line_memory.zig`)
+2. The O2 line summation (`line_physics.zig`, through `cache/profile_line_build.zig`)
is the most expensive single build. For every fine wavelength and every profile
node it sums the contribution of every nearby line, each one a Voigt evaluation,
and the strong-line line-mixing prep builds a relaxation matrix once per node,
@@ -190,7 +190,8 @@ it down.
`rtm/`.
2. `sampling_table.zig`: how the fine grid is placed around the O2 lines.
3. `line_physics.zig`: the line-by-line O2 spectroscopy, read with
- `cache/profile_line_memory.zig`, which is where it is summed and cached.
+ `cache/profile_line_build.zig`, which is where it is summed into the retained
+ `ProfileLineValues` cache.
4. `instrument_average.zig`: the slit average, calibration, and reflectance.
5. the parent `src/README.md` for how this stage sits between `optics/`, `rtm/`, and
the retrieval.
From 27cbc45ac59da40ce3ca0daa0e6950eeb0596e1e Mon Sep 17 00:00:00 2001
From: bout3fiddy <11488427+bout3fiddy@users.noreply.github.com>
Date: Thu, 18 Jun 2026 15:00:35 +0200
Subject: [PATCH 4/4] update retained benchmark results
---
benchmark/results.json | 965 +++++++++++++----------------------------
1 file changed, 305 insertions(+), 660 deletions(-)
diff --git a/benchmark/results.json b/benchmark/results.json
index f8330d7c..e259b16d 100644
--- a/benchmark/results.json
+++ b/benchmark/results.json
@@ -4,22 +4,22 @@
"forward_fast_mode": {
"average_active_cores": {
"baseline_reference_run": {
- "max": 1.9595680699588713,
- "mean": 1.9595680699588713,
- "median": 1.9595680699588713,
- "min": 1.9595680699588713,
- "p90": 1.9595680699588713,
+ "max": 1.9607051623633953,
+ "mean": 1.9607051623633953,
+ "median": 1.9607051623633953,
+ "min": 1.9607051623633953,
+ "p90": 1.9607051623633953,
"runs": 1,
- "total": 1.9595680699588713
+ "total": 1.9607051623633953
},
"four_scene_fast_total": {
- "max": 1.935765296243262,
- "mean": 1.9322070917933563,
- "median": 1.9349577848458255,
- "min": 1.908357723847781,
- "p90": 1.9354636507011336,
+ "max": 1.9369156293064864,
+ "mean": 1.9360731984115787,
+ "median": 1.9360846156102884,
+ "min": 1.935384633280563,
+ "p90": 1.936663588187209,
"runs": 10,
- "total": 19.322070917933562
+ "total": 19.360731984115787
}
},
"kind": "forward",
@@ -27,22 +27,22 @@
"mode": "fast_mode",
"process_cpu_s": {
"baseline_reference_run_s": {
- "max": 3.850176000000001,
- "mean": 3.850176000000001,
- "median": 3.850176000000001,
- "min": 3.850176000000001,
- "p90": 3.850176000000001,
+ "max": 3.793938999999998,
+ "mean": 3.793938999999998,
+ "median": 3.793938999999998,
+ "min": 3.793938999999998,
+ "p90": 3.793938999999998,
"runs": 1,
- "total": 3.850176000000001
+ "total": 3.793938999999998
},
"four_scene_fast_total_s": {
- "max": 9.436226999999988,
- "mean": 9.405632099999996,
- "median": 9.399705000000008,
- "min": 9.383469000000005,
- "p90": 9.43296,
+ "max": 9.455247999999997,
+ "mean": 9.399367700000003,
+ "median": 9.411383500000007,
+ "min": 9.304975000000006,
+ "p90": 9.442291599999994,
"runs": 10,
- "total": 94.05632099999997
+ "total": 93.99367700000002
}
},
"residuals": {
@@ -93,46 +93,46 @@
},
"timing_s": {
"baseline_reference_run_s": {
- "max": 1.964808499906212,
- "mean": 1.964808499906212,
- "median": 1.964808499906212,
- "min": 1.964808499906212,
- "p90": 1.964808499906212,
+ "max": 1.9349869999969087,
+ "mean": 1.9349869999969087,
+ "median": 1.9349869999969087,
+ "min": 1.9349869999969087,
+ "p90": 1.9349869999969087,
"runs": 1,
- "total": 1.964808499906212
+ "total": 1.9349869999969087
},
"four_scene_fast_total_s": {
- "max": 4.9427824155427516,
- "mean": 4.867921541351825,
- "median": 4.8603268335573375,
- "min": 4.848407666664571,
- "p90": 4.884749553306028,
+ "max": 4.88546195800518,
+ "mean": 4.85486497490092,
+ "median": 4.862458520499786,
+ "min": 4.804873791996215,
+ "p90": 4.878576733003865,
"runs": 10,
- "total": 48.67921541351825
+ "total": 48.5486497490092
}
}
},
"forward_no_session": {
"average_active_cores": {
- "max": 1.9238180840989547,
- "mean": 1.8935161348684653,
- "median": 1.9223516531992337,
- "min": 1.6381322250107577,
- "p90": 1.9233515444126261,
+ "max": 1.9248672773607367,
+ "mean": 1.8794398107277768,
+ "median": 1.9238532813680953,
+ "min": 1.478868274874014,
+ "p90": 1.9244051651820338,
"runs": 10,
- "total": 18.93516134868465
+ "total": 18.794398107277768
},
"kind": "forward",
"label": "Forward, no session",
"mode": "no_session",
"process_cpu_s": {
- "max": 1.9667810000000001,
- "mean": 1.9352844999999999,
- "median": 1.9318954999999995,
- "min": 1.9268610000000006,
- "p90": 1.9420849999999998,
+ "max": 1.920749,
+ "mean": 1.8807350999999997,
+ "median": 1.8794899999999997,
+ "min": 1.8570049999999996,
+ "p90": 1.8933691999999993,
"runs": 10,
- "total": 19.352845
+ "total": 18.807350999999997
},
"residuals": {
"disamar_reference": {
@@ -163,46 +163,46 @@
}
},
"timing_s": {
- "max": 1.2006240827031434,
- "mean": 1.0246980749536305,
- "median": 1.0044917918276042,
- "min": 1.002010916825384,
- "p90": 1.031424982845783,
+ "max": 1.2987965409993194,
+ "mean": 1.0075848831998884,
+ "median": 0.9769171250009094,
+ "min": 0.9654141669998353,
+ "p90": 1.014188078500956,
"runs": 10,
- "total": 10.246980749536306
+ "total": 10.075848831998883
}
},
"forward_session": {
"average_active_cores": {
"cached_run": {
- "max": 1.0004582566986748,
- "mean": 0.9999458146992998,
- "median": 0.9999262971687768,
- "min": 0.9995393501128464,
- "p90": 1.0002942297212107,
+ "max": 1.0004378799709677,
+ "mean": 0.9996243744188386,
+ "median": 0.9995615211296693,
+ "min": 0.9991111658085099,
+ "p90": 1.000158800785082,
"runs": 10,
- "total": 9.999458146992998
+ "total": 9.996243744188385
},
- "first_cached_run": 1.9897431806058123,
- "first_use_total": 1.9217841073690336,
- "setup": 1.8963817434301875
+ "first_cached_run": 1.990657814521174,
+ "first_use_total": 1.9265476907225756,
+ "setup": 1.902893147359245
},
"kind": "forward",
"label": "Forward, session",
"mode": "session",
"process_cpu_s": {
"cached_run_s": {
- "max": 0.0015690000000034843,
- "mean": 0.001460000000001216,
- "median": 0.0014590000000005432,
- "min": 0.0013590000000007763,
- "p90": 0.001541100000000384,
+ "max": 0.001450999999999425,
+ "mean": 0.0013578000000006086,
+ "median": 0.0013455000000011097,
+ "min": 0.0012840000000018392,
+ "p90": 0.0014303000000005285,
"runs": 10,
- "total": 0.014600000000012159
+ "total": 0.013578000000006085
},
- "first_cached_run_s": 0.5441159999999989,
- "first_use_total_s": 1.9314899999999966,
- "setup_s": 1.3873739999999977
+ "first_cached_run_s": 0.5286759999999973,
+ "first_use_total_s": 1.8983569999999972,
+ "setup_s": 1.369681
},
"residuals": {
"vs_no_session": {
@@ -215,47 +215,47 @@
},
"timing_s": {
"cached_run_s": {
- "max": 0.001569374930113554,
- "mean": 0.0014600878581404685,
- "median": 0.0014586669858545065,
- "min": 0.0013586250133812428,
- "p90": 0.001541775418445468,
+ "max": 0.0014517080016958062,
+ "mean": 0.001358316699770512,
+ "median": 0.001345812999716145,
+ "min": 0.0012844999982917216,
+ "p90": 0.0014307082990853814,
"runs": 10,
- "total": 0.014600878581404686
+ "total": 0.01358316699770512
},
- "first_cached_run_s": 0.27346041705459356,
- "first_use_total_s": 1.0050504594109952,
- "setup_s": 0.7315900423564017
+ "first_cached_run_s": 0.26557854199927533,
+ "first_use_total_s": 0.9853672499993991,
+ "setup_s": 0.7197887080001237
}
},
"retrieval_fast_mode": {
"average_active_cores": {
"first_use_total": {
- "max": 1.9006525687748617,
- "mean": 1.898689435637888,
- "median": 1.8997844834127726,
- "min": 1.8930785212904726,
- "p90": 1.9003899937178343,
+ "max": 1.9334363280589624,
+ "mean": 1.932473960725962,
+ "median": 1.9322587713812105,
+ "min": 1.931537142657937,
+ "p90": 1.9332161266976287,
"runs": 10,
- "total": 18.98689435637888
+ "total": 19.32473960725962
},
"retrieval": {
- "max": 1.9288725327445093,
- "mean": 1.9267964132755135,
- "median": 1.9270069959661296,
- "min": 1.9210190930313076,
- "p90": 1.9288712198736575,
+ "max": 1.9337326005029263,
+ "mean": 1.9327475301866268,
+ "median": 1.93253739941427,
+ "min": 1.93180856660279,
+ "p90": 1.933502067705359,
"runs": 10,
- "total": 19.267964132755136
+ "total": 19.32747530186627
},
"setup": {
- "max": 1.878383229716903,
- "mean": 1.8751509106291313,
- "median": 1.87622926453257,
- "min": 1.8693083737053804,
- "p90": 1.878276103681468,
+ "max": 1.0025929200872576,
+ "mean": 0.9974709565213378,
+ "median": 0.9970832011581956,
+ "min": 0.9927773728019393,
+ "p90": 1.0022812722253716,
"runs": 10,
- "total": 18.751509106291312
+ "total": 9.974709565213377
}
},
"kind": "optimal_estimation",
@@ -263,31 +263,31 @@
"mode": "fast_mode",
"process_cpu_s": {
"first_use_total_s": {
- "max": 2.183932999999996,
- "mean": 2.1637756999999964,
- "median": 2.16078899999998,
- "min": 2.1572409999999707,
- "p90": 2.1722446999999874,
+ "max": 1.0031219999999905,
+ "mean": 1.0021997999999912,
+ "median": 1.0022084999999947,
+ "min": 1.0013659999999902,
+ "p90": 1.002946499999973,
"runs": 10,
- "total": 21.637756999999965
+ "total": 10.021997999999911
},
"retrieval_s": {
- "max": 1.0145960000000116,
- "mean": 1.0008668999999997,
- "median": 0.9992569999999859,
- "min": 0.996422999999993,
- "p90": 1.005939799999996,
+ "max": 1.002966999999984,
+ "mean": 1.002048499999995,
+ "median": 1.002048000000002,
+ "min": 1.0012209999999868,
+ "p90": 1.0027968999999786,
"runs": 10,
- "total": 10.008668999999998
+ "total": 10.020484999999951
},
"setup_s": {
- "max": 1.1693369999999845,
- "mean": 1.1629087999999967,
- "median": 1.1622770000000031,
- "min": 1.1587100000000135,
- "p90": 1.1663048999999917,
+ "max": 0.0001629999999863685,
+ "mean": 0.00015129999999601295,
+ "median": 0.0001505000000037171,
+ "min": 0.00014399999997749546,
+ "p90": 0.0001584999999977299,
"runs": 10,
- "total": 11.629087999999967
+ "total": 0.0015129999999601296
}
},
"residuals": {
@@ -296,62 +296,62 @@
},
"timing_s": {
"first_use_total_s": {
- "max": 1.153641000855714,
- "mean": 1.1396208001300692,
- "median": 1.1371017289347947,
- "min": 1.135196874383837,
- "p90": 1.1438435254152863,
+ "max": 0.5192377500025032,
+ "mean": 0.518609804100197,
+ "median": 0.5184811250001076,
+ "min": 0.5180004170033499,
+ "p90": 0.5191386743976182,
"runs": 10,
- "total": 11.396208001300693
+ "total": 5.186098041001969
},
"retrieval_s": {
- "max": 0.5281550837680697,
- "mean": 0.5194493168033659,
- "median": 0.5183592080138624,
- "min": 0.516583124641329,
- "p90": 0.5222717840224504,
+ "max": 0.5190876660017238,
+ "mean": 0.5184581166999124,
+ "median": 0.5183323125002062,
+ "min": 0.517854667003121,
+ "p90": 0.5189837537996936,
"runs": 10,
- "total": 5.1944931680336595
+ "total": 5.1845811669991235
},
"setup_s": {
- "max": 0.6254859170876443,
- "mean": 0.6201714833267034,
- "median": 0.6193227500189096,
- "min": 0.6172903338447213,
- "p90": 0.6231065043248236,
+ "max": 0.00016370800221920945,
+ "mean": 0.0001516874002845725,
+ "median": 0.0001515420008217916,
+ "min": 0.00014441700113820843,
+ "p90": 0.00015864549968682695,
"runs": 10,
- "total": 6.201714833267033
+ "total": 0.001516874002845725
}
}
},
"retrieval_session": {
"average_active_cores": {
"first_use_total": {
- "max": 1.9482760504465428,
- "mean": 1.947140383715044,
- "median": 1.9470003712372859,
- "min": 1.9460349290914665,
- "p90": 1.9479394551311018,
+ "max": 1.9492420599130753,
+ "mean": 1.9481113944442487,
+ "median": 1.9479896378962462,
+ "min": 1.9477004965784386,
+ "p90": 1.9484465880090982,
"runs": 10,
- "total": 19.47140383715044
+ "total": 19.481113944442487
},
"retrieval": {
- "max": 1.9813520873214674,
- "mean": 1.9805844077966452,
- "median": 1.9805606878081667,
- "min": 1.9798507624031743,
- "p90": 1.9812075721971327,
+ "max": 1.983652947555797,
+ "mean": 1.9819245359579543,
+ "median": 1.9818060787000633,
+ "min": 1.9809877972291974,
+ "p90": 1.9823790096023168,
"runs": 10,
- "total": 19.80584407796645
+ "total": 19.819245359579543
},
"setup": {
- "max": 1.9002655636015189,
- "mean": 1.8976577192787272,
- "median": 1.897440416529498,
- "min": 1.895778570655443,
- "p90": 1.8989136029953497,
+ "max": 1.8990620886059246,
+ "mean": 1.8987123643648096,
+ "median": 1.8987249535348427,
+ "min": 1.8983314766499688,
+ "p90": 1.8988696303546417,
"runs": 10,
- "total": 18.97657719278727
+ "total": 18.987123643648097
}
},
"kind": "optimal_estimation",
@@ -359,31 +359,31 @@
"mode": "session",
"process_cpu_s": {
"first_use_total_s": {
- "max": 3.5642609999999877,
- "mean": 3.551356899999996,
- "median": 3.5502264999999937,
- "min": 3.544240000000002,
- "p90": 3.5592704999999767,
+ "max": 3.566330999999991,
+ "mean": 3.5611297000000035,
+ "median": 3.56074000000001,
+ "min": 3.5562929999999824,
+ "p90": 3.5650899000000296,
"runs": 10,
- "total": 35.51356899999996
+ "total": 35.611297000000036
},
"retrieval_s": {
- "max": 2.1677500000000123,
- "mean": 2.1555191000000007,
- "median": 2.154267500000003,
- "min": 2.149449000000004,
- "p90": 2.1605337999999934,
+ "max": 2.156645999999995,
+ "mean": 2.1507607000000006,
+ "median": 2.1503299999999967,
+ "min": 2.1454069999999774,
+ "p90": 2.1546380999999997,
"runs": 10,
- "total": 21.555191000000008
+ "total": 21.507607000000007
},
"setup_s": {
- "max": 1.3995789999999886,
- "mean": 1.3958377999999954,
- "median": 1.3954845000000091,
- "min": 1.393981999999994,
- "p90": 1.3973874999999965,
+ "max": 1.410886000000005,
+ "mean": 1.4103690000000029,
+ "median": 1.4104670000000112,
+ "min": 1.409684999999996,
+ "p90": 1.4107033000000058,
"runs": 10,
- "total": 13.958377999999954
+ "total": 14.103690000000029
}
},
"residuals": {
@@ -393,53 +393,53 @@
},
"timing_s": {
"first_use_total_s": {
- "max": 1.8300513750873506,
- "mean": 1.8238829955458642,
- "median": 1.8236639581155032,
- "min": 1.8203542078845203,
- "p90": 1.8269428121857345,
+ "max": 1.8297585420004907,
+ "mean": 1.827990654300811,
+ "median": 1.8280602079994424,
+ "min": 1.8256723339982273,
+ "p90": 1.8297247920021618,
"runs": 10,
- "total": 18.23882995545864
+ "total": 18.27990654300811
},
"retrieval_s": {
- "max": 1.0941647910512984,
- "mean": 1.0883247330784798,
- "median": 1.0877012708224356,
- "min": 1.0852649160660803,
- "p90": 1.0911868412513286,
+ "max": 1.087209334000363,
+ "mean": 1.0851877710003464,
+ "median": 1.0853002289986762,
+ "min": 1.0825805839995155,
+ "p90": 1.0871293086998777,
"runs": 10,
- "total": 10.883247330784798
+ "total": 10.851877710003464
},
"setup_s": {
- "max": 0.7366091660223901,
- "mean": 0.7355582624673843,
- "median": 0.7353832710068673,
- "min": 0.7345859999768436,
- "p90": 0.7365267413202673,
+ "max": 0.7430917499987117,
+ "mean": 0.7428028833004646,
+ "median": 0.7428557290022582,
+ "min": 0.7423894999992626,
+ "p90": 0.7429920002989092,
"runs": 10,
- "total": 7.355582624673843
+ "total": 7.428028833004646
}
}
},
"retrieval_sweep_fast_mode": {
"average_active_cores": {
"first_use_total": {
- "max": 1.9292465704217443,
- "mean": 1.923083279928195,
- "median": 1.9211929736299802,
- "min": 1.9171923347523585,
- "p90": 1.9290186965376586,
+ "max": 1.964806187433832,
+ "mean": 1.9575979714205172,
+ "median": 1.9542254571726028,
+ "min": 1.95304830836231,
+ "p90": 1.963954250212518,
"runs": 5,
- "total": 9.615416399640974
+ "total": 9.787989857102586
},
"retrieval": {
- "max": 1.9614083043821908,
- "mean": 1.9557679195959312,
- "median": 1.95418389924391,
- "min": 1.950820218263699,
- "p90": 1.9607246854018578,
+ "max": 1.9649419685791005,
+ "mean": 1.9577674424318476,
+ "median": 1.954413237021781,
+ "min": 1.9532330650804648,
+ "p90": 1.9640909304213212,
"runs": 5,
- "total": 9.778839597979657
+ "total": 9.788837212159239
}
},
"converged_count": 5,
@@ -448,24 +448,24 @@
"mode": "fast_mode",
"process_cpu_s": {
"first_use_total_s": {
- "max": 3.1942820000000154,
- "mean": 2.8719246000000114,
- "median": 2.7363670000000297,
- "min": 2.6018459999999948,
- "p90": 3.1803860000000044,
+ "max": 2.0293960000000197,
+ "mean": 1.7151502000000107,
+ "median": 1.5837319999999977,
+ "min": 1.449465000000032,
+ "p90": 2.0204156000000126,
"runs": 5,
- "total": 14.359623000000056
+ "total": 8.575751000000054
},
"retrieval_s": {
- "max": 2.028275000000008,
- "mean": 1.7105520000000012,
- "median": 1.5753270000000157,
- "min": 1.4429409999999905,
- "p90": 2.016867400000001,
+ "max": 2.0292510000000163,
+ "mean": 1.7149998000000095,
+ "median": 1.5835749999999962,
+ "min": 1.4493100000000254,
+ "p90": 2.0202702000000103,
"runs": 5,
- "total": 8.552760000000006
+ "total": 8.574999000000048
},
- "total_process_cpu_s": 14.359623000000056
+ "total_process_cpu_s": 8.575751000000054
},
"residuals": {
"truth_aerosol_mid_pressure_abs_error_hpa": {
@@ -492,45 +492,45 @@
"run_count": 5,
"timing_s": {
"first_use_total_s": {
- "max": 1.6557147484272718,
- "mean": 1.4930677577853202,
- "median": 1.4243061668239534,
- "min": 1.3571126656606793,
- "p90": 1.6487054323777557,
+ "max": 1.0328733759997704,
+ "mean": 0.8758325749993674,
+ "median": 0.8109026249985618,
+ "min": 0.7420848330002627,
+ "p90": 1.0287461256004462,
"runs": 5,
- "total": 7.465338788926601
+ "total": 4.379162874996837
},
"retrieval_s": {
- "max": 1.034091165754944,
- "mean": 0.8743570582009852,
- "median": 0.8061303752474487,
- "min": 0.7396586248651147,
- "p90": 1.0286307995207609,
+ "max": 1.0327282090001972,
+ "mean": 0.8756816670000263,
+ "median": 0.8107455419994949,
+ "min": 0.7419291250007518,
+ "p90": 1.0286005090005346,
"runs": 5,
- "total": 4.371785291004926
+ "total": 4.378408335000131
},
- "total_wall_s": 7.465338788926601
+ "total_wall_s": 4.379162874996837
}
},
"retrieval_sweep_session": {
"average_active_cores": {
"first_use_total": {
- "max": 1.9792287673415523,
- "mean": 1.9719103497840087,
- "median": 1.9696950956809465,
- "min": 1.9681633728050625,
- "p90": 1.9770847753977279,
+ "max": 1.9810011604178175,
+ "mean": 1.9740646906446269,
+ "median": 1.9705482669741885,
+ "min": 1.9694285070811117,
+ "p90": 1.9804910250431587,
"runs": 5,
- "total": 9.859551748920044
+ "total": 9.870323453223135
},
"retrieval": {
- "max": 1.9938274039832375,
- "mean": 1.9910007936208967,
- "median": 1.9909385235456762,
- "min": 1.987433716155894,
- "p90": 1.993138215758069,
+ "max": 1.9949164268209958,
+ "mean": 1.9931918666633366,
+ "median": 1.9929731472687364,
+ "min": 1.9919318602548088,
+ "p90": 1.9945809557266097,
"runs": 5,
- "total": 9.955003968104483
+ "total": 9.965959333316682
}
},
"converged_count": 5,
@@ -539,24 +539,24 @@
"mode": "session",
"process_cpu_s": {
"first_use_total_s": {
- "max": 10.24295699999999,
- "mean": 7.715566200000012,
- "median": 6.368831,
- "min": 5.869263000000018,
- "p90": 10.124553000000002,
+ "max": 10.23378199999999,
+ "mean": 7.695310200000006,
+ "median": 6.3864309999999875,
+ "min": 5.890625,
+ "p90": 10.067394000000007,
"runs": 5,
- "total": 38.57783100000006
+ "total": 38.47655100000003
},
"retrieval_s": {
- "max": 8.839351999999991,
- "mean": 6.316100400000005,
- "median": 4.974997000000002,
- "min": 4.474050000000005,
- "p90": 8.719159599999994,
+ "max": 8.819894000000005,
+ "mean": 6.282678800000008,
+ "median": 4.974661999999995,
+ "min": 4.477193,
+ "p90": 8.654316800000009,
"runs": 5,
- "total": 31.580502000000024
+ "total": 31.41339400000004
},
- "total_process_cpu_s": 38.57783100000006
+ "total_process_cpu_s": 38.47655100000003
},
"residuals": {
"truth_aerosol_mid_pressure_abs_error_hpa": {
@@ -583,24 +583,24 @@
"run_count": 5,
"timing_s": {
"first_use_total_s": {
- "max": 5.175226415973157,
- "mean": 3.910810708347708,
- "median": 3.2334095840342343,
- "min": 2.981446583289653,
- "p90": 5.120861932914704,
+ "max": 5.165964666997752,
+ "mean": 3.895696125199902,
+ "median": 3.240941167001438,
+ "min": 2.991032666999672,
+ "p90": 5.083249850197899,
"runs": 5,
- "total": 19.55405354173854
+ "total": 19.47848062599951
},
"retrieval_s": {
- "max": 4.433358666021377,
- "mean": 3.1724465833045543,
- "median": 2.4991195420734584,
- "min": 2.2458912921138108,
- "p90": 4.378587432857603,
+ "max": 4.42118470800051,
+ "mean": 3.151562941600423,
+ "median": 2.4972448750013427,
+ "min": 2.2464893750002375,
+ "p90": 4.338894024799811,
"runs": 5,
- "total": 15.862232916522771
+ "total": 15.757814708002115
},
- "total_wall_s": 19.55405354173854
+ "total_wall_s": 19.47848062599951
}
}
},
@@ -628,382 +628,37 @@
"runs": 5
},
"first_use_total_speedup_s": {
- "max": 3.5370349576696754,
- "max_abs": 3.5370349576696754,
- "mean": 2.4177429505623875,
- "median": 1.809103417210281,
- "min": 1.6243339176289737,
+ "max": 4.143409416996292,
+ "max_abs": 4.143409416996292,
+ "mean": 3.0198635502005344,
+ "median": 2.430038542002876,
+ "min": 2.2489478339994093,
"runs": 5
},
"retrieval_speedup_s": {
- "max": 3.412918415851891,
- "max_abs": 3.412918415851891,
- "mean": 2.298089525103569,
- "median": 1.6929891668260098,
- "min": 1.506232667248696,
+ "max": 3.398775748999469,
+ "max_abs": 3.398775748999469,
+ "mean": 2.2758812746003967,
+ "median": 1.6864993330018478,
+ "min": 1.5045602499994857,
"runs": 5
},
- "total_retrieval_wall_s_saved": 12.088714752811939
+ "total_retrieval_wall_s_saved": 15.099317751002673
}
},
- "created_at_unix_s": 1781712300.413607,
- "finished_at_unix_s": 1781712436.118529,
+ "created_at_unix_s": 1781787483.2323399,
+ "finished_at_unix_s": 1781787608.8886158,
"memory": {
- "peak_rss_bytes": 72712192,
- "peak_rss_delta_bytes": 34504704,
- "peak_rss_delta_mib": 32.90625,
- "peak_rss_mib": 69.34375,
+ "peak_rss_bytes": 72302592,
+ "peak_rss_delta_bytes": 34439168,
+ "peak_rss_delta_mib": 32.84375,
+ "peak_rss_mib": 68.953125,
"peak_rss_source": "resource.getrusage(RUSAGE_SELF).ru_maxrss",
"peak_rss_supported": true,
- "start_peak_rss_bytes": 38207488,
- "start_peak_rss_mib": 36.4375
+ "start_peak_rss_bytes": 37863424,
+ "start_peak_rss_mib": 36.109375
},
"memory_layout": {
- "instrument_sampling": {
- "boundary": "post-timing diagnostic; excluded from benchmark wall time and peak RSS",
- "channel_stats": {
- "irradiance": {
- "kernel_count": 301,
- "side_kernel_count": 301,
- "side_sample_count": 65507,
- "support_count_histogram": {
- "151": 1,
- "163": 1,
- "169": 1,
- "178": 2,
- "179": 7,
- "180": 2,
- "181": 6,
- "182": 33,
- "183": 2,
- "184": 3,
- "185": 11,
- "186": 6,
- "187": 10,
- "188": 6,
- "189": 13,
- "190": 13,
- "191": 10,
- "192": 2,
- "193": 7,
- "194": 7,
- "195": 6,
- "196": 4,
- "197": 10,
- "198": 6,
- "199": 6,
- "200": 6,
- "201": 5,
- "202": 5,
- "203": 7,
- "204": 2,
- "205": 2,
- "206": 4,
- "207": 1,
- "208": 2,
- "209": 3,
- "210": 6,
- "211": 1,
- "212": 2,
- "213": 6,
- "214": 2,
- "215": 4,
- "216": 1,
- "217": 3,
- "218": 3,
- "219": 2,
- "221": 3,
- "222": 1,
- "223": 3,
- "224": 4,
- "225": 1,
- "226": 1,
- "227": 2,
- "230": 1,
- "234": 1,
- "237": 2,
- "243": 1,
- "244": 1,
- "245": 1,
- "249": 1,
- "254": 1,
- "255": 1,
- "256": 1,
- "257": 1,
- "261": 1,
- "266": 1,
- "270": 1,
- "280": 1,
- "281": 1,
- "283": 1,
- "305": 1,
- "310": 1,
- "313": 1,
- "319": 1,
- "335": 1,
- "345": 1,
- "353": 1,
- "355": 1,
- "374": 1,
- "378": 1,
- "392": 1,
- "409": 2,
- "433": 1,
- "438": 1,
- "440": 1,
- "455": 1,
- "466": 1,
- "475": 2,
- "487": 1,
- "489": 1,
- "494": 1,
- "496": 1,
- "512": 1,
- "518": 1
- },
- "support_count_stats": {
- "count": 301,
- "max": 518,
- "mean": 217.6312292358804,
- "median": 196.0,
- "min": 151,
- "p90": 266.0
- }
- },
- "radiance": {
- "kernel_count": 301,
- "side_kernel_count": 301,
- "side_sample_count": 65507,
- "support_count_histogram": {
- "151": 1,
- "163": 1,
- "169": 1,
- "178": 2,
- "179": 7,
- "180": 2,
- "181": 6,
- "182": 33,
- "183": 2,
- "184": 3,
- "185": 11,
- "186": 6,
- "187": 10,
- "188": 6,
- "189": 13,
- "190": 13,
- "191": 10,
- "192": 2,
- "193": 7,
- "194": 7,
- "195": 6,
- "196": 4,
- "197": 10,
- "198": 6,
- "199": 6,
- "200": 6,
- "201": 5,
- "202": 5,
- "203": 7,
- "204": 2,
- "205": 2,
- "206": 4,
- "207": 1,
- "208": 2,
- "209": 3,
- "210": 6,
- "211": 1,
- "212": 2,
- "213": 6,
- "214": 2,
- "215": 4,
- "216": 1,
- "217": 3,
- "218": 3,
- "219": 2,
- "221": 3,
- "222": 1,
- "223": 3,
- "224": 4,
- "225": 1,
- "226": 1,
- "227": 2,
- "230": 1,
- "234": 1,
- "237": 2,
- "243": 1,
- "244": 1,
- "245": 1,
- "249": 1,
- "254": 1,
- "255": 1,
- "256": 1,
- "257": 1,
- "261": 1,
- "266": 1,
- "270": 1,
- "280": 1,
- "281": 1,
- "283": 1,
- "305": 1,
- "310": 1,
- "313": 1,
- "319": 1,
- "335": 1,
- "345": 1,
- "353": 1,
- "355": 1,
- "374": 1,
- "378": 1,
- "392": 1,
- "409": 2,
- "433": 1,
- "438": 1,
- "440": 1,
- "455": 1,
- "466": 1,
- "475": 2,
- "487": 1,
- "489": 1,
- "494": 1,
- "496": 1,
- "512": 1,
- "518": 1
- },
- "support_count_stats": {
- "count": 301,
- "max": 518,
- "mean": 217.6312292358804,
- "median": 196.0,
- "min": 151,
- "p90": 266.0
- }
- }
- },
- "current_owned_wavelength_sampling_bytes": 2156472,
- "current_row_payload_bytes": 60200,
- "current_side_storage_bytes": 2096224,
- "estimated_row_payload_reduction_ratio": 0.8907763243147309,
- "estimated_row_payload_saved_bytes": 17586768,
- "estimated_row_payload_saved_mib": 16.772048950195312,
- "estimated_sampling_worker_count": 2,
- "inline_kernel_count": 0,
- "inline_threshold_samples": 5,
- "integration_kernel_scratch_bytes_at_worker_count": 65568,
- "integration_kernel_scratch_bytes_per_worker": 32784,
- "integration_mode_histogram": {
- "disamar_hr_grid": 602
- },
- "kernel_count": 602,
- "nominal_sample_count": 301,
- "previous_full_kernel_row_payload_bytes": 19743192,
- "side_kernel_count": 602,
- "side_sample_count": 131014,
- "source": "instrument_response_sampling over benchmark forward_case nominal grid",
- "support_count_histogram": {
- "151": 2,
- "163": 2,
- "169": 2,
- "178": 4,
- "179": 14,
- "180": 4,
- "181": 12,
- "182": 66,
- "183": 4,
- "184": 6,
- "185": 22,
- "186": 12,
- "187": 20,
- "188": 12,
- "189": 26,
- "190": 26,
- "191": 20,
- "192": 4,
- "193": 14,
- "194": 14,
- "195": 12,
- "196": 8,
- "197": 20,
- "198": 12,
- "199": 12,
- "200": 12,
- "201": 10,
- "202": 10,
- "203": 14,
- "204": 4,
- "205": 4,
- "206": 8,
- "207": 2,
- "208": 4,
- "209": 6,
- "210": 12,
- "211": 2,
- "212": 4,
- "213": 12,
- "214": 4,
- "215": 8,
- "216": 2,
- "217": 6,
- "218": 6,
- "219": 4,
- "221": 6,
- "222": 2,
- "223": 6,
- "224": 8,
- "225": 2,
- "226": 2,
- "227": 4,
- "230": 2,
- "234": 2,
- "237": 4,
- "243": 2,
- "244": 2,
- "245": 2,
- "249": 2,
- "254": 2,
- "255": 2,
- "256": 2,
- "257": 2,
- "261": 2,
- "266": 2,
- "270": 2,
- "280": 2,
- "281": 2,
- "283": 2,
- "305": 2,
- "310": 2,
- "313": 2,
- "319": 2,
- "335": 2,
- "345": 2,
- "353": 2,
- "355": 2,
- "374": 2,
- "378": 2,
- "392": 2,
- "409": 4,
- "433": 2,
- "438": 2,
- "440": 2,
- "455": 2,
- "466": 2,
- "475": 4,
- "487": 2,
- "489": 2,
- "494": 2,
- "496": 2,
- "512": 2,
- "518": 2
- },
- "support_count_stats": {
- "count": 602,
- "max": 518,
- "mean": 217.6312292358804,
- "median": 196.0,
- "min": 151,
- "p90": 266.0
- },
- "support_sample_count": 131014
- },
"optical_accumulation": {
"benchmark_profile_node_count": 47,
"boundary": "post-timing diagnostic; excluded from benchmark wall time and peak RSS",
@@ -1021,7 +676,7 @@
},
"native_binding": {
"command": "zig build sync-python-package -Doptimize=ReleaseFast",
- "elapsed_s": 0.12126141740009189
+ "elapsed_s": 0.1257055419991957
},
"output": "benchmark/results.json",
"report": {
@@ -1029,40 +684,30 @@
{
"case": "Forward, no session",
"residuals": "DISAMAR fixture worst interior max_abs 9.569e-14; RTM reflectance 5.390e-14",
- "timing": "median 1.004s"
+ "timing": "median 0.977s"
},
{
"case": "Forward, session",
"residuals": "vs no-session: radiance max_abs 0.000e+00; reflectance max_abs 0.000e+00",
- "timing": "setup 0.732s; cached run 0.001s; first-use 1.005s"
+ "timing": "setup 0.720s; cached run 0.001s; first-use 0.985s"
},
{
"case": "Forward, fast mode",
"residuals": "baseline max_abs 2.203e-04; 4-scene worst 4.969e-04, 1.601x noise",
- "timing": "baseline reference 1.965s; 4-scene fast median 4.860s"
+ "timing": "baseline reference 1.935s; 4-scene fast median 4.862s"
},
{
"case": "OE, session",
"residuals": "AOD diff 8.699e-08; mid-pressure diff 1.733e-04 hPa",
- "timing": "retrieval 1.087701s; setup 0.735383s"
+ "timing": "retrieval 1.085300s; setup 0.742856s"
},
{
"case": "OE, fast mode",
"residuals": "single fast-vs-session AOD delta -4.294e-06; sweep max AOD delta 2.719e-04; pressure delta 1.744e-01 hPa",
- "timing": "5-case median 1.424306s; mean 1.493068s; range 1.357113-1.655715s"
+ "timing": "5-case median 0.810903s; mean 0.875833s; range 0.742085-1.032873s"
}
],
"memory_layout_rows": [
- {
- "area": "Instrument sampling plan",
- "memory": "current owned plan 2156472 B; estimated row payload saved 16.77 MiB",
- "shape": "301 wavelengths, 602 channel kernels, support count median 196.0, max 518"
- },
- {
- "area": "Integration kernel scratch",
- "memory": "65568 B transient",
- "shape": "2 sampling worker(s), 32784 B each"
- },
{
"area": "Collision-complex profile cache",
"memory": "request cache 2072 -> 1048 B; sample spline scratch 10240 -> 0 B",
@@ -1073,11 +718,11 @@
{
"benchmark": "Full benchmark process",
"process_cpu": "see top-level totals",
- "process_memory": "peak RSS 69.3 MiB; delta since benchmark start 32.9 MiB"
+ "process_memory": "peak RSS 69.0 MiB; delta since benchmark start 32.8 MiB"
},
{
"benchmark": "Forward, no session",
- "process_cpu": "median CPU 1.932s; median active cores 1.92",
+ "process_cpu": "median CPU 1.879s; median active cores 1.92",
"process_memory": "included in full benchmark process peak RSS"
},
{
@@ -1087,44 +732,44 @@
},
{
"benchmark": "Forward, fast mode",
- "process_cpu": "median CPU 9.400s; median active cores 1.93",
+ "process_cpu": "median CPU 9.411s; median active cores 1.94",
"process_memory": "included in full benchmark process peak RSS"
},
{
"benchmark": "OE, no fast mode sweep",
- "process_cpu": "total CPU 38.578s; median active cores 1.97",
+ "process_cpu": "total CPU 38.477s; median active cores 1.97",
"process_memory": "included in full benchmark process peak RSS"
},
{
"benchmark": "OE, fast mode sweep",
- "process_cpu": "total CPU 14.360s; median active cores 1.92",
+ "process_cpu": "total CPU 8.576s; median active cores 1.95",
"process_memory": "included in full benchmark process peak RSS"
}
],
"total_wall_clock_rows": [
{
"benchmark": "Forward, no session",
- "total": "1.004s per baseline run median"
+ "total": "0.977s per baseline run median"
},
{
"benchmark": "Forward, session",
- "total": "1.005s first-use total, or 0.001s cached run only"
+ "total": "0.985s first-use total, or 0.001s cached run only"
},
{
"benchmark": "Forward, fast mode",
- "total": "4.860s median over 4 scenes"
+ "total": "4.862s median over 4 scenes"
},
{
"benchmark": "OE, no fast mode",
- "total": "19.554s over 5 retrievals"
+ "total": "19.478s over 5 retrievals"
},
{
"benchmark": "OE, fast mode",
- "total": "7.465s over 5 retrievals"
+ "total": "4.379s over 5 retrievals"
},
{
"benchmark": "OE fast-mode savings",
- "total": "12.089s over 5 retrievals"
+ "total": "15.099s over 5 retrievals"
}
]
},
@@ -1141,10 +786,10 @@
"retrieval_sweep_fast_mode_count": 5,
"retrieval_sweep_session_count": 5
},
- "run_id": "caf8446bd8c54385bfb9fe22f811eb48",
+ "run_id": "4c79293ec47f4edc85282706ff8e72fb",
"schema_version": 4,
"scratch_db": "benchmark/.runs/benchmark.sqlite",
- "total_benchmark_average_active_cores": 1.9348558685442712,
- "total_benchmark_process_cpu_s": 261.736896,
- "total_benchmark_wall_s": 135.27462187502533
+ "total_benchmark_average_active_cores": 1.9399266901538095,
+ "total_benchmark_process_cpu_s": 243.504245,
+ "total_benchmark_wall_s": 125.52239537499918
}