Skip to content

Commit 8efe798

Browse files
authored
Merge pull request #67 from greglucas/output-enum
ENH: Add Output enumeration to make it easier to access variables
2 parents 3e18692 + 2d2fa2b commit 8efe798

12 files changed

+139
-110
lines changed

CHANGELOG.md

+6-2
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ All notable changes to this project will be documented in this file.
1010
to the use of many global/save variables. There is a lock around the
1111
extension modules so that only one thread will be calling the routines
1212
at a time, so the Python library is safe to use in a multi-threaded context.
13+
- **ADDED** Variable enumeration for easier indexing into output arrays.
14+
- This can be used as `msis.Variable.O2` for getting the `O2` species index.
15+
For example, `output_array[..., msis.Variable.O2]`.
1316
- **MAINTENANCE** Default `-O1` optimization level for all builds.
1417
- Previously, this
1518
was only done on Windows machines. Users can change this by updating
16-
environment variables before building with `FFLAGS=-Ofast`, but note that
17-
some machines produce invalid results when higher optimizations are used.
19+
environment variables before building with `FFLAGS=-Ofast pip install .`,
20+
but note that some machines produce invalid results when higher
21+
optimizations are used.
1822
- **PERFORMANCE** Cache options state between subsequent runs.
1923
- Avoid calling initialization switches unless they have changed between runs
2024
- **PERFORMANCE** Speed up numpy function calls.

docs/source/reference/index.rst

+11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ To run the code, use the :mod:`pymsis.msis` module.
1414
msis.create_options
1515
msis.create_input
1616

17+
The output of the model is stored in basic numpy arrays with the final
18+
dimension being the variable/species. To get the output in a more human-readable
19+
format, use the :class:`~.Variable` enumeration that provides
20+
easier indexing into the output arrays.
21+
22+
.. autosummary::
23+
:toctree: generated/
24+
:nosignatures:
25+
26+
msis.Variable
27+
1728
To get input data for historical events, use the :mod:`pymsis.utils` module.
1829

1930
.. autosummary::

examples/plot_altitude_profiles.py

+5-19
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,13 @@
3333
output_midnight = np.squeeze(output_midnight)
3434
output_noon = np.squeeze(output_noon)
3535

36-
variables = [
37-
"Total mass density",
38-
"N2",
39-
"O2",
40-
"O",
41-
"He",
42-
"H",
43-
"Ar",
44-
"N",
45-
"Anomalous O",
46-
"NO",
47-
"Temperature",
48-
]
49-
5036
_, ax = plt.subplots()
51-
for i, label in enumerate(variables):
52-
if label in ("NO", "Total mass density", "Temperature"):
53-
# There is currently no NO data, also ignore non-number densities
37+
for variable in msis.Variable:
38+
if variable.name in ("Total mass density", "Temperature"):
39+
# Ignore non-number densities
5440
continue
55-
(line,) = ax.plot(output_midnight[:, i], alts, linestyle="--")
56-
ax.plot(output_noon[:, i], alts, c=line.get_color(), label=label)
41+
(line,) = ax.plot(output_midnight[:, variable], alts, linestyle="--")
42+
ax.plot(output_noon[:, variable], alts, c=line.get_color(), label=variable.name)
5743

5844
ax.legend(
5945
loc="upper center", bbox_to_anchor=(0.5, 1.15), fancybox=True, shadow=True, ncol=4

examples/plot_annual_variation.py

+3-17
Original file line numberDiff line numberDiff line change
@@ -35,26 +35,12 @@
3535
# Lets get the percent variation from the annual mean for each variable
3636
variation = 100 * (output / output.mean(axis=0) - 1)
3737

38-
variables = [
39-
"Total mass density",
40-
"N2",
41-
"O2",
42-
"O",
43-
"He",
44-
"H",
45-
"Ar",
46-
"N",
47-
"Anomalous O",
48-
"NO",
49-
"Temperature",
50-
]
51-
5238
_, ax = plt.subplots()
53-
for i, label in enumerate(variables):
54-
if label == "NO":
39+
for variable in msis.Variable:
40+
if variable.name == "NO":
5541
# There is currently no NO data
5642
continue
57-
ax.plot(dates, variation[:, i], label=label)
43+
ax.plot(dates, variation[:, variable], label=variable.name)
5844

5945
ax.legend(
6046
loc="upper center", bbox_to_anchor=(0.5, 1.15), fancybox=True, shadow=True, ncol=5

examples/plot_diurnal_variation.py

+2-19
Original file line numberDiff line numberDiff line change
@@ -36,26 +36,9 @@
3636
# Lets get the percent variation from the annual mean for each variable
3737
variation = 100 * (output / output.mean(axis=0) - 1)
3838

39-
variables = [
40-
"Total mass density",
41-
"N2",
42-
"O2",
43-
"O",
44-
"He",
45-
"H",
46-
"Ar",
47-
"N",
48-
"Anomalous O",
49-
"NO",
50-
"Temperature",
51-
]
52-
5339
_, ax = plt.subplots()
54-
for i, label in enumerate(variables):
55-
if label == "NO":
56-
# There is currently no NO data
57-
continue
58-
ax.plot(dates, variation[:, i], label=label)
40+
for variable in msis.Variable:
41+
ax.plot(dates, variation[:, variable], label=variable.name)
5942

6043
ax.legend(
6144
loc="upper center", bbox_to_anchor=(0.5, 1.15), fancybox=True, shadow=True, ncol=5

examples/plot_surface.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,9 @@
2828
# Get rid of the single dimensions
2929
output = np.squeeze(output)
3030

31-
i = 2 # O2
32-
3331
_, ax = plt.subplots()
3432
xx, yy = np.meshgrid(lons, lats)
35-
mesh = ax.pcolormesh(xx, yy, output[:, :, i].T, shading="auto")
33+
mesh = ax.pcolormesh(xx, yy, output[:, :, msis.Variable.O2].T, shading="auto")
3634
plt.colorbar(mesh, label="Number density (/m$^3$)")
3735

3836
ax.set_title(f"O$_2$ number density at {alt} km")

examples/plot_surface_animation.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -35,21 +35,21 @@
3535
# Get rid of the single dimensions
3636
output = np.squeeze(output)
3737

38-
i = 7 # N
39-
4038
fig, (ax_time, ax_mesh) = plt.subplots(
4139
nrows=2, gridspec_kw={"height_ratios": [1, 4]}, constrained_layout=True
4240
)
4341
xx, yy = np.meshgrid(lons, lats)
4442
vmin, vmax = 1e13, 8e13
4543
norm = matplotlib.colors.Normalize(vmin, vmax)
46-
mesh = ax_mesh.pcolormesh(xx, yy, output[0, :, :, i].T, shading="auto", norm=norm)
44+
mesh = ax_mesh.pcolormesh(
45+
xx, yy, output[0, :, :, msis.Variable.N].T, shading="auto", norm=norm
46+
)
4747
plt.colorbar(
4848
mesh, label=f"N number density at {alt} km (/m$^3$)", orientation="horizontal"
4949
)
5050

5151
time_data = output[:, len(lons) // 2, len(lats) // 2, :]
52-
ax_time.plot(dates, time_data[:, i], c="k")
52+
ax_time.plot(dates, time_data[:, msis.Variable.N], c="k")
5353
ax_time.set_xlim(dates[0], dates[-1])
5454
ax_time.set_ylim(vmin, vmax)
5555
ax_time.xaxis.set_major_locator(mdates.HourLocator(interval=3))
@@ -74,7 +74,7 @@ def update_surface(t):
7474
date_string = dates[t].astype("O").strftime("%H:%M")
7575
title.set_text(f"{date_string}")
7676
time_line.set_xdata([dates[t]])
77-
mesh.set_array(output[t, :, :, i].T)
77+
mesh.set_array(output[t, :, :, msis.Variable.N].T)
7878
sun.set_data([sun_loc[t]], [0])
7979

8080

examples/plot_version_diff_altitude.py

+6-19
Original file line numberDiff line numberDiff line change
@@ -39,27 +39,14 @@
3939
diff_midnight = np.squeeze(diff_midnight)
4040
diff_noon = np.squeeze(diff_noon)
4141

42-
variables = [
43-
"Total mass density",
44-
"N2",
45-
"O2",
46-
"O",
47-
"He",
48-
"H",
49-
"Ar",
50-
"N",
51-
"Anomalous O",
52-
"NO",
53-
"Temperature",
54-
]
55-
5642
_, ax = plt.subplots()
57-
for i, label in enumerate(variables):
58-
if label in ("NO", "Total mass density", "Temperature"):
59-
# There is currently no NO data, also ignore non-number densities
43+
for variable in msis.Variable:
44+
if variable.name in ("NO", "Total mass density", "Temperature"):
45+
# There is currently no NO data for earlier versions,
46+
# also ignore non-number densities
6047
continue
61-
(line,) = ax.plot(diff_midnight[:, i], alts, linestyle="--")
62-
ax.plot(diff_noon[:, i], alts, c=line.get_color(), label=label)
48+
(line,) = ax.plot(diff_midnight[:, variable], alts, linestyle="--")
49+
ax.plot(diff_noon[:, variable], alts, c=line.get_color(), label=variable.name)
6350

6451
ax.legend(
6552
loc="upper center", bbox_to_anchor=(0.5, 1.15), fancybox=True, shadow=True, ncol=4

examples/plot_version_diff_surface.py

+8-17
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,18 @@
3232
# Get rid of the single dimensions
3333
diff = np.squeeze(diff)
3434

35-
variables = [
36-
"Total mass density",
37-
"N2",
38-
"O2",
39-
"O",
40-
"He",
41-
"H",
42-
"Ar",
43-
"N",
44-
"Anomalous O",
45-
"NO",
46-
"Temperature",
47-
]
48-
4935
fig, axarr = plt.subplots(nrows=3, ncols=3, constrained_layout=True, figsize=(8, 6))
5036
xx, yy = np.meshgrid(lons, lats)
5137
norm = mpl.colors.Normalize(-50, 50)
5238
cmap = mpl.colormaps["RdBu_r"]
53-
for i, ax in enumerate(axarr.flatten()):
54-
mesh = ax.pcolormesh(xx, yy, diff[:, :, i].T, shading="auto", norm=norm, cmap=cmap)
55-
ax.set_title(f"{variables[i]}")
39+
for i, variable in enumerate(msis.Variable):
40+
if i > 8:
41+
break
42+
ax = axarr.flatten()[i]
43+
mesh = ax.pcolormesh(
44+
xx, yy, diff[:, :, variable].T, shading="auto", norm=norm, cmap=cmap
45+
)
46+
ax.set_title(f"{variable.name}")
5647

5748
plt.colorbar(
5849
mesh, ax=axarr, label="Change from MSIS-00 to MSIS2 (%)", orientation="horizontal"

pymsis/msis.py

+71-8
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Interface for running and creating input for the MSIS models."""
22

33
import threading
4+
from enum import IntEnum
45
from pathlib import Path
56

67
import numpy as np
@@ -21,6 +22,54 @@
2122
lib._lock = threading.Lock()
2223

2324

25+
class Variable(IntEnum):
26+
"""
27+
Enumeration of output data indices for the pymsis run calls.
28+
29+
This can be used to access the data from the output arrays instead of having
30+
to remember the order of the output. For example,
31+
``output_array[..., Variable.MASS_DENSITY]``.
32+
33+
Attributes
34+
----------
35+
MASS_DENSITY
36+
Index of total mass density (kg/m3).
37+
N2
38+
Index of N2 number density (m-3).
39+
O2
40+
Index of O2 number density (m-3).
41+
O
42+
Index of O number density (m-3).
43+
HE
44+
Index of He number density (m-3).
45+
H
46+
Index of H number density (m-3).
47+
AR
48+
Index of Ar number density (m-3).
49+
N
50+
Index of N number density (m-3).
51+
ANOMALOUS_O
52+
Index of anomalous oxygen number density (m-3).
53+
NO
54+
Index of NO number density (m-3).
55+
TEMPERATURE
56+
Index of temperature (K).
57+
58+
"""
59+
60+
MASS_DENSITY = 0
61+
N2 = 1
62+
O2 = 2
63+
O = 3 # noqa: E741 (ambiguous name)
64+
HE = 4
65+
H = 5
66+
AR = 6
67+
N = 7
68+
ANOMALOUS_O = 8
69+
NO = 9
70+
TEMPERATURE = 10
71+
72+
2473
def run(
2574
dates: npt.ArrayLike,
2675
lons: npt.ArrayLike,
@@ -35,13 +84,25 @@ def run(
3584
**kwargs: dict,
3685
) -> npt.NDArray:
3786
"""
38-
Call MSIS looping over all possible inputs.
39-
40-
If ndates is the same as nlons, nlats, and nalts, then a flattened
41-
multi-point input array is assumed. Otherwise, the data
42-
will be expanded in a grid-like fashion. The possible
43-
return shapes are therefore (ndates, 11) and
44-
(ndates, nlons, nlats, nalts, 11).
87+
Call MSIS to calculate the atmosphere at the provided input points.
88+
89+
**Satellite Fly-Through Mode:**
90+
If ndates is the same length as nlons, nlats, and nalts, then the
91+
input arrays are assumed to be aligned and no regridding is done.
92+
This is equivalent to a satellite fly-through, producing an output
93+
return shape of (ndates, 11).
94+
95+
**Grid Mode:**
96+
If the input arrays have different lengths the data will be expanded
97+
in a grid-like fashion broadcasting to a larger shape than the input
98+
arrays. This is equivalent to a full atmosphere simulation where you
99+
want to calculate the data at every grid point. The output shape will
100+
be 5D (ndates, nlons, nlats, nalts, 11), with potentially single element
101+
dimensions if you have a single date, lon, lat, or alt.
102+
103+
The output array can be indexed with the :class:`~.Variable` enum
104+
for easier access. ``output_array[..., Variable.MASS_DENSITY]``
105+
returns the total mass density.
45106
46107
Parameters
47108
----------
@@ -58,7 +119,7 @@ def run(
58119
f107as : ArrayLike, optional
59120
F10.7 running 81-day average centered on the given date(s)
60121
aps : ArrayLike, optional
61-
| Ap for the given date(s), (1-6 only used if `geomagnetic_activity=-1`)
122+
| Ap for the given date(s), (1-6 only used if ``geomagnetic_activity=-1``)
62123
| (0) Daily Ap
63124
| (1) 3 hr ap index for current time
64125
| (2) 3 hr ap index for 3 hrs before current time
@@ -75,6 +136,8 @@ def run(
75136
MSIS version number, one of (0, 2.0, 2.1).
76137
**kwargs : dict
77138
Single options for the switches can be defined through keyword arguments.
139+
For example, run(..., geomagnetic_activity=-1) will set the geomagnetic
140+
activity switch to -1 (storm-time ap mode).
78141
79142
Returns
80143
-------

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ lint.select = ["B", "D", "E", "F", "I", "N", "S", "W", "PL", "PT", "UP", "RUF",
8686
lint.ignore = ["B028", "D203", "D212", "PLR0913", "S310"]
8787

8888
[tool.ruff.lint.per-file-ignores]
89-
"examples/*" = ["ANN", "D"]
89+
"examples/*" = ["ANN", "D", "PLR2004"]
9090
"tests/*" = ["ANN", "D", "S"]
9191
"tools/*" = ["ANN", "S"]
9292
".github/*" = ["S"]

tests/test_msis.py

+20
Original file line numberDiff line numberDiff line change
@@ -520,3 +520,23 @@ def run_function(input_items):
520520
for future in results:
521521
result, expected_result = future.result()
522522
assert_allclose(result, expected_result, rtol=1e-5)
523+
524+
525+
def test_output_enum(input_data):
526+
# Make sure we can access the output enums
527+
assert msis.Variable.MASS_DENSITY == 0
528+
assert msis.Variable._member_names_ == [
529+
"MASS_DENSITY",
530+
"N2",
531+
"O2",
532+
"O",
533+
"HE",
534+
"H",
535+
"AR",
536+
"N",
537+
"ANOMALOUS_O",
538+
"NO",
539+
"TEMPERATURE",
540+
]
541+
data = msis.run(*input_data)
542+
assert data[..., msis.Variable.MASS_DENSITY] == data[..., 0]

0 commit comments

Comments
 (0)