Passed
Push — master ( 76dfc4...822cea )
by Stefan
05:24
created

nrlmsise00.dataset.core   A

Complexity

Total Complexity 17

Size/Duplication

Total Lines 286
Duplicated Lines 75.87 %

Importance

Changes 0
Metric Value
wmc 17
eloc 117
dl 217
loc 286
rs 10
c 0
b 0
f 0

4 Functions

Rating   Name   Duplication   Size   Complexity  
B _check_lst() 30 30 7
A _check_nd() 0 8 2
A _check_gm() 20 20 5
B msise_4d() 167 167 3

How to fix   Duplicated Code   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

1
# Copyright (c) 2020 Stefan Bender
2
#
3
# This file is part of pynrlmsise00.
4
# pynrlmsise00 is free software: you can redistribute it or modify
5
# it under the terms of the GNU General Public License as published
6
# by the Free Software Foundation, version 2.
7
# See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html.
8
"""Python 4-D `xarray.dataset` interface to the NRLMSISE-00 model
9
10
"""
11
import numpy as np
12
import pandas as pd
13
import xarray as xr
14
15
from spaceweather import sw_daily
16
17
from ..core import msise_flat
18
19
__all__ = ["msise_4d"]
20
21
MSIS_OUTPUT = [
22
	# name, long name, units
23
	# number densities
24
	("He", "He number density", "cm^-3"),
25
	("O", "O number density", "cm^-3"),
26
	("N2", "N2 number density", "cm^-3"),
27
	("O2", "O2 number density", "cm^-3"),
28
	("Ar", "AR number density", "cm^-3"),
29
	("rho", "total mass density", "g cm^-3"),  # includes d[8] in gtd7d
30
	("H", "H number density", "cm^-3"),
31
	("N", "N number density", "cm^-3"),
32
	("AnomO", "Anomalous oxygen number density", "cm^-3"),
33
	# temperature
34
	("Texo", "Exospheric temperature", "K"),
35
	("Talt", "Temperature at alt", "K"),
36
]
37
38
SW_INDICES = [
39
	# name, long name, units
40
	("Ap", "Daily Ap index", "nT"),
41
	(
42
		"f107",
43
		"Observed solar f10.7 cm radio flux of the previous day",
44
		"sfu, 10^-22 W m^-2 Hz^-1"
45
	),
46
	(
47
		"f107a",
48
		"Observed 81-day running average of the solar f10.7 cm radio flux centred on day",
49
		"sfu, 10^-22 W m^-2 Hz^-1"
50
	),
51
]
52
53
54
# helper functions
55
def _check_nd(a, ndim=1):
56
	a = np.atleast_1d(a)
57
	if a.ndim > ndim:
58
		raise ValueError(
59
			"Only scalars and up to {ndim}-D arrays are currently supported, "
60
			"got {a.ndim} dimensional array.".format(ndim=ndim, a=a)
61
		)
62
	return a
63
64
65 View Code Duplication
def _check_gm(gm, dts, df=None):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
66
	"""Check that GM indices have the correct shape
67
68
	Returns the GM index broadcasted to shape (`dts`,).
69
	"""
70
	def _dfloc(t, df=None):
71
		return df.loc[t.floor("D")]
72
73
	if gm is None and df is not None:
74
		# select arbitrary shapes from pandas dataframes
75
		return np.vectorize(_dfloc, excluded=["df"])(dts, df=df)
76
	gm = np.atleast_1d(gm)
77
	if gm.ndim > 1:
78
		raise ValueError(
79
			"GM coefficients must be either scalars or one-dimensional arrays, "
80
			"other shapes are currently not supported."
81
		)
82
	if gm.shape != dts.shape:
83
		gm = np.broadcast_to(gm, dts.shape)
84
	return gm
85
86
87 View Code Duplication
def _check_lst(lst, time, lon):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
88
	"""Check that LST has the correct shape
89
90
	Returns the LST broadcasted to shape (`time`, `lon`).
91
	"""
92
	lst = np.atleast_1d(lst)
93
	if lst.ndim > 2:
94
		raise ValueError("Only up to 2-D arrays are supported for LST.")
95
	if lst.ndim == 2:
96
		if (
97
			lst.shape[0] in [1, lon.size]
98
			and lst.shape[1] in [1, time.size]
99
		):
100
			# got shape (lon, time), (1, time), (lon, 1) or (1, 1)
101
			lst = lst.T
102
		else:
103
			# require shape (time, lon), (1, lon), (time, 1), or (1, 1)
104
			assert (
105
				lst.shape[0] in [1, time.size]
106
				and lst.shape[1] in [1, lon.size]
107
			)
108
	elif lst.ndim == 1:
109
		if lst.size == lon.size:
110
			# longitude takes precedence
111
			lst = lst[None, :]
112
		else:
113
			assert lst.size in [1, time.size]
114
			lst = lst[:, None]
115
	lsts = np.broadcast_to(lst, (time.size, lon.size))
116
	return lsts
117
118
119 View Code Duplication
def msise_4d(
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
120
	time, alt, lat, lon,
121
	f107a=None, f107=None, ap=None,
122
	lst=None,
123
	ap_a=None, flags=None,
124
	method="gtd7",
125
):
126
	"""4-D Xarray Interface to :func:`msise_flat()`.
127
128
	4-D MSIS model atmosphere as a `xarray.Dataset` with dimensions
129
	(time, alt, lat, lon). Only scalars and 1-D arrays for `time`, `alt`,
130
	`lat`, and `lon` are supported.
131
132
	The geomagnetic and Solar flux indices can be acquired using the
133
	`spaceweather` package (when set to `None`, the default).
134
	They can also be supplied explicitly as scalars or 1-D arrays with
135
	the same shape as `time`.
136
137
	The local solar time (LST) will usually be calculated from `time` and `lon`,
138
	but can be set explicitly using a fixed scalar LST, a 1-D array matching
139
	the shape of either `time` or `lon`, or a 2-D array matching the shape
140
	of (`time`, `lon`).
141
142
	Parameters
143
	----------
144
	time: float or 1-d array_like (I,)
145
		Time as `datetime.datetime`s.
146
	alt: float or 1-d array_like (J,)
147
		Altitudes in [km].
148
	lat: float or 1-d array_like (K,)
149
		Latitudes in [°N].
150
	lon: float or 1-d array_like (L,)
151
		Longitudes in [°E]
152
	f107a: float or 1-d array_like (I,), optional
153
		The 81-day running average Solar 10.7cm radio flux,
154
		centred at the day(s) of `time`.
155
		Set to `None` (default) to use the `spaceweather` package.
156
	f107: float or 1-d array_like (I,), optional
157
		The Solar 10.7cm radio flux at the previous day(s) of `time`.
158
		Set to `None` (default) to use the `spaceweather` package.
159
	ap: float or 1-d array_like (I,), optional
160
		The daily geomagnetic Ap index at the day(s) of `time`.
161
		Set to `None` (default) to use the `spaceweather` package.
162
	lst: float, 1-d (I,) or (L,) or 2-d array_like (I,L), optional
163
		The local solar time at `time` and `lon`, to override the
164
		calculated values.
165
		Default: `None` (calculate from `time` and `lon`)
166
	ap_a: list of int (7,), optional
167
		List of Ap indices, passed to `msise_flat()`,
168
		broadcasting is currently not supported.
169
	flags: list of int (23,), optional
170
		List of flags, passed to `msise_flat()`,
171
		broadcasting is currently not supported.
172
	method: str, optional, default "gtd7"
173
		Select MSISE-00 method, changes the output of "rho",
174
		the atmospheric mass density.
175
176
	Returns
177
	-------
178
	msise_4d: `xarray.Dataset`
179
		The MSIS atmosphere with dimensions ("time", "alt", "lat", "lon")
180
		and shape (I, J, K, L) containing the data arrays:
181
		"He", "O", "N2", "O2", "Ar", "rho", "H", "N", "AnomO", "Texo", "Talt",
182
		as well as the local solar times "lst" (I,L), and the
183
		values used for "Ap" (I,), "f107" (I,), "f107a" (I,).
184
185
	Example
186
	-------
187
	Using "internal" setting of the geomagnetic and Solar indices
188
	and automatic conversion to 4-D looks like this
189
	("time" can be an array of `datetime.datetime`s as well):
190
191
	>>> from datetime import datetime
192
	>>> from nrlmsise00.dataset import msise_4d
193
	>>> alts = np.arange(200, 401, 100.)  # = [200, 300, 400] [km]
194
	>>> lats = np.arange(60, 71, 10.)  # = [60, 70] [°N]
195
	>>> lons = np.arange(-70., 71., 35.)  # = [-70, -35,  0, 35, 70] [°E]
196
	>>> # broadcasting is done internally
197
	>>> ds = msise_4d(datetime(2009, 6, 21, 8, 3, 20), alts, lats, lons)
198
	>>> ds
199
	<xarray.Dataset>
200
	Dimensions:  (alt: 3, lat: 2, lon: 5, time: 1)
201
	Coordinates:
202
	  * time     (time) datetime64[ns] 2009-06-21T08:03:20
203
	  * alt      (alt) float64 200.0 300.0 400.0
204
	  * lat      (lat) float64 60.0 70.0
205
	  * lon      (lon) float64 -70.0 -35.0 0.0 35.0 70.0
206
	Data variables:
207
	    He       (time, alt, lat, lon) float64 8.597e+05 1.063e+06 ... 4.936e+05
208
	    O        (time, alt, lat, lon) float64 1.248e+09 1.46e+09 ... 2.635e+07
209
	    N2       (time, alt, lat, lon) float64 2.555e+09 2.654e+09 ... 1.667e+06
210
	    O2       (time, alt, lat, lon) float64 2.1e+08 2.062e+08 ... 3.471e+04
211
	    Ar       (time, alt, lat, lon) float64 3.16e+06 3.287e+06 ... 76.55 67.16
212
	    rho      (time, alt, lat, lon) float64 1.635e-13 1.736e-13 ... 7.984e-16
213
	    H        (time, alt, lat, lon) float64 3.144e+05 3.02e+05 ... 1.237e+05
214
	    N        (time, alt, lat, lon) float64 9.095e+06 1.069e+07 ... 6.765e+05
215
	    AnomO    (time, alt, lat, lon) float64 1.173e-08 1.173e-08 ... 1.101e+04
216
	    Texo     (time, alt, lat, lon) float64 805.2 823.7 807.1 ... 818.7 821.2
217
	    Talt     (time, alt, lat, lon) float64 757.9 758.7 766.4 ... 818.7 821.1
218
	    lst      (time, lon) float64 3.389 5.722 8.056 10.39 12.72
219
	    Ap       (time) int32 6
220
	    f107     (time) float64 66.7
221
	    f107a    (time) float64 69.0
222
223
	See also
224
	--------
225
	msise_flat
226
	"""
227
228
	time = _check_nd(time)
229
	alt = _check_nd(alt)
230
	lat = _check_nd(lat)
231
	lon = _check_nd(lon)
232
233
	sw = sw_daily()
234
	# convert arbitrary shapes
235
	dts = np.vectorize(pd.to_datetime)(time, utc=True)
236
	dtps = dts - pd.to_timedelta("1d")
237
238
	ap = _check_gm(ap, dts, df=sw[["Apavg"]])
239
	f107 = _check_gm(f107, dtps, df=sw[["f107_obs"]])
240
	f107a = _check_gm(f107a, dts, df=sw[["f107_81ctr_obs"]])
241
242
	# expand dimensions to 4d
243
	ts = time[:, None, None, None]
244
	alts = alt[None, :, None, None]
245
	lats = lat[None, None, :, None]
246
	lons = lon[None, None, None, :]
247
248
	aps = ap[:, None, None, None]
249
	f107s = f107[:, None, None, None]
250
	f107as = f107a[:, None, None, None]
251
252
	if lst is not None:
253
		lsts = _check_lst(lst, time, lon)
254
		lst = lsts[:, None, None, :]
255
	else:
256
		lsts = np.array([
257
			t.hour + t.minute / 60. + t.second / 3600 + lon / 15.
258
			for t in time
259
		])
260
261
	msis_data = msise_flat(
262
		ts, alts, lats, lons,
263
		f107as, f107s, aps,
264
		lst=lst, ap_a=ap_a, flags=flags, method=method,
265
	)
266
	ret = xr.Dataset(
267
		{
268
			m[0]: (
269
				["time", "alt", "lat", "lon"],
270
				d,
271
				{"long_name": m[1], "units": m[2]}
272
			)
273
			for m, d in zip(MSIS_OUTPUT, np.rollaxis(msis_data, -1))
274
		},
275
		coords={"time": time, "alt": alt, "lat": lat, "lon": lon},
276
	)
277
	ret["lst"] = (
278
		["time", "lon"], lsts, {"long_name": "Mean Local Solar Time", "units": "h"}
279
	)
280
	ret["Ap"] = (["time"], ap)
281
	ret["f107"] = (["time"], f107)
282
	ret["f107a"] = (["time"], f107a)
283
	for _sw in SW_INDICES:
284
		ret[_sw[0]].attrs.update({"long_name": _sw[1], "units": _sw[2]})
285
	return ret
286