pyelq 1.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyelq/__init__.py +19 -0
- pyelq/component/__init__.py +6 -0
- pyelq/component/background.py +391 -0
- pyelq/component/component.py +79 -0
- pyelq/component/error_model.py +327 -0
- pyelq/component/offset.py +183 -0
- pyelq/component/source_model.py +875 -0
- pyelq/coordinate_system.py +598 -0
- pyelq/data_access/__init__.py +5 -0
- pyelq/data_access/data_access.py +104 -0
- pyelq/dispersion_model/__init__.py +5 -0
- pyelq/dispersion_model/gaussian_plume.py +625 -0
- pyelq/dlm.py +497 -0
- pyelq/gas_species.py +232 -0
- pyelq/meteorology.py +387 -0
- pyelq/model.py +209 -0
- pyelq/plotting/__init__.py +5 -0
- pyelq/plotting/plot.py +1183 -0
- pyelq/preprocessing.py +262 -0
- pyelq/sensor/__init__.py +5 -0
- pyelq/sensor/beam.py +55 -0
- pyelq/sensor/satellite.py +59 -0
- pyelq/sensor/sensor.py +241 -0
- pyelq/source_map.py +115 -0
- pyelq/support_functions/__init__.py +5 -0
- pyelq/support_functions/post_processing.py +377 -0
- pyelq/support_functions/spatio_temporal_interpolation.py +229 -0
- pyelq-1.1.0.dist-info/LICENSE.md +202 -0
- pyelq-1.1.0.dist-info/LICENSES/Apache-2.0.txt +73 -0
- pyelq-1.1.0.dist-info/METADATA +127 -0
- pyelq-1.1.0.dist-info/RECORD +32 -0
- pyelq-1.1.0.dist-info/WHEEL +4 -0
pyelq/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
"""Main pyELQ module."""
|
|
5
|
+
__all__ = [
|
|
6
|
+
"component",
|
|
7
|
+
"data_access",
|
|
8
|
+
"dispersion_model",
|
|
9
|
+
"plotting",
|
|
10
|
+
"sensor",
|
|
11
|
+
"support_functions",
|
|
12
|
+
"coordinate_system",
|
|
13
|
+
"dlm",
|
|
14
|
+
"gas_species",
|
|
15
|
+
"meteorology",
|
|
16
|
+
"model",
|
|
17
|
+
"preprocessing",
|
|
18
|
+
"source_map",
|
|
19
|
+
]
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Model components for background modelling."""
|
|
6
|
+
|
|
7
|
+
from abc import abstractmethod
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from openmcmc import gmrf, parameter
|
|
15
|
+
from openmcmc.distribution.distribution import Gamma
|
|
16
|
+
from openmcmc.distribution.location_scale import Normal
|
|
17
|
+
from openmcmc.model import Model
|
|
18
|
+
from openmcmc.sampler.sampler import NormalGamma, NormalNormal
|
|
19
|
+
from scipy import sparse
|
|
20
|
+
from sklearn.neighbors import NearestNeighbors
|
|
21
|
+
|
|
22
|
+
from pyelq.component.component import Component
|
|
23
|
+
from pyelq.coordinate_system import Coordinate
|
|
24
|
+
from pyelq.gas_species import GasSpecies
|
|
25
|
+
from pyelq.meteorology import MeteorologyGroup
|
|
26
|
+
from pyelq.sensor.beam import Beam
|
|
27
|
+
from pyelq.sensor.sensor import SensorGroup
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass
|
|
31
|
+
class Background(Component):
|
|
32
|
+
"""Superclass for background models.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
n_obs (int): total number of observations in the background model (across all sensors).
|
|
36
|
+
n_parameter (int): number of parameters in the background model
|
|
37
|
+
bg (np.ndarray): array of sampled background values, populated in self.from_mcmc() after the MCMC run is
|
|
38
|
+
completed.
|
|
39
|
+
precision_scalar (np.ndarray): array of sampled background precision values, populated in self.from_mcmc() after
|
|
40
|
+
the MCMC run is completed. Only populated if update_precision is True.
|
|
41
|
+
precision_matrix (Union[np.ndarray, sparse.csr_array]): un-scaled precision matrix for the background parameter
|
|
42
|
+
vector.
|
|
43
|
+
mean_bg (float): global mean background value. Should be populated from the value specified in the GasSpecies
|
|
44
|
+
object.
|
|
45
|
+
update_precision (bool): logical determining whether the background (scalar) precision parameter should be
|
|
46
|
+
updated as part of the MCMC. Defaults to False.
|
|
47
|
+
prior_precision_shape (float): shape parameter for the prior gamma distribution for the scalar precision
|
|
48
|
+
parameter(s).
|
|
49
|
+
prior_precision_rate (float): rate parameter for the prior gamma distribution for the scalar precision
|
|
50
|
+
parameter(s).
|
|
51
|
+
initial_precision (float): initial value for the scalar precision parameter.
|
|
52
|
+
basis_matrix (sparse.csr_array): [n_obs x n_time] matrix mapping the background model parameters on to the
|
|
53
|
+
observations.
|
|
54
|
+
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
n_obs: int = field(init=False)
|
|
58
|
+
n_parameter: int = field(init=False)
|
|
59
|
+
bg: np.ndarray = field(init=False)
|
|
60
|
+
precision_scalar: np.ndarray = field(init=False)
|
|
61
|
+
precision_matrix: Union[np.ndarray, sparse.csc_matrix] = field(init=False)
|
|
62
|
+
mean_bg: Union[float, None] = None
|
|
63
|
+
update_precision: bool = False
|
|
64
|
+
prior_precision_shape: float = 1e-3
|
|
65
|
+
prior_precision_rate: float = 1e-3
|
|
66
|
+
initial_precision: float = 1.0
|
|
67
|
+
basis_matrix: sparse.csr_array = field(init=False)
|
|
68
|
+
|
|
69
|
+
@abstractmethod
|
|
70
|
+
def initialise(self, sensor_object: SensorGroup, meteorology: MeteorologyGroup, gas_species: GasSpecies):
|
|
71
|
+
"""Take data inputs and extract relevant properties.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
sensor_object (SensorGroup): sensor data
|
|
75
|
+
meteorology (MeteorologyGroup): meteorology data
|
|
76
|
+
gas_species (GasSpecies): gas species information
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def make_model(self, model: list = None) -> list:
|
|
81
|
+
"""Take model list and append new elements from current model component.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
model (list, optional): Current list of model elements. Defaults to None.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
list: model output list.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
bg_precision_predictor = parameter.ScaledMatrix(matrix="P_bg", scalar="lambda_bg")
|
|
91
|
+
model.append(Normal("bg", mean="mu_bg", precision=bg_precision_predictor))
|
|
92
|
+
if self.update_precision:
|
|
93
|
+
model.append(Gamma("lambda_bg", shape="a_lam_bg", rate="b_lam_bg"))
|
|
94
|
+
return model
|
|
95
|
+
|
|
96
|
+
def make_sampler(self, model: Model, sampler_list: list = None) -> list:
|
|
97
|
+
"""Take sampler list and append new elements from current model component.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
model (Model): Full model list of distributions.
|
|
101
|
+
sampler_list (list, optional): Current list of samplers. Defaults to None.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
list: sampler output list.
|
|
105
|
+
|
|
106
|
+
"""
|
|
107
|
+
if sampler_list is None:
|
|
108
|
+
sampler_list = []
|
|
109
|
+
sampler_list.append(NormalNormal("bg", model))
|
|
110
|
+
if self.update_precision:
|
|
111
|
+
sampler_list.append(NormalGamma("lambda_bg", model))
|
|
112
|
+
return sampler_list
|
|
113
|
+
|
|
114
|
+
def make_state(self, state: dict = None) -> dict:
|
|
115
|
+
"""Take state dictionary and append initial values from model component.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
state (dict, optional): current state vector. Defaults to None.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
dict: current state vector with components added.
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
state["mu_bg"] = np.ones((self.n_parameter, 1)) * self.mean_bg
|
|
125
|
+
state["B_bg"] = self.basis_matrix
|
|
126
|
+
state["bg"] = np.ones((self.n_parameter, 1)) * self.mean_bg
|
|
127
|
+
state["P_bg"] = self.precision_matrix
|
|
128
|
+
state["lambda_bg"] = self.initial_precision
|
|
129
|
+
if self.update_precision:
|
|
130
|
+
state["a_lam_bg"] = self.prior_precision_shape
|
|
131
|
+
state["b_lam_bg"] = self.prior_precision_rate
|
|
132
|
+
return state
|
|
133
|
+
|
|
134
|
+
def from_mcmc(self, store: dict):
|
|
135
|
+
"""Extract results of mcmc from mcmc.store and attach to components.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
store (dict): mcmc result dictionary.
|
|
139
|
+
|
|
140
|
+
"""
|
|
141
|
+
self.bg = store["bg"]
|
|
142
|
+
if self.update_precision:
|
|
143
|
+
self.precision_scalar = store["lambda_bg"]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@dataclass
|
|
147
|
+
class TemporalBackground(Background):
|
|
148
|
+
"""Model which imposes only temporal correlation on the background parameters.
|
|
149
|
+
|
|
150
|
+
Assumes that the prior mean concentration of the background at every location/time point is the global average
|
|
151
|
+
background concentration as defined in the input GasSpecies object.
|
|
152
|
+
|
|
153
|
+
Generates the (un-scaled) prior background precision matrix using the function gmrf.precision_temporal: this
|
|
154
|
+
precision matrix imposes first-oder Markov structure for the temporal dependence.
|
|
155
|
+
|
|
156
|
+
By default, the times used for the model definition are the set of unique times in the observation set.
|
|
157
|
+
|
|
158
|
+
This background model only requires the initialise function, and does not require the implementation of any further
|
|
159
|
+
methods.
|
|
160
|
+
|
|
161
|
+
Attributes:
|
|
162
|
+
time (Union[np.ndarray, pd.arrays.DatetimeArray]): vector of times used in defining the model.
|
|
163
|
+
|
|
164
|
+
"""
|
|
165
|
+
|
|
166
|
+
time: Union[np.ndarray, pd.arrays.DatetimeArray] = field(init=False)
|
|
167
|
+
|
|
168
|
+
def initialise(self, sensor_object: SensorGroup, meteorology: MeteorologyGroup, gas_species: GasSpecies):
|
|
169
|
+
"""Create temporal background model from sensor, meteorology and gas species inputs.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
sensor_object (SensorGroup): sensor data object.
|
|
173
|
+
meteorology (MeteorologyGroup): meteorology data object.
|
|
174
|
+
gas_species (GasSpecies): gas species data object.
|
|
175
|
+
|
|
176
|
+
"""
|
|
177
|
+
self.n_obs = sensor_object.nof_observations
|
|
178
|
+
self.time, unique_inverse = np.unique(sensor_object.time, return_inverse=True)
|
|
179
|
+
self.time = pd.array(self.time, dtype="datetime64[ns]")
|
|
180
|
+
self.n_parameter = len(self.time)
|
|
181
|
+
self.basis_matrix = sparse.csr_array((np.ones(self.n_obs), (np.array(range(self.n_obs)), unique_inverse)))
|
|
182
|
+
self.precision_matrix = gmrf.precision_temporal(time=self.time)
|
|
183
|
+
if self.mean_bg is None:
|
|
184
|
+
self.mean_bg = gas_species.global_background
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
@dataclass
|
|
188
|
+
class SpatioTemporalBackground(Background):
|
|
189
|
+
"""Model which imposes both spatial and temporal correlation on the background parameters.
|
|
190
|
+
|
|
191
|
+
Defines a grid in time, and assumes a correlated time-series per sensor using the defined time grid.
|
|
192
|
+
|
|
193
|
+
The background parameter is an [n_location * n_time x 1] (if self.spatial_dependence is True) or an [n_time x 1]
|
|
194
|
+
vector (if self.spatial_dependence is False). In the spatio-temporal case, the background vector is assumed to
|
|
195
|
+
unwrap over space and time as follows:
|
|
196
|
+
bg = [b_1(t_1), b_2(t_1),..., b_nlct(t_1),...,b_1(t_k),..., b_nlct(t_k),...].T
|
|
197
|
+
where nlct is the number of sensor locations.
|
|
198
|
+
This unwrapping mechanism is chosen as it greatly speeds up the sparse matrix operations in the solver (vs. the
|
|
199
|
+
alternative).
|
|
200
|
+
|
|
201
|
+
self.basis_matrix is set up to map the elements of the full background vector onto the observations, on the basis
|
|
202
|
+
of spatial location and nearest time knot.
|
|
203
|
+
|
|
204
|
+
The temporal background correlation is computed using gmrf.precision_temporal, and the spatial correlation is
|
|
205
|
+
computed using a squared exponential correlation function, parametrized by self.spatial_correlation_param (spatial
|
|
206
|
+
correlation, measured in metres). The full precision matrix is simply a Kronecker product between the two
|
|
207
|
+
component precision matrices.
|
|
208
|
+
|
|
209
|
+
Attributes:
|
|
210
|
+
n_time (int): number of time knots for which the model is defined. Note that this does not need to be the same
|
|
211
|
+
as the number of concentration observations in the analysis.
|
|
212
|
+
n_location (int): number of spatial knots in the model.
|
|
213
|
+
time (pd.arrays.DatetimeArray): vector of times used in defining the model.
|
|
214
|
+
spatial_dependence (bool): flag indicating whether the background parameters should be spatially correlated. If
|
|
215
|
+
True, the model assumes a separate background time-series per sensor location, and assumes these
|
|
216
|
+
time-series to be spatially correlated. If False (default), the background parameters are assumed to be
|
|
217
|
+
common between sensors (only temporally correlated).
|
|
218
|
+
spatial_correlation_param (float): correlation length parameter, determining the degree of spatial correlation
|
|
219
|
+
imposed on the background time-series. Units are metres. Assumes equal correlation in all spatial
|
|
220
|
+
directions. Defaults to 1.0.
|
|
221
|
+
location (np.ndarray): [n_location x 3] array of sensor locations, used for calculating the spatial correlation
|
|
222
|
+
between the sensor background values. If self.spatial_dependence is False, this attribute is simply set to
|
|
223
|
+
be the location of the first sensor in the sensor object.
|
|
224
|
+
temporal_precision_matrix (Union[np.ndarray, sparse.csc_matrix]): temporal component of the precision matrix.
|
|
225
|
+
The full model precision matrix is the Kronecker product of this matrix with self.spatial_precision_matrix.
|
|
226
|
+
spatial_precision_matrix (np.ndarray): spatial component of the precision matrix. The full model precision
|
|
227
|
+
matrix is the Kronecker product of this matrix with the self.temporal_precision_matrix. Simply set to 1 if
|
|
228
|
+
self.spatial_dependence is False.
|
|
229
|
+
precision_time_0 (float): precision relating to the first time stamp in the model. Defaults to 0.01.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
n_time: Union[int, None] = None
|
|
234
|
+
n_location: int = field(init=False)
|
|
235
|
+
time: pd.arrays.DatetimeArray = field(init=False)
|
|
236
|
+
spatial_dependence: bool = False
|
|
237
|
+
spatial_correlation_param: float = field(init=False, default=1.0)
|
|
238
|
+
location: Coordinate = field(init=False)
|
|
239
|
+
temporal_precision_matrix: Union[np.ndarray, sparse.csc_matrix] = field(init=False)
|
|
240
|
+
spatial_precision_matrix: np.ndarray = field(init=False)
|
|
241
|
+
precision_time_0: float = field(init=False, default=0.01)
|
|
242
|
+
|
|
243
|
+
def initialise(self, sensor_object: SensorGroup, meteorology: MeteorologyGroup, gas_species: GasSpecies):
|
|
244
|
+
"""Take data inputs and extract relevant properties.
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
sensor_object (SensorGroup): sensor data
|
|
248
|
+
meteorology (MeteorologyGroup): meteorology data wind data
|
|
249
|
+
gas_species (GasSpecies): gas species information
|
|
250
|
+
|
|
251
|
+
"""
|
|
252
|
+
self.make_temporal_knots(sensor_object)
|
|
253
|
+
self.make_spatial_knots(sensor_object)
|
|
254
|
+
self.n_parameter = self.n_time * self.n_location
|
|
255
|
+
self.n_obs = sensor_object.nof_observations
|
|
256
|
+
|
|
257
|
+
self.make_precision_matrix()
|
|
258
|
+
self.make_parameter_mapping(sensor_object)
|
|
259
|
+
|
|
260
|
+
if self.mean_bg is None:
|
|
261
|
+
self.mean_bg = gas_species.global_background
|
|
262
|
+
|
|
263
|
+
def make_parameter_mapping(self, sensor_object: SensorGroup):
|
|
264
|
+
"""Create the mapping of parameters onto observations, through creation of the associated basis matrix.
|
|
265
|
+
|
|
266
|
+
The background vector unwraps first over the spatial (sensor) location dimension, then over the temporal
|
|
267
|
+
dimension. For more detail, see the main class docstring.
|
|
268
|
+
|
|
269
|
+
The data vector in the solver state is assumed to consist of the individual sensor data vectors stacked
|
|
270
|
+
consecutively.
|
|
271
|
+
|
|
272
|
+
Args:
|
|
273
|
+
sensor_object (SensorGroup): group of sensor objects.
|
|
274
|
+
|
|
275
|
+
"""
|
|
276
|
+
nn_object = NearestNeighbors(n_neighbors=1, algorithm="kd_tree").fit(self.time.to_numpy().reshape(-1, 1))
|
|
277
|
+
for k, sensor in enumerate(sensor_object.values()):
|
|
278
|
+
_, time_index = nn_object.kneighbors(sensor.time.to_numpy().reshape(-1, 1))
|
|
279
|
+
basis_matrix = sparse.csr_array(
|
|
280
|
+
(np.ones(sensor.nof_observations), (np.array(range(sensor.nof_observations)), time_index.flatten())),
|
|
281
|
+
shape=(sensor.nof_observations, self.n_time),
|
|
282
|
+
)
|
|
283
|
+
if self.spatial_dependence:
|
|
284
|
+
basis_matrix = sparse.kron(basis_matrix, np.eye(N=self.n_location, M=1, k=-k).T)
|
|
285
|
+
|
|
286
|
+
if k == 0:
|
|
287
|
+
self.basis_matrix = basis_matrix
|
|
288
|
+
else:
|
|
289
|
+
self.basis_matrix = sparse.vstack([self.basis_matrix, basis_matrix])
|
|
290
|
+
|
|
291
|
+
def make_temporal_knots(self, sensor_object: SensorGroup):
|
|
292
|
+
"""Create the temporal grid for the model.
|
|
293
|
+
|
|
294
|
+
If self.n_time is not specified, then the model will use the unique set of times from the sensor data.
|
|
295
|
+
|
|
296
|
+
If self.n_time is specified, then the model will define a time grid with self.n_time elements.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
sensor_object (SensorGroup): group of sensor objects.
|
|
300
|
+
|
|
301
|
+
"""
|
|
302
|
+
if self.n_time is None:
|
|
303
|
+
self.time = pd.array(np.unique(sensor_object.time), dtype="datetime64[ns]")
|
|
304
|
+
self.n_time = len(self.time)
|
|
305
|
+
else:
|
|
306
|
+
self.time = pd.array(
|
|
307
|
+
pd.date_range(start=np.min(sensor_object.time), end=np.max(sensor_object.time), periods=self.n_time),
|
|
308
|
+
dtype="datetime64[ns]",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
def make_spatial_knots(self, sensor_object: SensorGroup):
|
|
312
|
+
"""Create the spatial grid for the model.
|
|
313
|
+
|
|
314
|
+
If self.spatial_dependence is False, the code assumes that only a single (arbitrary) location is used, thereby
|
|
315
|
+
eliminating any spatial dependence.
|
|
316
|
+
|
|
317
|
+
If self.spatial_dependence is True, a separate but correlated time-series of background parameters is assumed
|
|
318
|
+
for each sensor location.
|
|
319
|
+
|
|
320
|
+
Args:
|
|
321
|
+
sensor_object (SensorGroup): group of sensor objects.
|
|
322
|
+
|
|
323
|
+
"""
|
|
324
|
+
if self.spatial_dependence:
|
|
325
|
+
self.n_location = sensor_object.nof_sensors
|
|
326
|
+
self.get_locations_from_sensors(sensor_object)
|
|
327
|
+
else:
|
|
328
|
+
self.n_location = 1
|
|
329
|
+
self.location = sensor_object[list(sensor_object.keys())[0]].location
|
|
330
|
+
|
|
331
|
+
def make_precision_matrix(self):
|
|
332
|
+
"""Create the full precision matrix for the background parameters.
|
|
333
|
+
|
|
334
|
+
Defined as the Kronecker product of the temporal precision matrix and the spatial precision matrix.
|
|
335
|
+
|
|
336
|
+
"""
|
|
337
|
+
self.temporal_precision_matrix = gmrf.precision_temporal(time=self.time)
|
|
338
|
+
lam = self.temporal_precision_matrix[0, 0]
|
|
339
|
+
self.temporal_precision_matrix[0, 0] = lam * (2.0 - lam / (self.precision_time_0 + lam))
|
|
340
|
+
|
|
341
|
+
if self.spatial_dependence:
|
|
342
|
+
self.make_spatial_precision_matrix()
|
|
343
|
+
self.precision_matrix = sparse.kron(self.temporal_precision_matrix, self.spatial_precision_matrix)
|
|
344
|
+
else:
|
|
345
|
+
self.precision_matrix = self.temporal_precision_matrix
|
|
346
|
+
if (self.n_parameter == 1) and sparse.issparse(self.precision_matrix):
|
|
347
|
+
self.precision_matrix = self.precision_matrix.toarray()
|
|
348
|
+
|
|
349
|
+
def make_spatial_precision_matrix(self):
|
|
350
|
+
"""Create the spatial precision matrix for the model.
|
|
351
|
+
|
|
352
|
+
The spatial precision matrix is simply calculated as the inverse of a squared exponential covariance matrix
|
|
353
|
+
calculated using the sensor locations.
|
|
354
|
+
|
|
355
|
+
"""
|
|
356
|
+
location_array = self.location.to_array()
|
|
357
|
+
spatial_covariance_matrix = np.exp(
|
|
358
|
+
-(1 / (2 * np.power(self.spatial_correlation_param, 2)))
|
|
359
|
+
* (
|
|
360
|
+
np.power(location_array[:, [0]] - location_array[:, [0]].T, 2)
|
|
361
|
+
+ np.power(location_array[:, [1]] - location_array[:, [1]].T, 2)
|
|
362
|
+
+ np.power(location_array[:, [2]] - location_array[:, [2]].T, 2)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
365
|
+
self.spatial_precision_matrix = np.linalg.inv(
|
|
366
|
+
spatial_covariance_matrix + (1e-6) * np.eye(spatial_covariance_matrix.shape[0])
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
def get_locations_from_sensors(self, sensor_object: SensorGroup):
|
|
370
|
+
"""Extract the location information from the sensor object.
|
|
371
|
+
|
|
372
|
+
Attaches a Coordinate.ENU object as the self.location attribute, with all the sensor locations stored on the
|
|
373
|
+
same object.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
sensor_object (SensorGroup): group of sensor objects.
|
|
377
|
+
|
|
378
|
+
"""
|
|
379
|
+
self.location = deepcopy(sensor_object[list(sensor_object.keys())[0]].location.to_enu())
|
|
380
|
+
self.location.east = np.full(shape=(self.n_location,), fill_value=np.nan)
|
|
381
|
+
self.location.north = np.full(shape=(self.n_location,), fill_value=np.nan)
|
|
382
|
+
self.location.up = np.full(shape=(self.n_location,), fill_value=np.nan)
|
|
383
|
+
for k, sensor in enumerate(sensor_object.values()):
|
|
384
|
+
if isinstance(sensor, Beam):
|
|
385
|
+
self.location.east[k] = np.mean(sensor.location.to_enu().east, axis=0)
|
|
386
|
+
self.location.north[k] = np.mean(sensor.location.to_enu().north, axis=0)
|
|
387
|
+
self.location.up[k] = np.mean(sensor.location.to_enu().up, axis=0)
|
|
388
|
+
else:
|
|
389
|
+
self.location.east[k] = sensor.location.to_enu().east
|
|
390
|
+
self.location.north[k] = sensor.location.to_enu().north
|
|
391
|
+
self.location.up[k] = sensor.location.to_enu().up
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
"""Superclass for model components."""
|
|
6
|
+
from abc import ABC, abstractmethod
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
|
|
9
|
+
from openmcmc.model import Model
|
|
10
|
+
|
|
11
|
+
from pyelq.gas_species import GasSpecies
|
|
12
|
+
from pyelq.meteorology import MeteorologyGroup
|
|
13
|
+
from pyelq.sensor.sensor import SensorGroup
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Component(ABC):
|
|
18
|
+
"""Abstract class defining methods and rules for model elements.
|
|
19
|
+
|
|
20
|
+
The bulk of attributes will be defined in the subclasses inheriting from this superclass.
|
|
21
|
+
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def initialise(self, sensor_object: SensorGroup, meteorology: MeteorologyGroup, gas_species: GasSpecies):
|
|
26
|
+
"""Take data inputs and extract relevant properties.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
sensor_object (SensorGroup): sensor data
|
|
30
|
+
meteorology (MeteorologyGroup): meteorology data
|
|
31
|
+
gas_species (GasSpecies): gas species information
|
|
32
|
+
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
@abstractmethod
|
|
36
|
+
def make_model(self, model: list) -> list:
|
|
37
|
+
"""Take model list and append new elements from current model component.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
model (list, optional): Current list of model elements. Defaults to [].
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
list: model output list.
|
|
44
|
+
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
def make_sampler(self, model: Model, sampler_list: list) -> list:
|
|
49
|
+
"""Take sampler list and append new elements from current model component.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model (Model): Full model list of distributions.
|
|
53
|
+
sampler_list (list, optional): Current list of samplers. Defaults to [].
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
list: sampler output list.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
@abstractmethod
|
|
61
|
+
def make_state(self, state: dict) -> dict:
|
|
62
|
+
"""Take state dictionary and append initial values from model component.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
state (dict, optional): current state vector. Defaults to {}.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict: current state vector with components added.
|
|
69
|
+
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
@abstractmethod
|
|
73
|
+
def from_mcmc(self, store: dict):
|
|
74
|
+
"""Extract results of mcmc from mcmc.store and attach to components.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
store (dict): mcmc result dictionary.
|
|
78
|
+
|
|
79
|
+
"""
|