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/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
@@ -0,0 +1,5 @@
1
+ # SPDX-FileCopyrightText: 2024 Shell Global Solutions International B.V. All Rights Reserved.
2
+ #
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ """Plotting Module."""
5
+ __all__ = ["plot"]