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
|
@@ -0,0 +1,875 @@
|
|
|
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
|
+
"""Component Class and subclasses for source model.
|
|
7
|
+
|
|
8
|
+
A SourceModel instance inherits from 3 super-classes:
|
|
9
|
+
- Component: this is the general superclass for ELQModel components, which prototypes generic methods.
|
|
10
|
+
- A type of SourceGrouping: this class type implements an allocation of sources to different categories (e.g. slab
|
|
11
|
+
or spike), and sets up a sampler for estimating the classification of each source within the source map.
|
|
12
|
+
Inheriting from the NullGrouping class ensures that the allocation of all sources is fixed during the inversion,
|
|
13
|
+
and is not updated.
|
|
14
|
+
- A type of SourceDistribution: this class type implements a particular type of response distribution (mostly
|
|
15
|
+
Normal, but also allows for cases where we have e.g. exp(log_s) or a non-Gaussian prior).
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from abc import abstractmethod
|
|
20
|
+
from copy import deepcopy
|
|
21
|
+
from dataclasses import dataclass, field
|
|
22
|
+
from typing import TYPE_CHECKING, Tuple, Union
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
from openmcmc import parameter
|
|
26
|
+
from openmcmc.distribution.distribution import Categorical, Gamma, Poisson, Uniform
|
|
27
|
+
from openmcmc.distribution.location_scale import Normal as mcmcNormal
|
|
28
|
+
from openmcmc.model import Model
|
|
29
|
+
from openmcmc.sampler.metropolis_hastings import RandomWalkLoop
|
|
30
|
+
from openmcmc.sampler.reversible_jump import ReversibleJump
|
|
31
|
+
from openmcmc.sampler.sampler import MixtureAllocation, NormalGamma, NormalNormal
|
|
32
|
+
|
|
33
|
+
from pyelq.component.component import Component
|
|
34
|
+
from pyelq.coordinate_system import ENU
|
|
35
|
+
from pyelq.dispersion_model.gaussian_plume import GaussianPlume
|
|
36
|
+
from pyelq.gas_species import GasSpecies
|
|
37
|
+
from pyelq.meteorology import Meteorology
|
|
38
|
+
from pyelq.sensor.sensor import SensorGroup
|
|
39
|
+
from pyelq.source_map import SourceMap
|
|
40
|
+
|
|
41
|
+
if TYPE_CHECKING:
|
|
42
|
+
from pyelq.plotting.plot import Plot
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class SourceGrouping:
|
|
47
|
+
"""Superclass for source grouping approach.
|
|
48
|
+
|
|
49
|
+
Source grouping method determines the group allocation of each source in the model, e.g: slab and spike
|
|
50
|
+
distribution makes an on/off allocation for each source.
|
|
51
|
+
|
|
52
|
+
Attributes:
|
|
53
|
+
nof_sources (int): number of sources in the model.
|
|
54
|
+
emission_rate_mean (Union[float, np.ndarray]): prior mean parameter for the emission rate distribution.
|
|
55
|
+
_source_key (str): label for the source parameter to be used in the distributions, samplers, MCMC state etc.
|
|
56
|
+
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
nof_sources: int = field(init=False)
|
|
60
|
+
emission_rate_mean: Union[float, np.ndarray] = field(init=False)
|
|
61
|
+
_source_key: str = field(init=False, default="s")
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
def make_allocation_model(self, model: list) -> list:
|
|
65
|
+
"""Initialise the source allocation part of the model, and the parameters of the source response distribution.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
model (list): overall model, consisting of list of distributions.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
list: overall model list, updated with allocation distribution.
|
|
72
|
+
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
@abstractmethod
|
|
76
|
+
def make_allocation_sampler(self, model: Model, sampler_list: list) -> list:
|
|
77
|
+
"""Initialise the allocation part of the sampler.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
model (Model): overall model, consisting of list of distributions.
|
|
81
|
+
sampler_list (list): list of samplers for individual parameters.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
list: sampler_list updated with sampler for the source allocation.
|
|
85
|
+
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
@abstractmethod
|
|
89
|
+
def make_allocation_state(self, state: dict) -> dict:
|
|
90
|
+
"""Initialise the allocation part of the state.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
state (dict): dictionary containing current state information.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
dict: state updated with parameters related to the source grouping.
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
@abstractmethod
|
|
101
|
+
def from_mcmc_group(self, store: dict):
|
|
102
|
+
"""Extract posterior allocation samples from the MCMC sampler, attach them to the class.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
store (dict): dictionary containing samples from the MCMC.
|
|
106
|
+
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@dataclass
|
|
111
|
+
class NullGrouping(SourceGrouping):
|
|
112
|
+
"""Null grouping: the grouping of the sources will not change during the course of the inversion.
|
|
113
|
+
|
|
114
|
+
Note that this is intended to support two distinct cases:
|
|
115
|
+
1) The case where the source map is fixed, and a given prior mean and prior precision value are assigned to
|
|
116
|
+
each source (can be a common value for all sources, or can be a distinct allocation to each element of the
|
|
117
|
+
source map).
|
|
118
|
+
2) The case where the dimensionality of the source map is changing during the inversion, and a common prior
|
|
119
|
+
mean and precision term are used for all sources.
|
|
120
|
+
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
def make_allocation_model(self, model: list) -> list:
|
|
124
|
+
"""Initialise the source allocation part of the model.
|
|
125
|
+
|
|
126
|
+
In the NullGrouping case, the source allocation is fixed throughout, so this function does nothing (simply
|
|
127
|
+
returns the existing model un-modified).
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
model (list): model as constructed so far, consisting of list of distributions.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
list: overall model list, updated with allocation distribution.
|
|
134
|
+
|
|
135
|
+
"""
|
|
136
|
+
return model
|
|
137
|
+
|
|
138
|
+
def make_allocation_sampler(self, model: Model, sampler_list: list) -> list:
|
|
139
|
+
"""Initialise the allocation part of the sampler.
|
|
140
|
+
|
|
141
|
+
In the NullGrouping case, the source allocation is fixed throughout, so this function does nothing (simply
|
|
142
|
+
returns the existing sampler list un-modified).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
model (Model): overall model set for the problem.
|
|
146
|
+
sampler_list (list): list of samplers for individual parameters.
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
list: sampler_list updated with sampler for the source allocation.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
return sampler_list
|
|
153
|
+
|
|
154
|
+
def make_allocation_state(self, state: dict) -> dict:
|
|
155
|
+
"""Initialise the allocation part of the state.
|
|
156
|
+
|
|
157
|
+
The prior mean parameter and the fixed source allocation are added to the state.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
state (dict): dictionary containing current state information.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
dict: state updated with parameters related to the source grouping.
|
|
164
|
+
|
|
165
|
+
"""
|
|
166
|
+
state["mu_s"] = np.array(self.emission_rate_mean, ndmin=1)
|
|
167
|
+
state["alloc_s"] = np.zeros((self.nof_sources, 1), dtype="int")
|
|
168
|
+
return state
|
|
169
|
+
|
|
170
|
+
def from_mcmc_group(self, store: dict):
|
|
171
|
+
"""Extract posterior allocation samples from the MCMC sampler, attach them to the class.
|
|
172
|
+
|
|
173
|
+
We have not implemented anything here as there is nothing to fetch from the MCMC solution here for the
|
|
174
|
+
NullGrouping Class.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
store (dict): dictionary containing samples from the MCMC.
|
|
178
|
+
|
|
179
|
+
"""
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class SlabAndSpike(SourceGrouping):
|
|
184
|
+
"""Slab and spike source model, special case for the source grouping.
|
|
185
|
+
|
|
186
|
+
Slab and spike: the prior for the emission rates is a two-component mixture, and the allocation is to be
|
|
187
|
+
estimated as part of the inversion.
|
|
188
|
+
|
|
189
|
+
Attributes:
|
|
190
|
+
slab_probability (float): prior probability of allocation to the slab component. Defaults to 0.05.
|
|
191
|
+
allocation (np.ndarray): set of allocation samples, with shape=(n_sources, n_iterations). Attached to
|
|
192
|
+
the class by self.from_mcmc_group().
|
|
193
|
+
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
slab_probability: float = 0.05
|
|
197
|
+
allocation: np.ndarray = field(init=False)
|
|
198
|
+
|
|
199
|
+
def make_allocation_model(self, model: list) -> list:
|
|
200
|
+
"""Initialise the source allocation part of the model.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
model (list): model as constructed so far, consisting of list of distributions.
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
list: overall model list, updated with allocation distribution.
|
|
207
|
+
|
|
208
|
+
"""
|
|
209
|
+
model.append(Categorical("alloc_s", prob="s_prob"))
|
|
210
|
+
return model
|
|
211
|
+
|
|
212
|
+
def make_allocation_sampler(self, model: Model, sampler_list: list) -> list:
|
|
213
|
+
"""Initialise the allocation part of the sampler.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
model (Model): overall model set for the problem.
|
|
217
|
+
sampler_list (list): list of samplers for individual parameters.
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
list: sampler_list updated with sampler for the source allocation.
|
|
221
|
+
|
|
222
|
+
"""
|
|
223
|
+
sampler_list.append(MixtureAllocation(param="alloc_s", model=model, response_param=self._source_key))
|
|
224
|
+
return sampler_list
|
|
225
|
+
|
|
226
|
+
def make_allocation_state(self, state: dict) -> dict:
|
|
227
|
+
"""Initialise the allocation part of the state.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
state (dict): dictionary containing current state information.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
dict: state updated with parameters related to the source grouping.
|
|
234
|
+
|
|
235
|
+
"""
|
|
236
|
+
state["mu_s"] = np.array(self.emission_rate_mean, ndmin=1)
|
|
237
|
+
state["s_prob"] = np.tile(np.array([self.slab_probability, 1 - self.slab_probability]), (self.nof_sources, 1))
|
|
238
|
+
state["alloc_s"] = np.ones((self.nof_sources, 1), dtype="int")
|
|
239
|
+
return state
|
|
240
|
+
|
|
241
|
+
def from_mcmc_group(self, store: dict):
|
|
242
|
+
"""Extract posterior allocation samples from the MCMC sampler, attach them to the class.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
store (dict): dictionary containing samples from the MCMC.
|
|
246
|
+
|
|
247
|
+
"""
|
|
248
|
+
self.allocation = store["alloc_s"]
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@dataclass
|
|
252
|
+
class SourceDistribution:
|
|
253
|
+
"""Superclass for source emission rate distribution.
|
|
254
|
+
|
|
255
|
+
Source distribution determines the type of prior to be used for the source emission rates, and the transformation
|
|
256
|
+
linking the source parameters and the data.
|
|
257
|
+
|
|
258
|
+
Elements related to transformation of source parameters are also specified at the model level.
|
|
259
|
+
|
|
260
|
+
Attributes:
|
|
261
|
+
nof_sources (int): number of sources in the model.
|
|
262
|
+
emission_rate (np.ndarray): set of emission rate samples, with shape=(n_sources, n_iterations). Attached to
|
|
263
|
+
the class by self.from_mcmc_dist().
|
|
264
|
+
|
|
265
|
+
"""
|
|
266
|
+
|
|
267
|
+
nof_sources: int = field(init=False)
|
|
268
|
+
emission_rate: np.ndarray = field(init=False)
|
|
269
|
+
|
|
270
|
+
@abstractmethod
|
|
271
|
+
def make_source_model(self, model: list) -> list:
|
|
272
|
+
"""Add distributional component to the overall model corresponding to the source emission rate distribution.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
model (list): model as constructed so far, consisting of list of distributions.
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
list: overall model list, updated with distributions related to source prior.
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
|
|
282
|
+
@abstractmethod
|
|
283
|
+
def make_source_sampler(self, model: Model, sampler_list: list) -> list:
|
|
284
|
+
"""Initialise the source prior distribution part of the sampler.
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
model (Model): overall model set for the problem.
|
|
288
|
+
sampler_list (list): list of samplers for individual parameters.
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
list: sampler_list updated with sampler for the emission rate parameters.
|
|
292
|
+
|
|
293
|
+
"""
|
|
294
|
+
|
|
295
|
+
@abstractmethod
|
|
296
|
+
def make_source_state(self, state: dict) -> dict:
|
|
297
|
+
"""Initialise the emission rate parts of the state.
|
|
298
|
+
|
|
299
|
+
Args:
|
|
300
|
+
state (dict): dictionary containing current state information.
|
|
301
|
+
|
|
302
|
+
Returns:
|
|
303
|
+
dict: state updated with parameters related to the source emission rates.
|
|
304
|
+
|
|
305
|
+
"""
|
|
306
|
+
|
|
307
|
+
@abstractmethod
|
|
308
|
+
def from_mcmc_dist(self, store: dict):
|
|
309
|
+
"""Extract posterior emission rate samples from the MCMC, attach them to the class.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
store (dict): dictionary containing samples from the MCMC.
|
|
313
|
+
|
|
314
|
+
"""
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
@dataclass
|
|
318
|
+
class NormalResponse(SourceDistribution):
|
|
319
|
+
"""(Truncated) Gaussian prior for sources.
|
|
320
|
+
|
|
321
|
+
No transformation applied to parameters, i.e.:
|
|
322
|
+
- Prior distribution: s ~ N(mu, 1/precision)
|
|
323
|
+
- Likelihood contribution: y = A*s + b + ...
|
|
324
|
+
|
|
325
|
+
Attributes:
|
|
326
|
+
truncation (bool): indication of whether the emission rate prior should be truncated at 0. Defaults to True.
|
|
327
|
+
emission_rate_lb (Union[float, np.ndarray]): lower bound for the source emission rates. Defaults to 0.
|
|
328
|
+
emission_rate_mean (Union[float, np.ndarray]): prior mean for the emission rate distribution. Defaults to 0.
|
|
329
|
+
|
|
330
|
+
"""
|
|
331
|
+
|
|
332
|
+
truncation: bool = True
|
|
333
|
+
emission_rate_lb: Union[float, np.ndarray] = 0
|
|
334
|
+
emission_rate_mean: Union[float, np.ndarray] = 0
|
|
335
|
+
|
|
336
|
+
def make_source_model(self, model: list) -> list:
|
|
337
|
+
"""Add distributional component to the overall model corresponding to the source emission rate distribution.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
model (list): model as constructed so far, consisting of list of distributions.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
list: model, updated with distributions related to source prior.
|
|
344
|
+
|
|
345
|
+
"""
|
|
346
|
+
domain_response_lower = None
|
|
347
|
+
if self.truncation:
|
|
348
|
+
domain_response_lower = self.emission_rate_lb
|
|
349
|
+
|
|
350
|
+
model.append(
|
|
351
|
+
mcmcNormal(
|
|
352
|
+
"s",
|
|
353
|
+
mean=parameter.MixtureParameterVector(param="mu_s", allocation="alloc_s"),
|
|
354
|
+
precision=parameter.MixtureParameterMatrix(param="lambda_s", allocation="alloc_s"),
|
|
355
|
+
domain_response_lower=domain_response_lower,
|
|
356
|
+
)
|
|
357
|
+
)
|
|
358
|
+
return model
|
|
359
|
+
|
|
360
|
+
def make_source_sampler(self, model: Model, sampler_list: list = None) -> list:
|
|
361
|
+
"""Initialise the source prior distribution part of the sampler.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
model (Model): overall model set for the problem.
|
|
365
|
+
sampler_list (list): list of samplers for individual parameters.
|
|
366
|
+
|
|
367
|
+
Returns:
|
|
368
|
+
list: sampler_list updated with sampler for the emission rate parameters.
|
|
369
|
+
|
|
370
|
+
"""
|
|
371
|
+
if sampler_list is None:
|
|
372
|
+
sampler_list = []
|
|
373
|
+
sampler_list.append(NormalNormal("s", model))
|
|
374
|
+
return sampler_list
|
|
375
|
+
|
|
376
|
+
def make_source_state(self, state: dict) -> dict:
|
|
377
|
+
"""Initialise the emission rate part of the state.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
state (dict): dictionary containing current state information.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
dict: state updated with initial emission rate vector.
|
|
384
|
+
|
|
385
|
+
"""
|
|
386
|
+
state["s"] = np.zeros((self.nof_sources, 1))
|
|
387
|
+
return state
|
|
388
|
+
|
|
389
|
+
def from_mcmc_dist(self, store: dict):
|
|
390
|
+
"""Extract posterior emission rate samples from the MCMC sampler, attach them to the class.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
store (dict): dictionary containing samples from the MCMC.
|
|
394
|
+
|
|
395
|
+
"""
|
|
396
|
+
self.emission_rate = store["s"]
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@dataclass
|
|
400
|
+
class SourceModel(Component, SourceGrouping, SourceDistribution):
|
|
401
|
+
"""Superclass for the specification of the source model in an inversion run.
|
|
402
|
+
|
|
403
|
+
Various different types of model. A SourceModel is an optional component of a model, and thus inherits
|
|
404
|
+
from Component.
|
|
405
|
+
|
|
406
|
+
A subclass instance of SourceModel must inherit from:
|
|
407
|
+
- an INSTANCE of SourceDistribution, which specifies a prior emission rate distribution for all sources in the
|
|
408
|
+
source map.
|
|
409
|
+
- an INSTANCE of SourceGrouping, which specifies a type of mixture prior specification for the sources (for
|
|
410
|
+
which the allocation is to be estimated as part of the inversion).
|
|
411
|
+
|
|
412
|
+
If the flag reversible_jump == True, then the number of sources and their locations are also estimated as part of
|
|
413
|
+
the inversion, in addition to the emission rates. If this flag is set to true, the sensor_object, meteorology and
|
|
414
|
+
gas_species objects are all attached to the class, as they will be required in the repeated computation of updates
|
|
415
|
+
to the coupling matrix during the inversion.
|
|
416
|
+
|
|
417
|
+
Attributes:
|
|
418
|
+
dispersion_model (GaussianPlume): dispersion model used to generate the couplings between source locations and
|
|
419
|
+
sensor observations.
|
|
420
|
+
coupling (np.ndarray): coupling matrix generated using dispersion_model.
|
|
421
|
+
|
|
422
|
+
sensor_object (SensorGroup): stores sensor information for reversible jump coupling updates.
|
|
423
|
+
meteorology (MeteorologyGroup): stores meteorology information for reversible jump coupling updates.
|
|
424
|
+
gas_species (GasSpecies): stores gas species information for reversible jump coupling updates.
|
|
425
|
+
|
|
426
|
+
reversible_jump (bool): logical indicating whether the reversible jump algorithm for estimation of the number
|
|
427
|
+
of sources and their locations should be run. Defaults to False.
|
|
428
|
+
random_walk_step_size (np.ndarray): (3 x 1) array specifying the standard deviations of the distributions
|
|
429
|
+
from which the random walk sampler draws new source locations. Defaults to np.array([1.0, 1.0, 0.1]).
|
|
430
|
+
site_limits (np.ndarray): (3 x 2) array specifying the lower (column 0) and upper (column 1) limits of the
|
|
431
|
+
analysis site. Only relevant for cases where reversible_jump == True (where sources are free to move in
|
|
432
|
+
the solution).
|
|
433
|
+
rate_num_sources (int): specification for the parameter for the Poisson prior distribution for the total number
|
|
434
|
+
of sources. Only relevant for cases where reversible_jump == True (where the number of sources in the
|
|
435
|
+
solution can change).
|
|
436
|
+
n_sources_max (int): maximum number of sources that can feature in the solution. Only relevant for cases where
|
|
437
|
+
reversible_jump == True (where the number of sources in the solution can change).
|
|
438
|
+
emission_proposal_std (float): standard deviation of the truncated Gaussian distribution used to propose the
|
|
439
|
+
new source emission rate in case of a birth move.
|
|
440
|
+
|
|
441
|
+
update_precision (bool): logical indicating whether the prior precision parameter for emission rates should be
|
|
442
|
+
updated as part of the inversion. Defaults to false.
|
|
443
|
+
prior_precision_shape (Union[float, np.ndarray]): shape parameters for the prior Gamma distribution for the
|
|
444
|
+
source precision parameter.
|
|
445
|
+
prior_precision_rate (Union[float, np.ndarray]): rate parameters for the prior Gamma distribution for the
|
|
446
|
+
source precision parameter.
|
|
447
|
+
initial_precision (Union[float, np.ndarray]): initial value for the source emission rate precision parameter.
|
|
448
|
+
precision_scalar (np.ndarray): precision values generated by MCMC inversion.
|
|
449
|
+
|
|
450
|
+
coverage_detection (float): sensor detection threshold (in ppm) to be used for coverage calculations.
|
|
451
|
+
coverage_test_source (float): test source (in kg/hr) which we wish to be able to see in coverage calculation.
|
|
452
|
+
|
|
453
|
+
threshold_function (Callable): Callable function which returns a single value that defines the threshold
|
|
454
|
+
for the coupling in a lambda function form. Examples: lambda x: np.quantile(x, 0.95, axis=0),
|
|
455
|
+
lambda x: np.max(x, axis=0), lambda x: np.mean(x, axis=0). Defaults to np.quantile.
|
|
456
|
+
|
|
457
|
+
"""
|
|
458
|
+
|
|
459
|
+
dispersion_model: GaussianPlume = field(init=False, default=None)
|
|
460
|
+
coupling: np.ndarray = field(init=False)
|
|
461
|
+
|
|
462
|
+
sensor_object: SensorGroup = field(init=False, default=None)
|
|
463
|
+
meteorology: Meteorology = field(init=False, default=None)
|
|
464
|
+
gas_species: GasSpecies = field(init=False, default=None)
|
|
465
|
+
|
|
466
|
+
reversible_jump: bool = False
|
|
467
|
+
random_walk_step_size: np.ndarray = field(default_factory=lambda: np.array([1.0, 1.0, 0.1], ndmin=2).T)
|
|
468
|
+
site_limits: np.ndarray = None
|
|
469
|
+
rate_num_sources: int = 5
|
|
470
|
+
n_sources_max: int = 20
|
|
471
|
+
emission_proposal_std: float = 0.5
|
|
472
|
+
|
|
473
|
+
update_precision: bool = False
|
|
474
|
+
prior_precision_shape: Union[float, np.ndarray] = 1e-3
|
|
475
|
+
prior_precision_rate: Union[float, np.ndarray] = 1e-3
|
|
476
|
+
initial_precision: Union[float, np.ndarray] = 1.0
|
|
477
|
+
precision_scalar: np.ndarray = field(init=False)
|
|
478
|
+
|
|
479
|
+
coverage_detection: float = 0.1
|
|
480
|
+
coverage_test_source: float = 6.0
|
|
481
|
+
|
|
482
|
+
threshold_function: callable = lambda x: np.quantile(x, 0.95, axis=0)
|
|
483
|
+
|
|
484
|
+
@property
|
|
485
|
+
def nof_sources(self):
|
|
486
|
+
"""Get number of sources in the source map."""
|
|
487
|
+
return self.dispersion_model.source_map.nof_sources
|
|
488
|
+
|
|
489
|
+
@property
|
|
490
|
+
def coverage_threshold(self):
|
|
491
|
+
"""Compute coverage threshold from detection threshold and test source strength."""
|
|
492
|
+
return self.coverage_test_source / self.coverage_detection
|
|
493
|
+
|
|
494
|
+
def initialise(self, sensor_object: SensorGroup, meteorology: Meteorology, gas_species: GasSpecies):
|
|
495
|
+
"""Set up the source model.
|
|
496
|
+
|
|
497
|
+
Extract required information from the sensor, meteorology and gas species objects:
|
|
498
|
+
- Attach coupling calculated using self.dispersion_model.
|
|
499
|
+
- (If self.reversible_jump == True) Attach objects to source model which will be used in RJMCMC sampler,
|
|
500
|
+
they will be required when we need to update the couplings when new source locations are proposed when
|
|
501
|
+
we move/birth/death.
|
|
502
|
+
|
|
503
|
+
Args:
|
|
504
|
+
sensor_object (SensorGroup): object containing sensor data.
|
|
505
|
+
meteorology (MeteorologyGroup): object containing meteorology data.
|
|
506
|
+
gas_species (GasSpecies): object containing gas species information.
|
|
507
|
+
|
|
508
|
+
"""
|
|
509
|
+
self.initialise_dispersion_model(sensor_object)
|
|
510
|
+
self.coupling = self.dispersion_model.compute_coupling(
|
|
511
|
+
sensor_object, meteorology, gas_species, output_stacked=True
|
|
512
|
+
)
|
|
513
|
+
self.screen_coverage()
|
|
514
|
+
if self.reversible_jump:
|
|
515
|
+
self.sensor_object = sensor_object
|
|
516
|
+
self.meteorology = meteorology
|
|
517
|
+
self.gas_species = gas_species
|
|
518
|
+
|
|
519
|
+
def initialise_dispersion_model(self, sensor_object: SensorGroup):
|
|
520
|
+
"""Initialise the dispersion model.
|
|
521
|
+
|
|
522
|
+
If a dispersion_model has already been attached to this instance, then this function takes no action.
|
|
523
|
+
|
|
524
|
+
If a dispersion_model has not already been attached to the instance, then this function adds a GaussianPlume
|
|
525
|
+
dispersion model, with a default source map that has limits set based on the sensor locations.
|
|
526
|
+
|
|
527
|
+
Args:
|
|
528
|
+
sensor_object (SensorGroup): object containing sensor data.
|
|
529
|
+
|
|
530
|
+
"""
|
|
531
|
+
if self.dispersion_model is None:
|
|
532
|
+
source_map = SourceMap()
|
|
533
|
+
sensor_locations = sensor_object.location.to_enu()
|
|
534
|
+
location_object = ENU(
|
|
535
|
+
ref_latitude=sensor_locations.ref_latitude,
|
|
536
|
+
ref_longitude=sensor_locations.ref_longitude,
|
|
537
|
+
ref_altitude=sensor_locations.ref_altitude,
|
|
538
|
+
)
|
|
539
|
+
source_map.generate_sources(
|
|
540
|
+
coordinate_object=location_object,
|
|
541
|
+
sourcemap_limits=np.array(
|
|
542
|
+
[
|
|
543
|
+
[np.min(sensor_locations.east), np.max(sensor_locations.east)],
|
|
544
|
+
[np.min(sensor_locations.north), np.max(sensor_locations.north)],
|
|
545
|
+
[np.min(sensor_locations.up), np.max(sensor_locations.up)],
|
|
546
|
+
]
|
|
547
|
+
),
|
|
548
|
+
sourcemap_type="grid",
|
|
549
|
+
)
|
|
550
|
+
self.dispersion_model = GaussianPlume(source_map)
|
|
551
|
+
|
|
552
|
+
def screen_coverage(self):
|
|
553
|
+
"""Screen the initial source map for coverage."""
|
|
554
|
+
in_coverage_area = self.dispersion_model.compute_coverage(
|
|
555
|
+
self.coupling, coverage_threshold=self.coverage_threshold, threshold_function=self.threshold_function
|
|
556
|
+
)
|
|
557
|
+
self.coupling = self.coupling[:, in_coverage_area]
|
|
558
|
+
all_locations = self.dispersion_model.source_map.location.to_array()
|
|
559
|
+
screened_locations = all_locations[in_coverage_area, :]
|
|
560
|
+
self.dispersion_model.source_map.location.from_array(screened_locations)
|
|
561
|
+
|
|
562
|
+
def update_coupling_column(self, state: dict, update_column: int) -> dict:
|
|
563
|
+
"""Update the coupling, based on changes to the source locations as part of inversion.
|
|
564
|
+
|
|
565
|
+
To be used in two different situations:
|
|
566
|
+
- movement of source locations (e.g. Metropolis Hastings, random walk).
|
|
567
|
+
- adding of new source locations (e.g. reversible jump birth move).
|
|
568
|
+
If [update_column < A.shape[1]]: an existing column of the A matrix is updated.
|
|
569
|
+
If [update_column == A.shape[1]]: a new column is appended to the right-hand side of the A matrix
|
|
570
|
+
(corresponding to a new source).
|
|
571
|
+
|
|
572
|
+
A central assumption of this function is that the sensor information and meteorology information
|
|
573
|
+
have already been interpolated onto the same space/time points.
|
|
574
|
+
|
|
575
|
+
If an update_column is supplied, the coupling for that source location only is calculated to save on
|
|
576
|
+
computation time. If update_column is None, then we just re-compute the whole coupling matrix.
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
state (dict): dictionary containing state parameters.
|
|
580
|
+
update_column (int): index of the coupling column to be updated.
|
|
581
|
+
|
|
582
|
+
Returns:
|
|
583
|
+
state (dict): state dictionary containing updated coupling information.
|
|
584
|
+
|
|
585
|
+
"""
|
|
586
|
+
self.dispersion_model.source_map.location.from_array(state["z_src"][:, [update_column]].T)
|
|
587
|
+
new_coupling = self.dispersion_model.compute_coupling(
|
|
588
|
+
self.sensor_object, self.meteorology, self.gas_species, output_stacked=True, run_interpolation=False
|
|
589
|
+
)
|
|
590
|
+
|
|
591
|
+
if update_column == state["A"].shape[1]:
|
|
592
|
+
state["A"] = np.concatenate((state["A"], new_coupling), axis=1)
|
|
593
|
+
elif update_column < state["A"].shape[1]:
|
|
594
|
+
state["A"][:, [update_column]] = new_coupling
|
|
595
|
+
else:
|
|
596
|
+
raise ValueError("Invalid column specification for updating.")
|
|
597
|
+
return state
|
|
598
|
+
|
|
599
|
+
def birth_function(self, current_state: dict, prop_state: dict) -> Tuple[dict, float, float]:
|
|
600
|
+
"""Update MCMC state based on source birth proposal.
|
|
601
|
+
|
|
602
|
+
Proposed state updated as follows:
|
|
603
|
+
1- Add column to coupling matrix for new source location.
|
|
604
|
+
2- If required, adjust other components of the state which correspond to the sources.
|
|
605
|
+
The source emission rate vector will be adjusted using the standardised functionality
|
|
606
|
+
in the openMCMC package.
|
|
607
|
+
|
|
608
|
+
After the coupling has been updated, a coverage test is applied for the new source
|
|
609
|
+
location. If the max coupling is too small, a large contribution is added to the
|
|
610
|
+
log-proposal density for the new state, to force the sampler to reject it.
|
|
611
|
+
|
|
612
|
+
A central assumption of this function is that the sensor information and meteorology information
|
|
613
|
+
have already been interpolated onto the same space/time points.
|
|
614
|
+
|
|
615
|
+
This function assumes that the new source location has been added as the final column of
|
|
616
|
+
the source location matrix, and so will correspondingly append the new coupling column to the right
|
|
617
|
+
hand side of the current state coupling, and append an emission rate as the last element of the
|
|
618
|
+
current state emission rate vector.
|
|
619
|
+
|
|
620
|
+
Args:
|
|
621
|
+
current_state (dict): dictionary containing parameters of the current state.
|
|
622
|
+
prop_state (dict): dictionary containing the parameters of the proposed state.
|
|
623
|
+
|
|
624
|
+
Returns:
|
|
625
|
+
prop_state (dict): proposed state, with coupling matrix and source emission rate vector updated.
|
|
626
|
+
logp_pr_g_cr (float): log-transition density of the proposed state given the current state
|
|
627
|
+
(i.e. log[p(proposed | current)])
|
|
628
|
+
logp_cr_g_pr (float): log-transition density of the current state given the proposed state
|
|
629
|
+
(i.e. log[p(current | proposed)])
|
|
630
|
+
|
|
631
|
+
"""
|
|
632
|
+
prop_state = self.update_coupling_column(prop_state, int(prop_state["n_src"]) - 1)
|
|
633
|
+
prop_state["alloc_s"] = np.concatenate((prop_state["alloc_s"], np.array([0], ndmin=2)), axis=0)
|
|
634
|
+
in_cov_area = self.dispersion_model.compute_coverage(
|
|
635
|
+
prop_state["A"][:, -1],
|
|
636
|
+
coverage_threshold=self.coverage_threshold,
|
|
637
|
+
threshold_function=self.threshold_function,
|
|
638
|
+
)
|
|
639
|
+
if not in_cov_area:
|
|
640
|
+
logp_pr_g_cr = 1e10
|
|
641
|
+
else:
|
|
642
|
+
logp_pr_g_cr = 0.0
|
|
643
|
+
logp_cr_g_pr = 0.0
|
|
644
|
+
|
|
645
|
+
return prop_state, logp_pr_g_cr, logp_cr_g_pr
|
|
646
|
+
|
|
647
|
+
@staticmethod
|
|
648
|
+
def death_function(current_state: dict, prop_state: dict, deletion_index: int) -> Tuple[dict, float, float]:
|
|
649
|
+
"""Update MCMC state based on source death proposal.
|
|
650
|
+
|
|
651
|
+
Proposed state updated as follows:
|
|
652
|
+
1- Remove column from coupling for deleted source.
|
|
653
|
+
2- If required, adjust other components of the state which correspond to the sources.
|
|
654
|
+
The source emission rate vector will be adjusted using the standardised functionality in the general_mcmc repo.
|
|
655
|
+
|
|
656
|
+
A central assumption of this function is that the sensor information and meteorology information have already
|
|
657
|
+
been interpolated onto the same space/time points.
|
|
658
|
+
|
|
659
|
+
Args:
|
|
660
|
+
current_state (dict): dictionary containing parameters of the current state.
|
|
661
|
+
prop_state (dict): dictionary containing the parameters of the proposed state.
|
|
662
|
+
deletion_index (int): index of the source to be deleted in the overall set of sources.
|
|
663
|
+
|
|
664
|
+
Returns:
|
|
665
|
+
prop_state (dict): proposed state, with coupling matrix and source emission rate vector updated.
|
|
666
|
+
logp_pr_g_cr (float): log-transition density of the proposed state given the current state
|
|
667
|
+
(i.e. log[p(proposed | current)])
|
|
668
|
+
logp_cr_g_pr (float): log-transition density of the current state given the proposed state
|
|
669
|
+
(i.e. log[p(current | proposed)])
|
|
670
|
+
|
|
671
|
+
"""
|
|
672
|
+
prop_state["A"] = np.delete(prop_state["A"], obj=deletion_index, axis=1)
|
|
673
|
+
prop_state["alloc_s"] = np.delete(prop_state["alloc_s"], obj=deletion_index, axis=0)
|
|
674
|
+
logp_pr_g_cr = 0.0
|
|
675
|
+
logp_cr_g_pr = 0.0
|
|
676
|
+
|
|
677
|
+
return prop_state, logp_pr_g_cr, logp_cr_g_pr
|
|
678
|
+
|
|
679
|
+
def move_function(self, current_state: dict, update_column: int) -> dict:
|
|
680
|
+
"""Re-compute the coupling after a source location move.
|
|
681
|
+
|
|
682
|
+
Function first updates the coupling column, and then checks whether the location passes a coverage test. If the
|
|
683
|
+
location does not have good enough coverage, the state reverts to the coupling from the current state.
|
|
684
|
+
|
|
685
|
+
Args:
|
|
686
|
+
current_state (dict): dictionary containing parameters of the current state.
|
|
687
|
+
update_column (int): index of the coupling column to be updated.
|
|
688
|
+
|
|
689
|
+
Returns:
|
|
690
|
+
dict: proposed state, with updated coupling matrix.
|
|
691
|
+
|
|
692
|
+
"""
|
|
693
|
+
prop_state = deepcopy(current_state)
|
|
694
|
+
prop_state = self.update_coupling_column(prop_state, update_column)
|
|
695
|
+
in_cov_area = self.dispersion_model.compute_coverage(
|
|
696
|
+
prop_state["A"][:, update_column],
|
|
697
|
+
coverage_threshold=self.coverage_threshold,
|
|
698
|
+
threshold_function=self.threshold_function,
|
|
699
|
+
)
|
|
700
|
+
if not in_cov_area:
|
|
701
|
+
prop_state = deepcopy(current_state)
|
|
702
|
+
return prop_state
|
|
703
|
+
|
|
704
|
+
def make_model(self, model: list) -> list:
|
|
705
|
+
"""Take model list and append new elements from current model component.
|
|
706
|
+
|
|
707
|
+
Args:
|
|
708
|
+
model (list): Current list of model elements.
|
|
709
|
+
|
|
710
|
+
Returns:
|
|
711
|
+
list: model list updated with source-related distributions.
|
|
712
|
+
|
|
713
|
+
"""
|
|
714
|
+
model = self.make_allocation_model(model)
|
|
715
|
+
model = self.make_source_model(model)
|
|
716
|
+
if self.update_precision:
|
|
717
|
+
model.append(Gamma("lambda_s", shape="a_lam_s", rate="b_lam_s"))
|
|
718
|
+
if self.reversible_jump:
|
|
719
|
+
model.append(
|
|
720
|
+
Uniform(
|
|
721
|
+
response="z_src",
|
|
722
|
+
domain_response_lower=self.site_limits[:, [0]],
|
|
723
|
+
domain_response_upper=self.site_limits[:, [1]],
|
|
724
|
+
)
|
|
725
|
+
)
|
|
726
|
+
model.append(Poisson(response="n_src", rate="rho"))
|
|
727
|
+
return model
|
|
728
|
+
|
|
729
|
+
def make_sampler(self, model: Model, sampler_list: list) -> list:
|
|
730
|
+
"""Take sampler list and append new elements from current model component.
|
|
731
|
+
|
|
732
|
+
Args:
|
|
733
|
+
model (Model): Full model list of distributions.
|
|
734
|
+
sampler_list (list): Current list of samplers.
|
|
735
|
+
|
|
736
|
+
Returns:
|
|
737
|
+
list: sampler list updated with source-related samplers.
|
|
738
|
+
|
|
739
|
+
"""
|
|
740
|
+
sampler_list = self.make_source_sampler(model, sampler_list)
|
|
741
|
+
sampler_list = self.make_allocation_sampler(model, sampler_list)
|
|
742
|
+
if self.update_precision:
|
|
743
|
+
sampler_list.append(NormalGamma("lambda_s", model))
|
|
744
|
+
if self.reversible_jump:
|
|
745
|
+
sampler_list = self.make_sampler_rjmcmc(model, sampler_list)
|
|
746
|
+
return sampler_list
|
|
747
|
+
|
|
748
|
+
def make_state(self, state: dict) -> dict:
|
|
749
|
+
"""Take state dictionary and append initial values from model component.
|
|
750
|
+
|
|
751
|
+
Args:
|
|
752
|
+
state (dict): current state vector.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
dict: current state vector with source-related parameters added.
|
|
756
|
+
|
|
757
|
+
"""
|
|
758
|
+
state = self.make_allocation_state(state)
|
|
759
|
+
state = self.make_source_state(state)
|
|
760
|
+
state["A"] = self.coupling
|
|
761
|
+
state["lambda_s"] = np.array(self.initial_precision, ndmin=1)
|
|
762
|
+
if self.update_precision:
|
|
763
|
+
state["a_lam_s"] = np.ones_like(self.initial_precision) * self.prior_precision_shape
|
|
764
|
+
state["b_lam_s"] = np.ones_like(self.initial_precision) * self.prior_precision_rate
|
|
765
|
+
if self.reversible_jump:
|
|
766
|
+
state["z_src"] = self.dispersion_model.source_map.location.to_array().T
|
|
767
|
+
state["n_src"] = state["z_src"].shape[1]
|
|
768
|
+
state["rho"] = self.rate_num_sources
|
|
769
|
+
return state
|
|
770
|
+
|
|
771
|
+
def make_sampler_rjmcmc(self, model: Model, sampler_list: list) -> list:
|
|
772
|
+
"""Create the parts of the sampler related to the reversible jump MCMC scheme.
|
|
773
|
+
|
|
774
|
+
RJ MCMC scheme:
|
|
775
|
+
- create the RandomWalkLoop sampler object which updates the source locations one-at-a-time.
|
|
776
|
+
- create the ReversibleJump sampler which proposes birth/death moves to add/remove sources from the source
|
|
777
|
+
map.
|
|
778
|
+
|
|
779
|
+
Args:
|
|
780
|
+
model (Model): model object containing probability density objects for all uncertain
|
|
781
|
+
parameters.
|
|
782
|
+
sampler_list (list): list of existing samplers.
|
|
783
|
+
|
|
784
|
+
Returns:
|
|
785
|
+
sampler_list (list): list of samplers updated with samplers corresponding to RJMCMC routine.
|
|
786
|
+
|
|
787
|
+
"""
|
|
788
|
+
sampler_list[-1].max_variable_size = self.n_sources_max
|
|
789
|
+
|
|
790
|
+
sampler_list.append(
|
|
791
|
+
RandomWalkLoop(
|
|
792
|
+
"z_src",
|
|
793
|
+
model,
|
|
794
|
+
step=self.random_walk_step_size,
|
|
795
|
+
max_variable_size=(3, self.n_sources_max),
|
|
796
|
+
domain_limits=self.site_limits,
|
|
797
|
+
state_update_function=self.move_function,
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
matching_params = {"variable": "s", "matrix": "A", "scale": 1.0, "limits": [0.0, 1e6]}
|
|
801
|
+
sampler_list.append(
|
|
802
|
+
ReversibleJump(
|
|
803
|
+
"n_src",
|
|
804
|
+
model,
|
|
805
|
+
step=np.array([1.0], ndmin=2),
|
|
806
|
+
associated_params="z_src",
|
|
807
|
+
n_max=self.n_sources_max,
|
|
808
|
+
state_birth_function=self.birth_function,
|
|
809
|
+
state_death_function=self.death_function,
|
|
810
|
+
matching_params=matching_params,
|
|
811
|
+
)
|
|
812
|
+
)
|
|
813
|
+
return sampler_list
|
|
814
|
+
|
|
815
|
+
def from_mcmc(self, store: dict):
|
|
816
|
+
"""Extract results of mcmc from mcmc.store and attach to components.
|
|
817
|
+
|
|
818
|
+
Args:
|
|
819
|
+
store (dict): mcmc result dictionary.
|
|
820
|
+
|
|
821
|
+
"""
|
|
822
|
+
self.from_mcmc_group(store)
|
|
823
|
+
self.from_mcmc_dist(store)
|
|
824
|
+
if self.update_precision:
|
|
825
|
+
self.precision_scalar = store["lambda_s"]
|
|
826
|
+
|
|
827
|
+
def plot_iterations(self, plot: "Plot", burn_in_value: int, y_axis_type: str = "linear") -> "Plot":
|
|
828
|
+
"""Plot the emission rate estimates source model object against MCMC iteration.
|
|
829
|
+
|
|
830
|
+
Args:
|
|
831
|
+
burn_in_value (int): Burn in value to show in plot.
|
|
832
|
+
y_axis_type (str, optional): String to indicate whether the y-axis should be linear of log scale.
|
|
833
|
+
plot (Plot): Plot object to which this figure will be added in the figure dictionary.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
plot (Plot): Plot object to which the figures added in the figure dictionary with
|
|
837
|
+
keys 'estimated_values_plot'/'log_estimated_values_plot' and 'number_of_sources_plot'
|
|
838
|
+
|
|
839
|
+
"""
|
|
840
|
+
plot.plot_emission_rate_estimates(source_model_object=self, burn_in=burn_in_value, y_axis_type=y_axis_type)
|
|
841
|
+
plot.plot_single_trace(object_to_plot=self)
|
|
842
|
+
return plot
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
@dataclass
|
|
846
|
+
class Normal(SourceModel, NullGrouping, NormalResponse):
|
|
847
|
+
"""Normal model, with null allocation.
|
|
848
|
+
|
|
849
|
+
(Truncated) Gaussian prior for emission rates, no grouping/allocation; no transformation applied to emission rate
|
|
850
|
+
parameters.
|
|
851
|
+
|
|
852
|
+
Can be used in the following cases:
|
|
853
|
+
- Fixed set of sources (grid or specific locations), all with the same Gaussian prior distribution.
|
|
854
|
+
- Variable number of sources, with a common prior distribution, estimated using reversible jump MCMC.
|
|
855
|
+
- Fixed set of sources with a bespoke prior per source (using the allocation to map prior parameters onto
|
|
856
|
+
sources).
|
|
857
|
+
|
|
858
|
+
"""
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
@dataclass
|
|
862
|
+
class NormalSlabAndSpike(SourceModel, SlabAndSpike, NormalResponse):
|
|
863
|
+
"""Normal Slab and Spike model.
|
|
864
|
+
|
|
865
|
+
(Truncated) Gaussian prior for emission rates, slab and spike prior, with allocation estimation; no transformation
|
|
866
|
+
applied to emission rate parameters.
|
|
867
|
+
|
|
868
|
+
Attributes:
|
|
869
|
+
initial_precision (np.ndarray): initial precision parameter for a slab and spike case. shape=(2, 1).
|
|
870
|
+
emission_rate_mean (np.ndarray): emission rate prior mean for a slab and spike case. shape=(2, 1).
|
|
871
|
+
|
|
872
|
+
"""
|
|
873
|
+
|
|
874
|
+
initial_precision: np.ndarray = field(default_factory=lambda: np.array([1 / (10**2), 1 / (0.01**2)], ndmin=2).T)
|
|
875
|
+
emission_rate_mean: np.ndarray = field(default_factory=lambda: np.array([0, 0], ndmin=2).T)
|