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/model.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
# -*- coding: utf-8 -*-
|
|
6
|
+
"""ELQModel module.
|
|
7
|
+
|
|
8
|
+
This module provides a class definition for the main functionalities of the codebase, providing the interface with the
|
|
9
|
+
openMCMC repo and defining some plotting wrappers.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
import warnings
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Union
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from openmcmc import parameter
|
|
18
|
+
from openmcmc.distribution import location_scale
|
|
19
|
+
from openmcmc.mcmc import MCMC
|
|
20
|
+
from openmcmc.model import Model
|
|
21
|
+
|
|
22
|
+
from pyelq.component.background import Background, SpatioTemporalBackground
|
|
23
|
+
from pyelq.component.error_model import BySensor, ErrorModel
|
|
24
|
+
from pyelq.component.offset import PerSensor
|
|
25
|
+
from pyelq.component.source_model import Normal, SourceModel
|
|
26
|
+
from pyelq.gas_species import GasSpecies
|
|
27
|
+
from pyelq.meteorology import Meteorology, MeteorologyGroup
|
|
28
|
+
from pyelq.plotting.plot import Plot
|
|
29
|
+
from pyelq.sensor.sensor import SensorGroup
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ELQModel:
|
|
34
|
+
"""Class for setting up, running, and post-processing the full ELQModel analysis.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
form (dict): dictionary detailing the form of the predictor for the concentration data. For details of the
|
|
38
|
+
required specification, see parameter.LinearCombinationWithTransform() in the openMCMC repo.
|
|
39
|
+
transform (dict): dictionary detailing transformations applied to the model components. For details of the
|
|
40
|
+
required specification, see parameter.LinearCombinationWithTransform() in the openMCMC repo.
|
|
41
|
+
model (Model): full model specification for the analysis, constructed in self.to_mcmc().
|
|
42
|
+
mcmc (MCMC): MCMC object containing model and sampler specification for the problem. Constructed from the
|
|
43
|
+
other components in self.to_mcmc().
|
|
44
|
+
n_iter (int): number of MCMC iterations to be run.
|
|
45
|
+
n_thin (int): number of iterations to thin by.
|
|
46
|
+
fitted_values (np.ndarray): samples of fitted values (i.e. model predictions for the data) generated during the
|
|
47
|
+
MCMC sampler. Attached in self.from_mcmc().
|
|
48
|
+
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
form: dict = field(init=False)
|
|
52
|
+
transform: dict = field(init=False)
|
|
53
|
+
model: Model = field(init=False)
|
|
54
|
+
mcmc: MCMC = field(init=False)
|
|
55
|
+
n_iter: int = 1000
|
|
56
|
+
n_thin: int = 1
|
|
57
|
+
fitted_values: np.ndarray = field(init=False)
|
|
58
|
+
|
|
59
|
+
def __init__(
|
|
60
|
+
self,
|
|
61
|
+
sensor_object: SensorGroup,
|
|
62
|
+
meteorology: Union[Meteorology, MeteorologyGroup],
|
|
63
|
+
gas_species: GasSpecies,
|
|
64
|
+
background: Background = SpatioTemporalBackground(),
|
|
65
|
+
source_model: SourceModel = Normal(),
|
|
66
|
+
error_model: ErrorModel = BySensor(),
|
|
67
|
+
offset_model: PerSensor = None,
|
|
68
|
+
):
|
|
69
|
+
"""Initialise the ELQModel model.
|
|
70
|
+
|
|
71
|
+
Model form is as follows:
|
|
72
|
+
y = A*s + b + d + e
|
|
73
|
+
where:
|
|
74
|
+
- y is the vector of observed concentration data (extracted from the sensor object).
|
|
75
|
+
- A*s is the source contribution (from the source model and dispersion model).
|
|
76
|
+
- b is from the background model.
|
|
77
|
+
- d is from the offset model.
|
|
78
|
+
- e is residual error term and var(e) comes from the error precision model.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
sensor_object (SensorGroup): sensor data.
|
|
82
|
+
meteorology (Union[Meteorology, MeteorologyGroup]): meteorology data.
|
|
83
|
+
gas_species (GasSpecies): gas species object.
|
|
84
|
+
background (Background): background model specification. Defaults to SpatioTemporalBackground().
|
|
85
|
+
source_model (SourceModel): source model specification. Defaults to Normal().
|
|
86
|
+
error_model (Precision): measurement precision model specification. Defaults to BySensor().
|
|
87
|
+
offset_model (PerSensor): offset model specification. Defaults to None.
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
self.sensor_object = sensor_object
|
|
91
|
+
self.meteorology = meteorology
|
|
92
|
+
self.gas_species = gas_species
|
|
93
|
+
self.components = {
|
|
94
|
+
"background": background,
|
|
95
|
+
"source": source_model,
|
|
96
|
+
"error_model": error_model,
|
|
97
|
+
"offset": offset_model,
|
|
98
|
+
}
|
|
99
|
+
if error_model is None:
|
|
100
|
+
self.components["error_model"] = BySensor()
|
|
101
|
+
warnings.warn("None is not an allowed type for error_model: resetting to default BySensor model.")
|
|
102
|
+
for key in list(self.components.keys()):
|
|
103
|
+
if self.components[key] is None:
|
|
104
|
+
self.components.pop(key)
|
|
105
|
+
|
|
106
|
+
def initialise(self):
|
|
107
|
+
"""Take data inputs and extract relevant properties."""
|
|
108
|
+
self.form = {}
|
|
109
|
+
self.transform = {}
|
|
110
|
+
component_keys = list(self.components.keys())
|
|
111
|
+
if "background" in component_keys:
|
|
112
|
+
self.form["bg"] = "B_bg"
|
|
113
|
+
self.transform["bg"] = False
|
|
114
|
+
if "source" in component_keys:
|
|
115
|
+
self.transform["s"] = False
|
|
116
|
+
self.form["s"] = "A"
|
|
117
|
+
if "offset" in component_keys:
|
|
118
|
+
self.form["d"] = "B_d"
|
|
119
|
+
self.transform["d"] = False
|
|
120
|
+
for key in component_keys:
|
|
121
|
+
self.components[key].initialise(self.sensor_object, self.meteorology, self.gas_species)
|
|
122
|
+
|
|
123
|
+
def to_mcmc(self):
|
|
124
|
+
"""Convert the ELQModel specification into an MCMC solver object that can be run.
|
|
125
|
+
|
|
126
|
+
Executing the following steps:
|
|
127
|
+
- Initialise the model object with the data likelihood (response distribution for y), and add all the
|
|
128
|
+
associated prior distributions, as specified by the model components.
|
|
129
|
+
- Initialise the state dictionary with the observed sensor data, and add parameters associated with all
|
|
130
|
+
the associated prior distributions, as specified by the model components.
|
|
131
|
+
- Initialise the MCMC sampler objects associated with each of the model components.
|
|
132
|
+
- Create the MCMC solver object, using all of the above information.
|
|
133
|
+
|
|
134
|
+
"""
|
|
135
|
+
response_precision = self.components["error_model"].precision_parameter
|
|
136
|
+
model = [
|
|
137
|
+
location_scale.Normal(
|
|
138
|
+
"y",
|
|
139
|
+
mean=parameter.LinearCombinationWithTransform(self.form, self.transform),
|
|
140
|
+
precision=response_precision,
|
|
141
|
+
)
|
|
142
|
+
]
|
|
143
|
+
|
|
144
|
+
initial_state = {"y": self.sensor_object.concentration}
|
|
145
|
+
|
|
146
|
+
for component in self.components.values():
|
|
147
|
+
model = component.make_model(model)
|
|
148
|
+
initial_state = component.make_state(initial_state)
|
|
149
|
+
|
|
150
|
+
self.model = Model(model, response={"y": "mean"})
|
|
151
|
+
|
|
152
|
+
sampler_list = []
|
|
153
|
+
for component in self.components.values():
|
|
154
|
+
sampler_list = component.make_sampler(self.model, sampler_list)
|
|
155
|
+
|
|
156
|
+
self.mcmc = MCMC(initial_state, sampler_list, self.model, n_burn=0, n_iter=self.n_iter, n_thin=self.n_thin)
|
|
157
|
+
|
|
158
|
+
def run_mcmc(self):
|
|
159
|
+
"""Run the mcmc function."""
|
|
160
|
+
self.mcmc.run_mcmc()
|
|
161
|
+
|
|
162
|
+
def from_mcmc(self):
|
|
163
|
+
"""Extract information from MCMC solver class once its has run.
|
|
164
|
+
|
|
165
|
+
Performs two operations:
|
|
166
|
+
- For each of the components of the model: extracts the related sampled parameter values and attaches these
|
|
167
|
+
to the component class.
|
|
168
|
+
- For all keys in the mcmc.store dictionary: extracts the sampled parameter values from self.mcmc.store and
|
|
169
|
+
puts them into the equivalent fields in the state
|
|
170
|
+
|
|
171
|
+
"""
|
|
172
|
+
state = self.mcmc.state
|
|
173
|
+
for component in self.components.values():
|
|
174
|
+
component.from_mcmc(self.mcmc.store)
|
|
175
|
+
for key in self.mcmc.store:
|
|
176
|
+
state[key] = self.mcmc.store[key]
|
|
177
|
+
|
|
178
|
+
def plot_log_posterior(self, burn_in_value: int, plot: Plot = Plot()) -> Plot():
|
|
179
|
+
"""Plots the trace of the log posterior over the iterations of the MCMC.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
burn_in_value (int): Burn in value to show in plot.
|
|
183
|
+
plot (Plot, optional): Plot object to which this figure will be added in the figure dictionary
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
plot (Plot): Plot object to which this figure is added in the figure dictionary with
|
|
187
|
+
key 'log_posterior_plot'
|
|
188
|
+
|
|
189
|
+
"""
|
|
190
|
+
plot.plot_single_trace(object_to_plot=self.mcmc, burn_in=burn_in_value)
|
|
191
|
+
return plot
|
|
192
|
+
|
|
193
|
+
def plot_fitted_values(self, plot: Plot = Plot()) -> Plot:
|
|
194
|
+
"""Plot the fitted values from the mcmc object against time, also shows the estimated background when possible.
|
|
195
|
+
|
|
196
|
+
Based on the inputs it plots the results of the mcmc analysis, being the fitted values of the concentration
|
|
197
|
+
measurements together with the 10th and 90th quantile lines to show the goodness of fit of the estimates.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
plot (Plot, optional): Plot object to which this figure will be added in the figure dictionary
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
plot (Plot): Plot object to which this figure is added in the figure dictionary with key 'fitted_values'
|
|
204
|
+
|
|
205
|
+
"""
|
|
206
|
+
plot.plot_fitted_values_per_sensor(
|
|
207
|
+
mcmc_object=self.mcmc, sensor_object=self.sensor_object, background_model=self.components["background"]
|
|
208
|
+
)
|
|
209
|
+
return plot
|