-
Notifications
You must be signed in to change notification settings - Fork 4
/
synthobs.py
377 lines (335 loc) · 12.4 KB
/
synthobs.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
"""Functions for calculating synthetic observations."""
import warnings
from iris.analysis import SUM
from iris.coords import AuxCoord, DimCoord
from iris.cube import Cube
from iris.exceptions import CoordinateNotFoundError as CoNotFound
from iris.util import reverse
import numpy as np
from .coord import roll_cube_pm180
from .model import um
__all__ = (
"calc_geom_mean_mirrored",
"calc_stellar_flux",
"calc_transmission_spectrum",
"calc_transmission_spectrum_day_night_average",
"read_normalized_stellar_flux",
"read_spectral_bands",
)
def calc_geom_mean_mirrored(cube_a, cube_b, add_shift=0, model=um):
"""
Calculate geometric mean of 2 cubes, one of them flipped along the x-axis.
This function can be used to get an average transmission flux
calculated separately from the day- and night-side perspective.
cube_a: iris.cube.Cube
Cube with an x-coordinate.
cube_b: iris.cube.Cube
Another cube, to be flipped before being averaged with cube A.
add_shift: int
Additional shift for the data along the x-coordinate.
model: aeolus.model.Model, optional
Model class with relevant variable names.
"""
# Get x-coordinate from the 1st cube.
x_coord = cube_a.coord(model.x)
# Roll and reverse the 2nd cube by 180 degrees
# This is specific to the UM output
cube_b_flipped = reverse(
roll_cube_pm180(cube_b, add_shift=add_shift, model=model), model.x
)
cube_b_flipped.replace_coord(x_coord)
# Calculate geometric mean of the two cubes
geom_mean = (cube_a * cube_b_flipped) ** 0.5
return geom_mean
def calc_stellar_flux(spectral_file, stellar_constant_at_1_au):
"""
Calculate the stellar flux per spectral band.
Parameters
----------
spectral_file: pathlib.Path
Path to the location of a SOCRATES spectral file.
stellar_constant_at_1_au: float or iris.cube.Cube
Stellar constant at 1 AU [W m-2].
Returns
-------
iris.cube.Cube
Stellar flux per spectral band [W m-2].
"""
# Ensure that an input constant is an iris cube
if not isinstance(stellar_constant_at_1_au, Cube):
stellar_constant_at_1_au = Cube(
stellar_constant_at_1_au,
long_name="stellar_constant_at_1_au",
units="W m-2",
)
normalized_stellar_flux = read_normalized_stellar_flux(spectral_file)
stellar_flux = normalized_stellar_flux * stellar_constant_at_1_au
stellar_flux.rename("stellar_flux")
return stellar_flux
def calc_transmission_spectrum(
trans_flux,
spectral_file=None,
stellar_constant_at_1_au=None,
stellar_radius=None,
planet_top_of_atmosphere=None,
model=um,
):
r"""
Convert the transmission flux to a planetary-stellar radius ratio.
Parameters
----------
trans_flux: iris.cube.Cube
Transmission flux on spectral bands and optionally lats and lons.
In the Met Office Unified Model this is
STASH items 555, 556, 755, 756 in section 1.
spectral_file: pathlib.Path
Path to the location of a SOCRATES spectral file.
stellar_constant_at_1_au: float or iris.cube.Cube
Stellar constant at 1 AU [W m-2].
stellar_radius: float or iris.cube.Cube
Stellar radius [m].
planet_top_of_atmosphere: float or iris.cube.Cube
The extent of the planetary atmosphere [m].
model: aeolus.model.Model, optional
Model class with relevant variable names.
Returns
-------
iris.cube.Cube
The ratio of the effective planetary radius to the stellar radius
per spectral band [1].
Spectral band centres [m] is attached as an auxiliary coordinate.
Notes
-----
The transmission spectrum is the ratio of the effective planetary
radius to the stellar radius calculated per spectral band:
.. math::
\frac{R_p (\nu)}{R_s} = \sqrt{(\frac{R_{p,TOA}}{R_s})^2 -
\frac{\sum_{lat,lon}^{}F_{trans} (\nu)}{F_{stellar} (\nu)}}
where
:math:`R_p(\nu)` is the effective planetary radius,
:math:`R_s` is the stellar radius,
:math:`R_{p,TOA}` is the extent of the planetary atmosphere
(i.e. the sum of the planetary radius and the height of the model domain),
:math:`\sum_{lat,lon}^{}F_{trans}(\nu)` is the total transmitted flux,
:math:`F_{stellar}(\nu)` is the stellar flux.
Smaller gas abundance leads to the corresponding limb to be
"more transmissive", which leads it having smaller transit depth.
"""
# Ensure that input constants are iris cubes
if not isinstance(stellar_constant_at_1_au, Cube):
stellar_constant_at_1_au = Cube(
stellar_constant_at_1_au,
long_name="stellar_constant_at_1_au",
units="W m-2",
)
if not isinstance(stellar_radius, Cube):
stellar_radius = Cube(
stellar_radius,
long_name="stellar_radius",
units="m",
)
if not isinstance(planet_top_of_atmosphere, Cube):
planet_top_of_atmosphere = Cube(
planet_top_of_atmosphere,
long_name="planet_top_of_atmosphere",
units="m",
)
# Calculate stellar flux
stellar_flux = calc_stellar_flux(spectral_file, stellar_constant_at_1_au)
# Sum transmission flux over all latitudes and longitudes
try:
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=UserWarning)
trans_flux = trans_flux.collapsed([model.y, model.x], SUM)
except CoNotFound:
pass
# trans_flux.rename("shortwave_transmission_flux")
trans_flux.units = "W m-2"
# Calculate the ratio of the total transmitted flux to the stellar flux
for coord in trans_flux.dim_coords:
if coord.name().lower() in ["pseudo", "pseudo_level"]:
coord.rename("spectral_band_index")
break
flux_ratio = trans_flux / stellar_flux
# The ratio of the effective planetary radius to the stellar radius
rp_eff_over_rs_squared = (
planet_top_of_atmosphere / stellar_radius
) ** 2 - flux_ratio
# Make all negative values zero
rp_eff_over_rs_squared = rp_eff_over_rs_squared.copy(
data=rp_eff_over_rs_squared.core_data().clip(min=0.0)
)
rp_eff_over_rs = rp_eff_over_rs_squared ** (0.5)
rp_eff_over_rs.rename(
"ratio_of_effective_planetary_radius_to_stellar_radius"
)
# Find spectral band centers
spectral_bands = read_spectral_bands(spectral_file)
spectral_band_centres = 0.5 * (
spectral_bands["lower_wavelength_limit"]
+ spectral_bands["upper_wavelength_limit"]
)
spectral_bands_coord = AuxCoord(
spectral_band_centres,
long_name="spectral_band_centres",
units="m",
)
spectral_bands_ll = AuxCoord(
spectral_bands["lower_wavelength_limit"],
long_name="spectral_band_lower_limit",
units="m",
)
spectral_bands_ul = AuxCoord(
spectral_bands["upper_wavelength_limit"],
long_name="spectral_band_upper_limit",
units="m",
)
# Attach spectral bands to the resulting cube as an auxiliary coordinate
coord_dim = rp_eff_over_rs.coord_dims("spectral_band_index")[0]
rp_eff_over_rs.add_aux_coord(spectral_bands_coord, data_dims=(coord_dim,))
rp_eff_over_rs.add_aux_coord(spectral_bands_ll, data_dims=(coord_dim,))
rp_eff_over_rs.add_aux_coord(spectral_bands_ul, data_dims=(coord_dim,))
return rp_eff_over_rs
def calc_transmission_spectrum_day_night_average(
trans_flux_day,
trans_flux_night,
add_shift=0,
spectral_file=None,
stellar_constant_at_1_au=None,
stellar_radius=None,
planet_top_of_atmosphere=None,
model=um,
):
r"""
Convert the transmission flux to a planetary-stellar radius ratio.
For UM output, this function averages the flux calculated from the day-side
and the night-side of the planet.
Why does it use a geometric mean? The reason to use a geometric average
instead of an arithmetic average is that the optical depths are added.
The flux decreases via Beer's law (i.e., it's proportional to
:math:`exp[-optical depth]`) so when you multiply the dayside fluxes and
nightside fluxes together and take a square root, you end up with
:math:`exp[-( optical depth 1 + optical depth 2)/2]`. Since each optical
depth is double the optical depth for it's respective side, the factors of
two cancel and you end up with :math:`exp[-(true optical depth)]`.
Parameters
----------
trans_flux_day: iris.cube.Cube
Transmission flux on spectral bands and optionally lats and lons.
Day-side perspective.
In the Met Office Unified Model this is
STASH items 555, 556, 755, 756 in section 1.
trans_flux_night: iris.cube.Cube
Samea as day, but for the night-side calculation.
add_shift: int, optional
Additional shift of data along the x-coordinate.
See Also
--------
aeolus.synthobs.calc_transmission_spectrum
"""
# Average the day and night flux
trans_flux = calc_geom_mean_mirrored(
trans_flux_day, trans_flux_night, add_shift=add_shift, model=model
)
return calc_transmission_spectrum(
trans_flux,
spectral_file=spectral_file,
stellar_constant_at_1_au=stellar_constant_at_1_au,
stellar_radius=stellar_radius,
planet_top_of_atmosphere=planet_top_of_atmosphere,
model=model,
)
def read_normalized_stellar_flux(spectral_file):
"""
Read the normalized stellar flux per band from a SOCRATES spectral file.
Parameters
----------
spectral_file: pathlib.Path
Path to the location of the SOCRATES spectral file.
Returns
-------
iris.cube.Cube
Normalized stellar flux per spectral band [1].
"""
with spectral_file.open("r") as f:
lines = []
band_block = False
for line in f:
if line.startswith("*BLOCK: TYPE = 2"):
band_block = True
if band_block:
if line.startswith("*END"):
break
elif line.lstrip().split(" ")[0].isnumeric():
lines.append(
tuple(
[
float(i)
for i in line.lstrip().rstrip("\n").split(" ")
if i
]
)
)
normalized_stellar_flux_arr = np.array(
lines,
dtype=[
("spectral_band_index", "u4"),
("normalized_stellar_flux", "f4"),
],
)
# Compose an iris cube
spectral_band_index = DimCoord(
normalized_stellar_flux_arr["spectral_band_index"],
long_name="spectral_band_index",
units="1",
)
normalized_stellar_flux = Cube(
normalized_stellar_flux_arr["normalized_stellar_flux"],
long_name="normalized_stellar_flux",
dim_coords_and_dims=[(spectral_band_index, 0)],
units="1",
)
return normalized_stellar_flux
def read_spectral_bands(spectral_file):
"""
Read spectral bands from a SOCRATES spectral file.
Parameters
----------
spectral_file: pathlib.Path
Path to the location of a SOCRATES spectral file.
Returns
-------
numpy.ndarray
An array with a list of tuples describing spectral bands.
Tuple structure:
(spectral_band_index, lower_wavelength_limit, upper_wavelength_limit).
Units [m] as stated in a spectral file but not checked directly.
"""
with spectral_file.open("r") as f:
lines = []
band_block = False
for line in f:
if line.startswith("*BLOCK: TYPE = 1"):
band_block = True
if band_block:
if line.startswith("*END"):
break
elif line.lstrip().split(" ")[0].isnumeric():
lines.append(
tuple(
[
float(i)
for i in line.lstrip().rstrip("\n").split(" ")
if i
]
)
)
spectral_bands = np.array(
lines,
dtype=[
("spectral_band_index", "u4"),
("lower_wavelength_limit", "f4"),
("upper_wavelength_limit", "f4"),
],
)
return spectral_bands