Skip to content

Commit 8233f90

Browse files
committed
refactor: modernise type annotations and Streamlit API usage
- Replace all `Optional[X]` with `X | None` and drop `typing.Optional` imports across src/ and tests/ - Use `astropy.constants` module import instead of direct `m_p` symbol import in drag.py - Simplify M-is-None guard clauses to ternary expressions - Replace `try/except ValueError: pass` with `contextlib.suppress` - Update `st.plotly_chart(use_container_width=True)` → `width="stretch"` and `st.button/data_editor(use_container_width=True)` → `width="content"` to match the current Streamlit API - Fix pytest.mark.parametrize argument tuples (string → tuple literal) and add `match=` to bare `pytest.raises` calls in tests - Split compound assert into separate assertions for clearer failure msgs
1 parent ea56f19 commit 8233f90

11 files changed

Lines changed: 58 additions & 70 deletions

File tree

pages/01_Propagation_Simulator.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
from __future__ import annotations
99

10+
import contextlib
1011
from datetime import datetime
11-
from typing import Optional
1212

1313
import astropy.units as u
1414
import pandas as pd
@@ -119,7 +119,7 @@ def _mini_metric(
119119
has_obs = len(_obs_clean) >= 2
120120

121121
if has_obs:
122-
clean_df: Optional[pd.DataFrame] = _obs_clean.copy()
122+
clean_df = _obs_clean.copy()
123123
clean_df["lon"] = gcs_params.lon_deg
124124
clean_df["lat"] = gcs_params.lat_deg
125125
clean_df["tilt"] = gcs_params.tilt_deg
@@ -190,7 +190,7 @@ def _mini_metric(
190190
target_name=config.target.name,
191191
height=1.0,
192192
)
193-
st.plotly_chart(gcs_fig, use_container_width=True)
193+
st.plotly_chart(gcs_fig, width="stretch")
194194

195195
# --------------------------------------------------------
196196
# Height-Time Diagram — shown once ≥2 observations exist
@@ -217,7 +217,7 @@ def _mini_metric(
217217
v_apex_error_kms=derived.v_apex_error_kms,
218218
event_label=config.event_str,
219219
)
220-
st.plotly_chart(ht_fig, use_container_width=True)
220+
st.plotly_chart(ht_fig, width="stretch")
221221

222222
# Velocity result displayed prominently below the plot
223223
v_col, proj_col, target_col, _ = st.columns([1, 1, 1, 1])
@@ -273,8 +273,8 @@ def _mini_metric(
273273
# --------------------------------------------------------
274274
# Display stored results
275275
# --------------------------------------------------------
276-
results: Optional[SimulationResults] = st.session_state.get(KEY_SIM_RESULTS)
277-
stored_config: Optional[SimulationConfig] = st.session_state.get(KEY_SIM_CONFIG)
276+
results: SimulationResults | None = st.session_state.get(KEY_SIM_RESULTS)
277+
stored_config: SimulationConfig | None = st.session_state.get(KEY_SIM_CONFIG)
278278

279279
if results is None:
280280
st.info(
@@ -294,12 +294,10 @@ def _mini_metric(
294294
# --------------------------------------------------------
295295
# Parse expected ToA for comparison
296296
# --------------------------------------------------------
297-
toa_expected: Optional[datetime] = None
297+
toa_expected: datetime | None = None
298298
if stored_config and stored_config.toa_raw:
299-
try:
299+
with contextlib.suppress(ValueError):
300300
toa_expected = datetime.strptime(stored_config.toa_raw, "%d/%m/%Y %H:%M:%S")
301-
except ValueError:
302-
pass
303301

304302
# --------------------------------------------------------
305303
# Summary KPI cards
@@ -448,7 +446,7 @@ def _mini_metric(
448446
label="DBM",
449447
color=PLOT_COLORS["DBM"],
450448
)
451-
st.plotly_chart(dbm_fig, use_container_width=True)
449+
st.plotly_chart(dbm_fig, width="stretch")
452450

453451
with col_modbm:
454452
st.markdown(
@@ -587,7 +585,7 @@ def _mini_metric(
587585
label="MoDBM",
588586
color=PLOT_COLORS["MODBM"],
589587
)
590-
st.plotly_chart(modbm_fig, use_container_width=True)
588+
st.plotly_chart(modbm_fig, width="stretch")
591589

592590
# --------------------------------------------------------
593591
# Full-width combined comparison
@@ -599,4 +597,4 @@ def _mini_metric(
599597
results=results,
600598
target_distance_au=stored_config.target.distance,
601599
)
602-
st.plotly_chart(prop_fig, use_container_width=True)
600+
st.plotly_chart(prop_fig, width="stretch")

src/heliotrace/models/schemas.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
from dataclasses import dataclass
1313
from datetime import datetime
14-
from typing import Optional
1514

1615
# ---------------------------------------------------------------------------
1716
# Input models
@@ -60,9 +59,9 @@ class SimulationConfig:
6059
c_d: float # Drag coefficient (dimensionless)
6160

6261
# Optional overrides
63-
toa_raw: Optional[str] = None # Expected arrival "DD/MM/YYYY HH:MM:SS"; None → skip
64-
m_override_g: Optional[float] = None # CME mass override [g]; None → use Pluta formula
65-
v0_override_kms: Optional[float] = (
62+
toa_raw: str | None = None # Expected arrival "DD/MM/YYYY HH:MM:SS"; None → skip
63+
m_override_g: float | None = None # CME mass override [g]; None → use Pluta formula
64+
v0_override_kms: float | None = (
6665
None # Hard override for v₀ toward target [km/s]; None → geometry
6766
)
6867

@@ -109,13 +108,13 @@ class SimulationResults:
109108
target_hit: bool
110109

111110
# DBM
112-
dbm_series: Optional[PropagationSeries]
113-
elapsed_time_DBM_h: Optional[float] # Transit time [h]
114-
velocity_arrival_DBM_kms: Optional[float] # Impact speed [km/s]
115-
arrival_time_DBM: Optional[datetime]
111+
dbm_series: PropagationSeries | None
112+
elapsed_time_DBM_h: float | None # Transit time [h]
113+
velocity_arrival_DBM_kms: float | None # Impact speed [km/s]
114+
arrival_time_DBM: datetime | None
116115

117116
# MoDBM
118-
modbm_series: Optional[PropagationSeries]
119-
elapsed_time_MODBM_h: Optional[float]
120-
velocity_arrival_MODBM_kms: Optional[float]
121-
arrival_time_MODBM: Optional[datetime]
117+
modbm_series: PropagationSeries | None
118+
elapsed_time_MODBM_h: float | None
119+
velocity_arrival_MODBM_kms: float | None
120+
arrival_time_MODBM: datetime | None

src/heliotrace/physics/drag.py

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,10 @@
1313
from __future__ import annotations
1414

1515
import logging
16-
from typing import Optional
1716

17+
import astropy.constants as const
1818
import astropy.units as u
1919
import numpy as np
20-
from astropy.constants import m_p
2120
from scipy.integrate import OdeSolution, solve_ivp
2221
from scipy.interpolate import interp1d
2322
from scipy.optimize import root_scalar
@@ -88,7 +87,7 @@ def get_ambient_solar_wind_density_DBM(r: u.Quantity) -> u.Quantity:
8887
"""
8988
r_rsun = r.to(u.R_sun).value
9089
n_init = 3.3e5 * u.cm ** (-3)
91-
rho_w = m_p * n_init / r_rsun**2
90+
rho_w = const.m_p * n_init / r_rsun**2
9291
return rho_w
9392

9493

@@ -97,7 +96,7 @@ def get_constant_drag_parameter_DBM(
9796
alpha: u.Quantity,
9897
kappa: float,
9998
v_apex: u.Quantity,
100-
M: Optional[u.Quantity] = None,
99+
M: u.Quantity | None = None,
101100
c_d: float = 1.0,
102101
) -> u.Quantity:
103102
"""
@@ -111,10 +110,7 @@ def get_constant_drag_parameter_DBM(
111110
:param c_d: Drag coefficient (dimensionless). Converges to ~1 beyond ~12 R☉.
112111
:return: Drag parameter γ [1 / km].
113112
"""
114-
if M is None:
115-
M = get_CME_mass_pluta(v_apex).to(u.kg)
116-
else:
117-
M = M.to(u.kg)
113+
M = get_CME_mass_pluta(v_apex).to(u.kg) if M is None else M.to(u.kg)
118114

119115
rho_w = get_ambient_solar_wind_density_DBM(r)
120116
A = get_CME_cross_section_pluta(r, alpha, kappa)
@@ -186,7 +182,9 @@ def get_ambient_solar_wind_density_MODBM(r_km: float, ssn: float) -> float:
186182
:return: Mass density [kg / km³].
187183
"""
188184
r_au = r_km * u.km.to(u.AU)
189-
rho_w = m_p.value * (0.0038 * ssn + 4.5) * (u.cm ** (-3)).to(u.km ** (-3)) * r_au ** (-2.11)
185+
rho_w = (
186+
const.m_p.value * (0.0038 * ssn + 4.5) * (u.cm ** (-3)).to(u.km ** (-3)) * r_au ** (-2.11)
187+
)
190188
return float(rho_w)
191189

192190

@@ -265,7 +263,7 @@ def simulate_equation_of_motion_MODBM(
265263
kappa: float,
266264
w_type: str,
267265
ssn: float,
268-
M: Optional[u.Quantity] = None,
266+
M: u.Quantity | None = None,
269267
c_d: float = 1.0,
270268
) -> OdeSolution:
271269
"""
@@ -285,10 +283,7 @@ def simulate_equation_of_motion_MODBM(
285283
:return: ``scipy.integrate.solve_ivp`` solution with ``dense_output=True``.
286284
"""
287285
mass_q: u.Quantity
288-
if M is None:
289-
mass_q = get_CME_mass_pluta(v_apex).to(u.kg)
290-
else:
291-
mass_q = M.to(u.kg)
286+
mass_q = get_CME_mass_pluta(v_apex).to(u.kg) if M is None else M.to(u.kg)
292287

293288
r0_km = r0.to(u.km).value
294289
v0_kms = v0.to(u.km / u.s).value

src/heliotrace/physics/fitting.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from __future__ import annotations
66

77
import logging
8-
from typing import Optional
98

109
import numpy as np
1110
from numpy.typing import ArrayLike
@@ -17,8 +16,8 @@
1716
def perform_linear_fit(
1817
x_data: ArrayLike,
1918
y_data: ArrayLike,
20-
y_error: Optional[float | ArrayLike] = None,
21-
initial_guess: Optional[list[float]] = None,
19+
y_error: float | ArrayLike | None = None,
20+
initial_guess: list[float] | None = None,
2221
) -> dict[str, float]:
2322
"""
2423
Fit a linear model ``y = m * x + b`` to the provided data.

src/heliotrace/ui/components/propagation_plot.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@
66

77
from __future__ import annotations
88

9-
from typing import Optional
10-
119
import astropy.units as u
1210
import plotly.graph_objects as go
1311
from plotly.subplots import make_subplots
@@ -29,7 +27,7 @@
2927

3028
def build_single_model_figure(
3129
series: PropagationSeries,
32-
elapsed_h: Optional[float],
30+
elapsed_h: float | None,
3331
target_distance_au: float,
3432
label: str,
3533
color: str,
@@ -166,8 +164,8 @@ def build_propagation_comparison_figure(
166164
)
167165

168166
def _add_model(
169-
series: Optional[PropagationSeries],
170-
elapsed_h: Optional[float],
167+
series: PropagationSeries | None,
168+
elapsed_h: float | None,
171169
label: str,
172170
color: str,
173171
) -> None:

src/heliotrace/ui/components/sidebar_inputs.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from __future__ import annotations
99

1010
from datetime import datetime
11-
from typing import Optional
1211

1312
import pandas as pd
1413
import streamlit as st
@@ -79,7 +78,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
7978
if st.button(
8079
"⚡ Fill with example",
8180
type="primary",
82-
use_container_width=True,
81+
width="content",
8382
help="Auto-fill all fields with the 2023-10-28 Halloween CME event.",
8483
):
8584
_fill_example()
@@ -240,7 +239,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
240239
obs_init,
241240
hide_index=True,
242241
num_rows="dynamic",
243-
use_container_width=True,
242+
width="content",
244243
column_config={
245244
"datetime": st.column_config.DatetimeColumn(
246245
"Time (UTC)",
@@ -352,7 +351,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
352351

353352
st.divider()
354353
st.caption("Optional overrides")
355-
toa_raw: Optional[str] = (
354+
toa_raw: str | None = (
356355
st.text_input(
357356
"Expected arrival time (optional)",
358357
key="sb_toa_raw",
@@ -377,7 +376,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
377376
format="%.2e",
378377
help="Override the Pluta (2018) mass formula. Enter 0 to keep the formula.",
379378
)
380-
m_override_g: Optional[float] = m_override_raw if m_override_raw > 0.0 else None
379+
m_override_g: float | None = m_override_raw if m_override_raw > 0.0 else None
381380

382381
v0_override_str: str = st.text_input(
383382
"v₀ override [km/s] (blank = geometry-derived)",
@@ -386,7 +385,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
386385
help="Hard override for the CME velocity component directed at the target. "
387386
r"Replaces the geometry-derived $v_0 = v_{\rm apex} \times$ projection ratio.",
388387
)
389-
v0_override_kms: Optional[float] = _parse_float(v0_override_str)
388+
v0_override_kms: float | None = _parse_float(v0_override_str)
390389
if v0_override_kms is not None:
391390
if v0_override_kms <= 0.0:
392391
st.error("❌ v₀ override must be > 0 km/s. Override ignored.")
@@ -405,7 +404,7 @@ def render_sidebar() -> tuple[SimulationConfig, GCSParams, pd.DataFrame, bool, b
405404
run_clicked: bool = st.button(
406405
"▶ Run Simulation",
407406
type="primary",
408-
use_container_width=True,
407+
width="content",
409408
)
410409

411410
config = SimulationConfig(

src/heliotrace/ui/utils.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,9 @@
55
from __future__ import annotations
66

77
from datetime import datetime
8-
from typing import Optional
98

109

11-
def format_diff(sim_time: Optional[datetime], ref_time: Optional[datetime]) -> Optional[str]:
10+
def format_diff(sim_time: datetime | None, ref_time: datetime | None) -> str | None:
1211
"""
1312
Format the signed difference between a simulated arrival and a reference time.
1413

tests/physics/test_apex_ratio.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737

3838

3939
@pytest.mark.parametrize(
40-
"cme_lat_deg, cme_lon_deg",
40+
("cme_lat_deg", "cme_lon_deg"),
4141
[(0, 0), (10, 20), (-5, 15)],
4242
)
4343
def test_direct_hit_ratio_near_unity(cme_lat_deg: float, cme_lon_deg: float) -> None:
@@ -60,7 +60,7 @@ def test_direct_hit_ratio_near_unity(cme_lat_deg: float, cme_lon_deg: float) ->
6060

6161

6262
@pytest.mark.parametrize(
63-
"alpha_deg, kappa",
63+
("alpha_deg", "kappa"),
6464
[(15, 0.2), (30, 0.42), (45, 0.6)],
6565
)
6666
def test_direct_hit_ratio_bounded(alpha_deg: float, kappa: float) -> None:
@@ -232,7 +232,7 @@ def test_tilt_does_not_affect_on_axis_hit(tilt_deg: float) -> None:
232232

233233

234234
@pytest.mark.parametrize(
235-
"sep_deg, expected_above_threshold",
235+
("sep_deg", "expected_above_threshold"),
236236
[(30, True), (50, True), (150, False)],
237237
)
238238
def test_operationally_relevant_threshold(sep_deg: float, expected_above_threshold: bool) -> None:

tests/physics/test_drag.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ def test_dbm_gamma_units() -> None:
240240

241241

242242
@pytest.mark.parametrize(
243-
"r_au, ssn",
243+
("r_au", "ssn"),
244244
[(1.0, 50.0), (1.5, 100.0), (2.0, 0.0)],
245245
)
246246
def test_modbm_density_power_law_exponent(r_au: float, ssn: float) -> None:
@@ -326,7 +326,7 @@ def test_modbm_gamma_inverse_in_mass() -> None:
326326
# ---------------------------------------------------------------------------
327327

328328

329-
@pytest.mark.parametrize("w_type, expected", [("slow", 363.0), ("fast", 483.0)])
329+
@pytest.mark.parametrize(("w_type", "expected"), [("slow", 363.0), ("fast", 483.0)])
330330
def test_modbm_wind_at_1au(w_type: str, expected: float) -> None:
331331
"""At 1 AU, r_AU = 1 → w = w0 * 1.0^0.099 = w0 exactly."""
332332
w = get_ambient_solar_wind_speed_MODBM(AU_KM, w_type)
@@ -366,7 +366,7 @@ def test_modbm_wind_fast_slow_ratio_constant(r_au: float) -> None:
366366
@pytest.mark.parametrize("bad_type", ["medium", "", "SLOW", "Fast"])
367367
def test_modbm_wind_invalid_type_raises(bad_type: str) -> None:
368368
"""Unknown w_type must raise ValueError."""
369-
with pytest.raises(ValueError):
369+
with pytest.raises(ValueError, match="Unknown w_type"):
370370
get_ambient_solar_wind_speed_MODBM(AU_KM, bad_type)
371371

372372

@@ -613,7 +613,7 @@ def test_arrival_raises_when_distance_not_reached() -> None:
613613
400.0 * u.km / u.s,
614614
1e-7 / u.km,
615615
)
616-
with pytest.raises(ValueError):
616+
with pytest.raises(ValueError, match="f\\(a\\) and f\\(b\\) must have different signs"):
617617
find_time_and_velocity_at_distance(sol, 1.0 * u.AU)
618618

619619

tests/physics/test_fitting.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_negative_slope_recovery() -> None:
112112

113113

114114
@pytest.mark.parametrize(
115-
"N, sigma",
115+
("N", "sigma"),
116116
[(5, 0.2), (10, 1.0), (20, 0.5), (50, 2.0)],
117117
)
118118
def test_weighted_slope_error_closed_form(N: int, sigma: float) -> None:

0 commit comments

Comments
 (0)