Passed
Push — master ( 680825...23f39f )
by Stefan
03:18
created

eppaurora.models.ssusiq2023._interp()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 8
nop 4
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
# coding: utf-8
2
# Copyright (c) 2023 Stefan Bender
3
#
4
# This file is part of pyeppaurora.
5
# pyeppaurora is free software: you can redistribute it or modify
6
# it under the terms of the GNU General Public License as published
7
# by the Free Software Foundation, version 2.
8
# See accompanying LICENSE file or http://www.gnu.org/licenses/gpl-2.0.html.
9
"""Empirical model for auroral ionization rates
10
11
Implements the empirical model for auroral ionization,
12
derived from SSUSI UV observations [1]_.
13
14
.. [1] Bender et al., in prep., 2023
15
"""
16
from logging import warning as warn
17
from os import path
18
from pkg_resources import resource_filename
19
20
import numpy as np
21
import xarray as xr
22
23
__all__ = [
24
	"ssusiq2023",
25
]
26
27
COEFF_FILE = "SSUSI_IRgrid_coeffs_f17f18.nc"
28
COEFF_PATH = resource_filename(__name__, path.join("data", COEFF_FILE))
29
30
31
def _interp(ds, method="linear", method_non_numeric="nearest", **kwargs):
32
	"""Fix `xarray` interpolation with non-numeric variables
33
	"""
34
	v_n = sorted(
35
		filter(lambda _v: np.issubdtype(ds[_v].dtype, np.number), ds)
36
	)
37
	v_nn = sorted(set(ds) - set(v_n))
38
	ds_n = ds[v_n].interp(method=method, **kwargs)
39
	ds_nn = ds[v_nn].sel(method=method_non_numeric, **kwargs)
40
	# override coordinates for `merge()`
41
	ds_nn = ds_nn.assign_coords(**ds_n.coords)
42
	return xr.merge([ds_n, ds_nn], join="left")
43
44
45
def ssusiq2023(
46
	gmlat,
47
	mlt,
48
	alt,
49
	sw_coeffs,
50
	coeff_ds=None,
51
	interpolate=False,
52
	method="linear",
53
	return_var=False,
54
):
55
	u"""
56
	Parameters
57
	----------
58
	gmlat: float
59
		Geomagnetic latitude in [degrees].
60
	mlt: float
61
		Magnetic local time in [hours].
62
	alt: float
63
		Altitude in [km]
64
	sw_coeffs: array_like or `xarray.DataArray`
65
		The space weather index values to use (for the requested time(s)),
66
		should be of shape (N, M) with N = number of proxies, currently 5:
67
		[Kp, PC, Ap, log(f10.7_81ctr_obs), log(v_plasma)].
68
		The `xarray.DataArray` should have a dimension named "proxy" with
69
		matching coordinates:
70
		["Kp", "PC", "Ap", "log_f107_81ctr_obs", "log_v_plasma"]
71
		All the other dimensions will be broadcasted.
72
	coeff_ds: `xarray.Dataset`, optional (default: None)
73
		Dataset with the model coefficients, `None` uses the packaged version.
74
	interpolate: bool, optional (default: False)
75
		If `True`, uses bilinear interpolate in MLT and geomagnetic latitude,
76
		using periodic (24h) boundary conditions in MLT. Otherwise, the closest
77
		MLT/geomagnetic latitude bin will be selected.
78
	method: str, optional (default: "linear")
79
		Interpolation method to use, see `scipy.interpolate.interpn` for options.
80
		Only used if `interpolate` is `True`.
81
	return_var: bool, optional (default: False)
82
		If `True`, returns the predicted variance in addition to the values,
83
		otherwise only the mean prediction is returned.
84
85
	Returns
86
	-------
87
	q: `xarray.DataArray`
88
		log(q), where q is the ionization rate in [cm⁻³ s⁻¹]
89
		if `return_var` is False.
90
	q, var(q): tuple of `xarray.DataArray`s
91
		log(q) and var(log(q)) where q is the ionization rate in [cm⁻³ s⁻¹]
92
		if `return_var` is True.
93
	"""
94
	coeff_ds = coeff_ds or xr.open_dataset(
95
		COEFF_PATH, decode_times=False, engine="h5netcdf"
96
	)
97
	coeff_sel = coeff_ds.sel(altitude=alt)
98
	if interpolate:
99
		_ds_m = coeff_sel.assign_coords(mlt=coeff_sel.mlt - 24)
100
		_ds_p = coeff_sel.assign_coords(mlt=coeff_sel.mlt + 24)
101
		_ds_mp = xr.concat([_ds_m, coeff_sel, _ds_p], dim="mlt")
102
		# square the standard deviation for interpolation
103
		_ds_mp["beta_var"] = _ds_mp["beta_std"]**2
104
		coeff_sel = _interp(
105
			_ds_mp,
106
			latitude=gmlat, mlt=mlt,
107
			method=method,
108
		)
109
		# and square root back to get the standard deviation
110
		coeff_sel["beta_std"] = np.sqrt(coeff_sel["beta_var"])
111
	else:
112
		coeff_sel = coeff_sel.sel(latitude=gmlat, mlt=mlt, method="nearest")
113
114
	# Determine if `xarray` read bytes or strings to
115
	# match the correct name in the proxy names.
116
	# Default is plain strings.
117
	offset = "offset"
118
	if isinstance(coeff_sel.proxy.values[0], bytes):
119
		offset = offset.encode()
120
	have_offset = offset in coeff_sel.proxy.values
121
122
	# prepare the coefficients (array) as a `xarray.DataArray`
123
	if isinstance(sw_coeffs, xr.DataArray):
124
		if have_offset:
125
			ones = xr.ones_like(sw_coeffs.isel(proxy=0))
126
			ones = ones.assign_coords(proxy="offset")
127
			sw_coeffs = xr.concat([sw_coeffs, ones], dim="proxy")
128
		sw_coeffs = sw_coeffs.sel(proxy=coeff_sel.proxy.astype(sw_coeffs.proxy.dtype))
129
	else:
130
		sw_coeffs = np.atleast_2d(sw_coeffs)
131
		if have_offset:
132
			aix = sw_coeffs.shape.index(len(coeff_sel.proxy.values) - 1)
133
			if aix != 0:
134
				warn(
135
					"Automatically changing axis. "
136
					"This is ambiguous, to remove the ambiguity, "
137
					"make sure that the different indexes (proxies) "
138
					"are ordered along the zero-th axis in multi-"
139
					"dimensional settings. I.e. each row corresponds "
140
					"to a different index, Kp, PC, Ap, etc."
141
				)
142
				sw_coeffs = sw_coeffs.swapaxes(aix, 0)
143
			sw_coeffs = np.vstack([sw_coeffs, np.ones(sw_coeffs.shape[1])])
144
		else:
145
			aix = sw_coeffs.shape.index(len(coeff_sel.proxy.values))
146
			if aix != 0:
147
				warn(
148
					"Automatically changing axis. "
149
					"This is ambiguous, to remove the ambiguity, "
150
					"make sure that the different indexes (proxies) "
151
					"are ordered along the zero-th axis in multi-"
152
					"dimensional settings. I.e. each row corresponds "
153
					"to a different index, Kp, PC, Ap, etc."
154
				)
155
				sw_coeffs = sw_coeffs.swapaxes(aix, 0)
156
		extra_dims = ["dim_{0}".format(_d) for _d in range(sw_coeffs.ndim - 1)]
157
		sw_coeffs = xr.DataArray(
158
			sw_coeffs,
159
			dims=["proxy"] + extra_dims,
160
			coords={"proxy": coeff_sel.proxy.values},
161
		)
162
163
	# Calculate model (mean) values from `beta`
164
	# fill NaNs with zero for `.dot()`
165
	coeffs = coeff_sel.beta.fillna(0.)
166
	q = coeffs.dot(sw_coeffs)
167
	q = q.rename("log_q")
168
	q.attrs = {
169
		"long_name": "natural logarithm of ionization rate",
170
		"units": "log(cm-3 s-1)",
171
	}
172
	if not return_var:
173
		return q
174
175
	# Calculate variance of the model from `beta_std`
176
	# fill NaNs with zero for `.dot()`
177
	coeffv = coeff_sel.beta_std.fillna(0.)**2
178
	q_var = coeffv.dot(sw_coeffs**2)
179
	if "sigma2" in coeff_sel.data_vars:
180
		# if available, add the posterior variance
181
		# to get the full posterior predictive variance
182
		q_var = coeff_sel["sigma2"] + q_var
183
	q_var = q_var.rename("var_log_q")
184
	q_var.attrs = {
185
		"long_name": "variance of the natural logarithm of ionization rate",
186
		"units": "1",
187
	}
188
	return q, q_var
189