Skip to content

Field Models And Orbit Tools

Experimental tools for comparing ER results with field models and environment conditions. They return results and never write to the Store on their own.

Tool Module Purpose
Test-particle orbits sopran.experimental.moon.test_particle Electron orbits in SVM3D plus a uniform external field (Boris, magnetic only)
Closed-field islands sopran.experimental.moon.closure Connected components of closed cells on a grid
Field residual and quiet baseline sopran.experimental.kaguya.field_residual Decomposition of observed minus model field; quiet-time median baseline
Environment-response regression sopran.experimental.statistics.ridge Standardized ridge, track-grouped cross-validation, external-field gain

Test-particle orbits

import numpy as np
from sopran.experimental.moon.svm3d import SVM3DShellGrid
from sopran.experimental.moon.test_particle import (
    TestParticleSettings, electron_velocity_me, trace_test_particles,
)

grid = SVM3DShellGrid.load("svm3d_shell_grid.npz")
r0 = np.array([[1737.4 + 100.0, 0.0, 0.0]])              # MOON_ME [km]
b_external = np.array([[2.0, -1.0, 4.0]])                  # uniform external field [nT]
b_local = b_external + grid.field_at(r0)
v0 = electron_velocity_me([200.0], [150.0], [0.0], b_local)   # 200 eV, pitch angle 150°
settings = TestParticleSettings.for_field(float(np.linalg.norm(b_local)), gyrations=500.0)
result = trace_test_particles(grid, r0, v0, b_external, settings=settings)
result.status                       # "surface_hit" / "returned" / "escaped" / "max_steps" ...
result.arrays["spacecraft_shell_pitch_deg"]   # pitch angle where the orbit returned to the start radius

for_field uses \(1/120\) of the gyro period as the time step and gyrations for the step budget. Reaching stop_altitude_km (default 6.1 km, the lower shell) gives surface_hit, exceeding escape_altitude_km gives escaped, and re-entering return_radius_km around the start gives returned. spacecraft_shell_* is the state at the first upward crossing of the start radius, the quantity to compare with a reflected electron seen at the spacecraft. There is no electric field, and outside the shell grid (bounds="nan") the status is invalid_field.

Closed-field islands

from sopran.experimental.moon.closure import classify_closed_islands

islands = classify_closed_islands(
    lon_deg, lat_deg, closed_like, path_max_nT, footpoint_separation_deg,
    min_path_max_nT=10.0, min_footpoint_separation_deg=5.0, min_cells=3,
)
islands.island_id, islands.island_count

On a complete longitude × latitude grid, cells with closed_like true, path maximum and footpoint separation above the thresholds are connected with four neighbours and longitude wraparound; components smaller than min_cells are dropped. The physical meaning of the thresholds is the caller's.

Field residual and quiet baseline

from sopran.experimental.kaguya.field_residual import (
    quiet_field_baseline, residual_decomposition, svm3d_residual,
)

residual = svm3d_residual(grid, position_me_km, observed_nT)   # observed − SVM3D
residual.parallel_nT, residual.perpendicular_abs_nT, residual.fractional_abs
baseline = quiet_field_baseline(lon_deg, lat_deg, b_sc_nT, quiet_mask, grid_deg=10.0, min_events=5)
compression = b_sc_nT / baseline.value          # B_sc / B_quiet

residual_decomposition(observed, base, direction) returns the residual vector, its component along direction (default: the observed direction), the perpendicular magnitude and \(|\text{residual}|/|\text{base}|\). quiet_field_baseline takes the median per grid_deg cell over the quiet_mask rows; cells with fewer than min_events use the global median, then fallback, and source records which one. In the lunarsat analysis quiet meant pressure and IMF below their medians outside the tail, and most of the ER field response to the solar wind was explained by \(B_\mathrm{sc}/B_\mathrm{quiet}\).

Environment-response regression

from sopran.experimental.statistics.ridge import (
    external_gain, feature_drop_diagnostics, grouped_cv_predictions, model_ladder,
)

ladder = model_ladder(
    frame, targets=["log_b_eff"],
    specs=[("local", ["log_b_sc"]), ("physical", ["log_b_sc", "log_pressure", "imf_abs"])],
    alpha=1.0, folds=5, seed=0, group_column="track_id",
)
ladder.metrics          # train_r2 / cv_r2 / n_rows
drops = feature_drop_diagnostics(
    frame, target="log_b_eff",
    feature_groups={"local": ["log_b_sc"], "upstream": ["log_pressure", "imf_abs"]},
    alpha=1.0, group_column="track_id",
)
gain = external_gain(base_x, physical_x, log_target, alpha=1.0, eps=0.25)
gain.external_gain, gain.corrected

Features are standardized with the training rows and the intercept is not penalized. With group_column, rows sharing a value stay in one fold, so cv_r2 measures prediction across tracks. external_gain takes the difference of the log predictions of the base and physical models as the gain \(\exp(\Delta)\) and returns corrected = max(exp(y − Δ) − eps, 0). The regression metrics describe correlation, not causation.

Limits

  • Test particles are valid for a magnetic-only, uniform external field within the interpolation accuracy of the SVM3D shell grid; the altitude range of the grid (default 6.1–200 km) cannot be left.
  • The physical reading of a residual (external field, induced field, model error) does not follow from the decomposition.
  • Regression outputs are not written to the Store; save them together with the settings and input provenance.