Skip to content

Commit 29168e0

Browse files
Hevagogmzsuetam
authored andcommitted
fix exception messages and more fixtures in tests
1 parent cde2788 commit 29168e0

File tree

1 file changed

+152
-143
lines changed

1 file changed

+152
-143
lines changed
Lines changed: 152 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
from __future__ import annotations
2+
13
import pytest
24
import os
35
import numpy as np
46
from scipy.optimize import fsolve
7+
import warnings
8+
from typing import Tuple, List, Generator
59

610
from PySDM import Formulae
711
from PySDM.physics import si
@@ -10,13 +14,15 @@
1014

1115

1216
class GroundTruthLoader:
13-
def __init__(self, groundtruth_dir_path, n_samples=2, random_seed=2137):
17+
def __init__(
18+
self, groundtruth_dir_path: str, n_samples: int = 2, random_seed: int = 2137
19+
):
1420
self.dir_path = groundtruth_dir_path
1521
self.RHs = None
1622
self.r0grid = None
1723
self.m_frac_evap = None
18-
self.n_samples = n_samples # Number of random samples to test
19-
np.random.seed(random_seed) # reproducible random samples during debugging
24+
self.n_samples = n_samples
25+
np.random.seed(random_seed)
2026

2127
def __enter__(self):
2228
try:
@@ -25,16 +31,64 @@ def __enter__(self):
2531
self.m_frac_evap = np.load(os.path.join(self.dir_path, "m_frac_evap.npy"))
2632
return self
2733
except FileNotFoundError as e:
28-
print(f"Error loading ground truth files: {e}")
29-
raise
34+
pytest.fail(f"Error loading ground truth files: {e}")
3035
except Exception as e:
31-
print(f"An unexpected error occurred while loading ground truth data: {e}")
32-
raise
36+
pytest.fail(f"Unexpected error loading ground truth data: {e}")
3337

3438
def __exit__(self, exc_type, exc_val, exc_tb):
3539
pass
3640

3741

42+
@pytest.fixture(scope="module")
43+
def ground_truth_data(request) -> Generator[GroundTruthLoader, None, None]:
44+
current_dir = os.path.dirname(os.path.abspath(request.fspath))
45+
groundtruth_dir = os.path.abspath(os.path.join(current_dir, "ground_truth"))
46+
if not os.path.isdir(groundtruth_dir):
47+
pytest.fail(f"Groundtruth directory not found at {groundtruth_dir}")
48+
with GroundTruthLoader(groundtruth_dir) as gt:
49+
yield gt
50+
51+
52+
@pytest.fixture
53+
def ground_truth_sample(ground_truth_data: GroundTruthLoader) -> List[dict]:
54+
gt = ground_truth_data
55+
n_rh_values = len(gt.RHs)
56+
n_radius_values = gt.r0grid.shape[1]
57+
total_points = n_rh_values * n_radius_values
58+
n_samples = min(gt.n_samples, total_points)
59+
if n_samples == 0:
60+
pytest.skip("No data points available to sample.")
61+
all_indices = np.array(
62+
[(i, j) for i in range(n_rh_values) for j in range(n_radius_values)]
63+
)
64+
sampled_indices_flat = np.random.choice(len(all_indices), n_samples, replace=False)
65+
sampled_ij_pairs = all_indices[sampled_indices_flat]
66+
return [
67+
{
68+
"rh": gt.RHs[i_rh],
69+
"r_m": gt.r0grid[0, j_r],
70+
"expected_m_frac_evap": gt.m_frac_evap[i_rh, j_r],
71+
"i_rh": i_rh,
72+
"j_r": j_r,
73+
}
74+
for i_rh, j_r in sampled_ij_pairs
75+
]
76+
77+
78+
@pytest.fixture(scope="module")
79+
def static_arrays() -> Tuple[np.ndarray, np.ndarray, np.ndarray, object]:
80+
formulae = Formulae(
81+
ventilation="PruppacherAndRasmussen1979",
82+
saturation_vapour_pressure="AugustRocheMagnus",
83+
diffusion_coordinate="WaterMassLogarithm",
84+
)
85+
radius_array = np.logspace(-4.5, -2.5, 50) * si.m
86+
RH_array = np.linspace(0.25, 0.99, 50)
87+
output_matrix = np.full((len(RH_array), len(radius_array)), np.nan)
88+
const = formulae.constants
89+
return radius_array, RH_array, output_matrix, const
90+
91+
3892
class TestNPYComparison:
3993
@staticmethod
4094
def _mix(dry_prop, vap_prop, ratio):
@@ -44,15 +98,11 @@ def _calculate_cloud_properties(
4498
self, planet: EarthLike, surface_RH: float, formulae_instance: Formulae
4599
):
46100
const = formulae_instance.constants
47-
48101
planet.RH_zref = surface_RH
49-
50102
pvs_stp = formulae_instance.saturation_vapour_pressure.pvs_water(planet.T_STP)
51-
52103
initial_water_vapour_mixing_ratio = const.eps / (
53104
planet.p_STP / planet.RH_zref / pvs_stp - 1
54105
)
55-
56106
R_air_mix = self._mix(const.Rd, const.Rv, initial_water_vapour_mixing_ratio)
57107
cp_mix = self._mix(const.c_pd, const.c_pv, initial_water_vapour_mixing_ratio)
58108

@@ -78,154 +128,113 @@ def solve_Tcloud(T_candidate):
78128
)
79129
return initial_water_vapour_mixing_ratio, Tcloud, Zcloud, pcloud
80130

81-
def test_figure_2_replication_accuracy(self):
82-
current_dir = os.path.dirname(os.path.abspath(__file__))
83-
groundtruth_dir = os.path.abspath(os.path.join(current_dir, "ground_truth"))
84-
if not os.path.isdir(groundtruth_dir):
85-
pytest.fail(f"Groundtruth directory not found at {groundtruth_dir}")
86-
131+
def test_figure_2_replication_accuracy(self, ground_truth_sample, static_arrays):
87132
formulae = Formulae(
88133
ventilation="PruppacherAndRasmussen1979",
89134
saturation_vapour_pressure="AugustRocheMagnus",
90135
diffusion_coordinate="WaterMassLogarithm",
91136
)
92-
93-
with GroundTruthLoader(groundtruth_dir) as gt:
94-
if gt.RHs is None or gt.r0grid is None or gt.m_frac_evap is None:
95-
pytest.fail("Ground truth data (.npy files) not loaded properly.")
96-
97-
n_rh_values = len(gt.RHs)
98-
n_radius_values = gt.r0grid.shape[1]
99-
100-
total_points = n_rh_values * n_radius_values
101-
n_samples = min(gt.n_samples, total_points)
102-
103-
if n_samples == 0:
104-
pytest.skip("No data points available to sample.")
105-
106-
all_indices = np.array(
107-
[(i, j) for i in range(n_rh_values) for j in range(n_radius_values)]
137+
for sample in ground_truth_sample:
138+
planet = EarthLike()
139+
rh = sample["rh"]
140+
r_m = sample["r_m"]
141+
expected = sample["expected_m_frac_evap"]
142+
i_rh = sample["i_rh"]
143+
j_r = sample["j_r"]
144+
try:
145+
iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties(
146+
planet, rh, formulae
147+
)
148+
simulated = self.calc_simulated_m_frac_evap_point(
149+
planet,
150+
formulae,
151+
i_rh,
152+
j_r,
153+
rh,
154+
r_m,
155+
expected,
156+
iwvmr,
157+
Tcloud,
158+
Zcloud,
159+
pcloud,
160+
)
161+
except Exception as e:
162+
pytest.fail(
163+
f"Error in _calculate_cloud_properties for RH={rh} (sample idx {i_rh},{j_r}): {e}."
164+
)
165+
error_context = (
166+
f"Sample (RH_idx={i_rh}, R_idx={j_r}), RH={rh:.4f}, R_m={r_m:.3e}. "
167+
f"Expected: {expected}, Got: {simulated}"
108168
)
109-
110-
sampled_indices_flat = np.random.choice(len(all_indices), n_samples, replace=False)
111-
sampled_ij_pairs = all_indices[sampled_indices_flat]
112-
113-
for i_rh, j_r in sampled_ij_pairs:
114-
current_planet_state = EarthLike()
115-
116-
current_rh = gt.RHs[i_rh]
117-
current_r_m = gt.r0grid[0, j_r]
118-
expected_m_frac_evap = gt.m_frac_evap[i_rh, j_r]
119-
120-
try:
121-
iwvmr, Tcloud, Zcloud, pcloud = self._calculate_cloud_properties(
122-
current_planet_state, current_rh, formulae
123-
)
124-
simulated_m_frac_evap_point = self.calc_simulated_m_frac_evap_point(
125-
current_planet_state,
126-
formulae,
127-
i_rh,
128-
j_r,
129-
current_rh,
130-
current_r_m,
131-
expected_m_frac_evap,
132-
iwvmr,
133-
Tcloud,
134-
Zcloud,
135-
pcloud,
136-
)
137-
except Exception as e:
138-
print(
139-
f"Warning: Error in _calculate_cloud_properties for RH={current_rh} (sample idx {i_rh},{j_r}): {e}."
140-
)
141-
142-
error_context = (
143-
f"Sample (RH_idx={i_rh}, R_idx={j_r}), "
144-
f"RH={current_rh:.4f}, R_m={current_r_m:.3e}. "
145-
f"Expected: {expected_m_frac_evap}, Got: {simulated_m_frac_evap_point}"
169+
if np.isnan(expected):
170+
assert np.isnan(
171+
simulated
172+
), f"NaN Mismatch. {error_context} (Expected NaN, got non-NaN)"
173+
else:
174+
assert not np.isnan(
175+
simulated
176+
), f"NaN Mismatch. {error_context} (Expected non-NaN, got NaN)"
177+
np.testing.assert_allclose(
178+
simulated,
179+
expected,
180+
rtol=1e-1,
181+
atol=1e-1,
182+
err_msg=f"Value Mismatch. {error_context}",
146183
)
147184

148-
if np.isnan(expected_m_frac_evap):
149-
assert np.isnan(
150-
simulated_m_frac_evap_point
151-
), f"NaN Mismatch. {error_context} (Expected NaN, got non-NaN)"
152-
else:
153-
assert not np.isnan(
154-
simulated_m_frac_evap_point
155-
), f"NaN Mismatch. {error_context} (Expected non-NaN, got NaN)"
156-
np.testing.assert_allclose(
157-
simulated_m_frac_evap_point,
158-
expected_m_frac_evap,
159-
rtol=1e-1, # Relative tolerance
160-
atol=1e-1, # Absolute tolerance
161-
err_msg=f"Value Mismatch. {error_context}",
162-
)
163-
164185
def calc_simulated_m_frac_evap_point(
165186
self,
166-
current_planet_state,
187+
planet,
167188
formulae,
168189
i_rh,
169190
j_r,
170-
current_rh,
171-
current_r_m,
172-
expected_m_frac_evap,
191+
rh,
192+
r_m,
193+
expected,
173194
iwvmr,
174195
Tcloud,
175196
Zcloud,
176197
pcloud,
177198
):
178-
179-
simulated_m_frac_evap_point = np.nan
180-
181-
if np.isnan(current_r_m) or current_r_m <= 0:
182-
print(f"Warning: Invalid radius current_r_m={current_r_m} for sample idx {i_rh},{j_r}.")
183-
else:
184-
settings = Settings(
185-
planet=current_planet_state,
186-
r_wet=current_r_m,
187-
mass_of_dry_air=1e5 * si.kg,
188-
initial_water_vapour_mixing_ratio=iwvmr,
189-
pcloud=pcloud,
190-
Zcloud=Zcloud,
191-
Tcloud=Tcloud,
192-
formulae=formulae,
193-
)
194-
simulation = Simulation(settings)
195-
196-
try:
197-
output = simulation.run()
198-
if (
199-
output
200-
and "r" in output
201-
and len(output["r"]) > 0
202-
and "z" in output
203-
and len(output["z"]) > 0
204-
):
205-
final_radius_um = output["r"][-1]
206-
if np.isnan(final_radius_um) or final_radius_um < 0:
207-
final_radius_m = final_radius_um * 1e-6
208-
if final_radius_m < 0: # Non-physical radius
209-
simulated_m_frac_evap_point = 1.0 # 1.0 means fully evaporated
210-
else:
211-
simulated_m_frac_evap_point = np.nan
212-
else:
213-
final_radius_m = final_radius_um * 1e-6
214-
if current_r_m == 0:
215-
frac_evap = 1.0
216-
else:
217-
frac_evap = 1.0 - (final_radius_m / current_r_m) ** 3
218-
frac_evap = np.clip(frac_evap, 0.0, 1.0)
219-
simulated_m_frac_evap_point = frac_evap
220-
else:
221-
simulated_m_frac_evap_point = np.nan
222-
except Exception as e:
223-
print(
224-
f"Warning: Simulation run failed for RH={current_rh}, r={current_r_m} (sample idx {i_rh},{j_r}): {e}."
225-
)
226-
if np.isclose(expected_m_frac_evap, 1.0, atol=1e-6):
227-
simulated_m_frac_evap_point = 1.0
199+
if np.isnan(r_m) or r_m <= 0:
200+
pytest.fail(f"Invalid radius r_m={r_m} for sample idx {i_rh},{j_r}.")
201+
settings = Settings(
202+
planet=planet,
203+
r_wet=r_m,
204+
mass_of_dry_air=1e5 * si.kg,
205+
initial_water_vapour_mixing_ratio=iwvmr,
206+
pcloud=pcloud,
207+
Zcloud=Zcloud,
208+
Tcloud=Tcloud,
209+
formulae=formulae,
210+
)
211+
simulation = Simulation(settings)
212+
try:
213+
output = simulation.run()
214+
if (
215+
output
216+
and "r" in output
217+
and len(output["r"]) > 0
218+
and "z" in output
219+
and len(output["z"]) > 0
220+
):
221+
final_radius_um = output["r"][-1]
222+
if np.isnan(final_radius_um) or final_radius_um < 0:
223+
final_radius_m = final_radius_um * 1e-6
224+
if final_radius_m < 0:
225+
return 1.0
226+
return np.nan
227+
final_radius_m = final_radius_um * 1e-6
228+
if r_m == 0:
229+
frac_evap = 1.0
228230
else:
229-
simulated_m_frac_evap_point = np.nan
230-
231-
return simulated_m_frac_evap_point
231+
frac_evap = 1.0 - (final_radius_m / r_m) ** 3
232+
return np.clip(frac_evap, 0.0, 1.0)
233+
return np.nan
234+
except Exception as e:
235+
warnings.warn(
236+
f"Simulation run failed for RH={rh:.4f}, r={r_m:.3e} (sample idx {i_rh},{j_r}): {type(e).__name__}: {e}"
237+
)
238+
if np.isclose(expected, 1.0, atol=1e-6):
239+
return 1.0
240+
return np.nan

0 commit comments

Comments
 (0)