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": [ - "\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "-2.358\n", - "\n", - "13.65\n", - "\n", - "29.65\n", - "\n", - "45.65\n", - "\n", - "61.66\n", - "7.445e-12\n", - "7.693e-05\n", - "0.0001539\n", - "0.0002308\n", - "0.0003077\n", - "0.0003847\n", - "O2-O2 collision-induced absorption optical depth\n", - "Altitude (km)\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "O2-O2 collision-induced absorption optical depth profile, 760.76 nm\n", - "\n", - "" - ], - "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": [ - "\n", - "Instrument spectral response function (ISRF), 760.76000 nm\n", - "\n", - "Instrument spectral response function (ISRF), 760.76000 nm\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "0\n", - "\n", - "0.2625\n", - "\n", - "0.525\n", - "\n", - "0.7875\n", - "\n", - "1.05\n", - "-0.1212\n", - "-0.07277\n", - "-0.0243\n", - "0.02416\n", - "0.07263\n", - "0.1211\n", - "Offset (nm)\n", - "Normalized ISRF\n", - "Instrument spectral response function (ISRF), 760.76000 nm\n", - "\n", - "" - ], - "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 }