Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
junzis
GitHub Repository: junzis/openap
Path: blob/master/tests/test_contrail.py
592 views
1
"""Comprehensive tests for the contrail module.
2
3
This module tests all contrail-related functions in openap.contrail,
4
including saturation pressure, relative humidity, critical temperature,
5
radiative forcing, and optical property evolution.
6
"""
7
8
import numpy as np
9
import pytest
10
11
from openap import contrail
12
13
14
# Tolerance for floating point comparisons
15
RTOL = 1e-4 # 0.01% relative tolerance
16
17
18
class TestContrailSaturationPressure:
19
"""Tests for saturation pressure functions."""
20
21
def test_saturation_pressure_over_water_freezing(self):
22
"""Test saturation pressure over water at freezing point."""
23
# At 273.15 K, saturation pressure should be about 611 Pa
24
p_sat = contrail.saturation_pressure_over_water(273.15)
25
assert p_sat == pytest.approx(611.0, rel=0.02)
26
27
def test_saturation_pressure_over_water_array(self):
28
"""Test saturation pressure over water with array input."""
29
temperatures = np.array([253.15, 263.15, 273.15])
30
p_sat = contrail.saturation_pressure_over_water(temperatures)
31
32
assert isinstance(p_sat, np.ndarray)
33
assert p_sat.shape == (3,)
34
# Pressure should increase with temperature
35
assert p_sat[0] < p_sat[1] < p_sat[2]
36
37
def test_saturation_pressure_over_ice_freezing(self):
38
"""Test saturation pressure over ice at freezing point."""
39
# At 273.15 K, saturation pressure over ice should be about 611 Pa
40
p_sat = contrail.saturation_pressure_over_ice(273.15)
41
assert p_sat == pytest.approx(611.0, rel=0.02)
42
43
def test_saturation_pressure_over_ice_array(self):
44
"""Test saturation pressure over ice with array input."""
45
temperatures = np.array([223.15, 243.15, 263.15])
46
p_sat = contrail.saturation_pressure_over_ice(temperatures)
47
48
assert isinstance(p_sat, np.ndarray)
49
assert p_sat.shape == (3,)
50
# Pressure should increase with temperature
51
assert p_sat[0] < p_sat[1] < p_sat[2]
52
53
def test_water_greater_than_ice(self):
54
"""Test that saturation pressure over water > ice at same temp."""
55
T = 253.15 # -20 C
56
p_water = contrail.saturation_pressure_over_water(T)
57
p_ice = contrail.saturation_pressure_over_ice(T)
58
59
# Saturation pressure over water is always higher than over ice
60
# for the same temperature (supercooled water)
61
assert p_water > p_ice
62
63
64
class TestContrailRelativeHumidity:
65
"""Tests for relative humidity functions."""
66
67
def test_relative_humidity_ice(self):
68
"""Test relative humidity calculation with respect to ice."""
69
# Typical upper troposphere conditions
70
specific_humidity = 0.0001 # kg/kg
71
pressure = 25000 # Pa (~10 km altitude)
72
temperature = 220 # K
73
74
rhi = contrail.relative_humidity(
75
specific_humidity, pressure, temperature, to="ice"
76
)
77
78
# Should be a reasonable value
79
assert 0 < rhi < 3 # Can be supersaturated
80
81
def test_relative_humidity_water(self):
82
"""Test relative humidity calculation with respect to water."""
83
specific_humidity = 0.0001
84
pressure = 25000
85
temperature = 220
86
87
rhw = contrail.relative_humidity(
88
specific_humidity, pressure, temperature, to="water"
89
)
90
91
# Should be a reasonable value
92
assert 0 < rhw < 2
93
94
def test_relative_humidity_array(self):
95
"""Test relative humidity with array inputs."""
96
specific_humidity = np.array([0.0001, 0.0002, 0.0003])
97
pressure = 25000
98
temperature = 220
99
100
rhi = contrail.relative_humidity(
101
specific_humidity, pressure, temperature, to="ice"
102
)
103
104
assert isinstance(rhi, np.ndarray)
105
assert rhi.shape == (3,)
106
# Higher specific humidity should give higher RH
107
assert rhi[0] < rhi[1] < rhi[2]
108
109
def test_relative_humidity_invalid_reference(self):
110
"""Test that invalid reference phase raises error."""
111
with pytest.raises(AssertionError):
112
contrail.relative_humidity(0.0001, 25000, 220, to="steam")
113
114
def test_rhw2rhi(self):
115
"""Test conversion from RH water to RH ice."""
116
T = 220 # K
117
rhw = 0.5
118
119
rhi = contrail.rhw2rhi(rhw, T)
120
121
# At cold temperatures, RHi should be higher than RHw
122
assert rhi > rhw
123
124
def test_rhw2rhi_array(self):
125
"""Test rhw2rhi with array inputs."""
126
temperatures = np.array([210, 220, 230])
127
rhw = 0.5
128
129
rhi = contrail.rhw2rhi(rhw, temperatures)
130
131
assert isinstance(rhi, np.ndarray)
132
assert rhi.shape == (3,)
133
134
135
class TestContrailCriticalTemperature:
136
"""Tests for critical temperature functions."""
137
138
def test_critical_temperature_water(self):
139
"""Test critical temperature calculation."""
140
# At typical cruise altitude (~10 km, ~25000 Pa)
141
pressure = 25000
142
143
t_crit = contrail.critical_temperature_water(pressure)
144
145
# Critical temperature should be around 220-230 K at cruise altitude
146
assert 215 < t_crit < 235
147
148
def test_critical_temperature_water_altitude_dependence(self):
149
"""Test that critical temperature varies with pressure."""
150
p_low = 20000 # Higher altitude
151
p_high = 30000 # Lower altitude
152
153
t_crit_low = contrail.critical_temperature_water(p_low)
154
t_crit_high = contrail.critical_temperature_water(p_high)
155
156
# Higher altitude (lower pressure) -> lower critical temperature
157
assert t_crit_low < t_crit_high
158
159
def test_critical_temperature_water_efficiency_dependence(self):
160
"""Test that critical temperature varies with propulsion efficiency."""
161
pressure = 25000
162
163
t_crit_low_eff = contrail.critical_temperature_water(
164
pressure, propulsion_efficiency=0.3
165
)
166
t_crit_high_eff = contrail.critical_temperature_water(
167
pressure, propulsion_efficiency=0.5
168
)
169
170
# Higher efficiency -> higher critical temperature
171
assert t_crit_high_eff > t_crit_low_eff
172
173
def test_critical_temperature_water_array(self):
174
"""Test critical temperature with array input."""
175
pressures = np.array([20000, 25000, 30000])
176
177
t_crit = contrail.critical_temperature_water(pressures)
178
179
assert isinstance(t_crit, np.ndarray)
180
assert t_crit.shape == (3,)
181
182
def test_critical_temperature_water_and_ice(self):
183
"""Test critical temperature for both water and ice."""
184
pressure = 25000
185
186
t_water, t_ice = contrail.critical_temperature_water_and_ice(pressure)
187
188
# Ice critical temperature should be lower than water
189
assert t_ice < t_water
190
191
def test_critical_temperature_water_and_ice_efficiency(self):
192
"""Test critical temperatures with custom efficiency."""
193
pressure = 25000
194
195
t_water, t_ice = contrail.critical_temperature_water_and_ice(
196
pressure, propulsion_efficiency=0.35
197
)
198
199
assert t_ice < t_water
200
assert 200 < t_ice < 230
201
assert 210 < t_water < 240
202
203
def test_backward_compatibility_propulsion_efficiency(self):
204
"""Test that module-level propulsion_efficiency constant exists."""
205
# This ensures backward compatibility
206
assert contrail.propulsion_efficiency == 0.4
207
assert contrail.DEFAULT_PROPULSION_EFFICIENCY == 0.4
208
209
210
class TestContrailRadiativeForcing:
211
"""Tests for radiative forcing functions."""
212
213
def test_rf_shortwave_daytime(self):
214
"""Test shortwave radiative forcing during daytime."""
215
zenith = 30 # degrees
216
tau = 0.4
217
tau_c = 0.36
218
219
rf_sw = contrail.rf_shortwave(zenith, tau, tau_c)
220
221
# Shortwave forcing should be negative (cooling)
222
assert rf_sw < 0
223
224
def test_rf_shortwave_nighttime(self):
225
"""Test shortwave radiative forcing at night."""
226
zenith = 100 # degrees (sun below horizon)
227
tau = 0.4
228
tau_c = 0.36
229
230
rf_sw = contrail.rf_shortwave(zenith, tau, tau_c)
231
232
# No shortwave forcing at night
233
assert rf_sw == 0
234
235
def test_rf_shortwave_horizon(self):
236
"""Test shortwave forcing at horizon."""
237
zenith = 90 # exactly at horizon
238
tau = 0.4
239
tau_c = 0.36
240
241
rf_sw = contrail.rf_shortwave(zenith, tau, tau_c)
242
243
# Very small or zero forcing at horizon
244
assert abs(rf_sw) < 1.0
245
246
def test_rf_shortwave_array(self):
247
"""Test shortwave forcing with array inputs."""
248
zenith = np.array([30, 60, 100])
249
tau = 0.4
250
tau_c = 0.36
251
252
rf_sw = contrail.rf_shortwave(zenith, tau, tau_c)
253
254
assert isinstance(rf_sw, np.ndarray)
255
assert rf_sw.shape == (3,)
256
assert rf_sw[0] < 0 # Daytime cooling
257
assert rf_sw[1] < 0 # Daytime cooling
258
assert rf_sw[2] == 0 # Nighttime
259
260
def test_rf_longwave(self):
261
"""Test longwave radiative forcing."""
262
olr = 250 # W/m^2
263
temperature = 220 # K
264
265
rf_lw = contrail.rf_longwave(olr, temperature)
266
267
# Longwave forcing should be positive (warming)
268
assert rf_lw > 0
269
270
def test_rf_longwave_array(self):
271
"""Test longwave forcing with array inputs."""
272
olr = np.array([200, 250, 300])
273
temperature = 220
274
275
rf_lw = contrail.rf_longwave(olr, temperature)
276
277
assert isinstance(rf_lw, np.ndarray)
278
assert rf_lw.shape == (3,)
279
# Higher OLR should give higher forcing
280
assert rf_lw[0] < rf_lw[1] < rf_lw[2]
281
282
def test_rf_longwave_nonnegative(self):
283
"""Test that longwave forcing is non-negative."""
284
# Edge case: very low OLR
285
rf_lw = contrail.rf_longwave(0, 220)
286
assert rf_lw >= 0
287
288
def test_rf_net(self):
289
"""Test net radiative forcing calculation."""
290
zenith = 30
291
tau = 0.4
292
tau_c = 0.36
293
olr = 250
294
temperature = 220
295
296
rf_total = contrail.rf_net(zenith, tau, tau_c, olr, temperature)
297
rf_sw = contrail.rf_shortwave(zenith, tau, tau_c)
298
rf_lw = contrail.rf_longwave(olr, temperature)
299
300
# Net should equal SW + LW
301
assert rf_total == pytest.approx(rf_sw + rf_lw, rel=RTOL)
302
303
def test_rf_net_nighttime_warming(self):
304
"""Test that net forcing is warming at night."""
305
zenith = 100 # Night
306
tau = 0.4
307
tau_c = 0.36
308
olr = 250
309
temperature = 220
310
311
rf_total = contrail.rf_net(zenith, tau, tau_c, olr, temperature)
312
313
# At night, only LW forcing, which is positive
314
assert rf_total > 0
315
316
317
class TestContrailOpticalEvolution:
318
"""Tests for contrail optical property evolution."""
319
320
def test_contrail_optical_properties_young(self):
321
"""Test optical properties for young contrail (0-1 hours)."""
322
tau, width, tau_c = contrail.contrail_optical_properties(0.5)
323
324
assert tau == 0.4
325
assert width == 500
326
assert tau_c == 0.36
327
328
def test_contrail_optical_properties_aged(self):
329
"""Test optical properties for aged contrail (6+ hours)."""
330
tau, width, tau_c = contrail.contrail_optical_properties(8.0)
331
332
assert tau == 0.71
333
assert width == 10500
334
assert tau_c == 0.639
335
336
def test_contrail_optical_properties_intermediate(self):
337
"""Test optical properties at intermediate ages."""
338
# 1-2 hours
339
tau, width, tau_c = contrail.contrail_optical_properties(1.5)
340
assert tau == 0.6
341
assert width == 1500
342
assert tau_c == 0.54
343
344
# 2-4 hours
345
tau, width, tau_c = contrail.contrail_optical_properties(3.0)
346
assert tau == 0.68
347
assert width == 3500
348
assert tau_c == 0.612
349
350
# 4-6 hours
351
tau, width, tau_c = contrail.contrail_optical_properties(5.0)
352
assert tau == 0.70
353
assert width == 6500
354
assert tau_c == 0.63
355
356
def test_contrail_optical_properties_array(self):
357
"""Test optical properties with array input."""
358
ages = np.array([0.5, 1.5, 3.0, 5.0, 8.0])
359
360
tau, width, tau_c = contrail.contrail_optical_properties(ages)
361
362
assert isinstance(tau, np.ndarray)
363
assert isinstance(width, np.ndarray)
364
assert isinstance(tau_c, np.ndarray)
365
assert tau.shape == (5,)
366
367
# Values should be monotonically increasing with age
368
assert np.all(np.diff(tau) >= 0)
369
assert np.all(np.diff(width) >= 0)
370
assert np.all(np.diff(tau_c) >= 0)
371
372
def test_contrail_optical_properties_scalar_return(self):
373
"""Test that scalar input returns scalar outputs."""
374
tau, width, tau_c = contrail.contrail_optical_properties(2.5)
375
376
assert isinstance(tau, float)
377
assert isinstance(width, float)
378
assert isinstance(tau_c, float)
379
380
381
class TestContrailLoadOLR:
382
"""Tests for OLR data loading."""
383
384
def test_load_olr_import_error(self):
385
"""Test that load_olr raises ImportError if xarray not available."""
386
# This test only works if xarray is not installed
387
# If xarray is installed, this test is skipped
388
try:
389
import xarray
390
391
pytest.skip("xarray is installed, cannot test ImportError")
392
except ImportError:
393
with pytest.raises(ImportError, match="xarray is required"):
394
contrail.load_olr("fake_file.nc", 0, 0, None)
395
396
397
class TestContrailModuleConstants:
398
"""Tests for module constants."""
399
400
def test_physical_constants(self):
401
"""Test that physical constants have correct values."""
402
assert contrail.gas_constant_water_vapor == 461.51
403
assert contrail.gas_constant_dry_air == 287.05
404
assert contrail.ei_water == 1.2232
405
assert contrail.spec_combustion_heat == 43e6
406
407
def test_default_propulsion_efficiency(self):
408
"""Test default propulsion efficiency value."""
409
assert contrail.DEFAULT_PROPULSION_EFFICIENCY == 0.4
410
411
412
if __name__ == "__main__":
413
pytest.main([__file__, "-v"])
414
415