pyelq 1.1.4__py3-none-any.whl → 1.2.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 +1 -0
- pyelq/component/__init__.py +1 -0
- pyelq/component/background.py +19 -13
- pyelq/component/component.py +2 -1
- pyelq/component/error_model.py +2 -1
- pyelq/component/offset.py +2 -1
- pyelq/component/source_model.py +78 -29
- pyelq/coordinate_system.py +1 -0
- pyelq/data_access/__init__.py +1 -0
- pyelq/data_access/data_access.py +1 -1
- pyelq/dispersion_model/__init__.py +4 -3
- pyelq/dispersion_model/dispersion_model.py +202 -0
- pyelq/dispersion_model/finite_volume.py +1084 -0
- pyelq/dispersion_model/gaussian_plume.py +8 -189
- pyelq/dispersion_model/site_layout.py +97 -0
- pyelq/dlm.py +11 -15
- pyelq/gas_species.py +1 -0
- pyelq/meteorology/__init__.py +6 -0
- pyelq/{meteorology.py → meteorology/meteorology.py} +388 -387
- pyelq/meteorology/meteorology_windfield.py +180 -0
- pyelq/model.py +2 -1
- pyelq/plotting/__init__.py +1 -0
- pyelq/plotting/plot.py +1 -0
- pyelq/preprocessing.py +98 -38
- pyelq/sensor/__init__.py +1 -0
- pyelq/sensor/sensor.py +70 -5
- pyelq/source_map.py +1 -0
- pyelq/support_functions/__init__.py +1 -0
- pyelq/support_functions/post_processing.py +1 -0
- pyelq/support_functions/spatio_temporal_interpolation.py +1 -0
- {pyelq-1.1.4.dist-info → pyelq-1.2.0.dist-info}/METADATA +45 -44
- pyelq-1.2.0.dist-info/RECORD +37 -0
- {pyelq-1.1.4.dist-info → pyelq-1.2.0.dist-info}/WHEEL +1 -1
- pyelq-1.1.4.dist-info/RECORD +0 -32
- {pyelq-1.1.4.dist-info → pyelq-1.2.0.dist-info/licenses}/LICENSE.md +0 -0
- {pyelq-1.1.4.dist-info → pyelq-1.2.0.dist-info/licenses}/LICENSES/Apache-2.0.txt +0 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText:
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
2
|
#
|
|
3
3
|
# SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
|
|
@@ -10,37 +10,32 @@ The class for the Gaussian Plume dispersion model used in pyELQ.
|
|
|
10
10
|
The Mathematics of Atmospheric Dispersion Modeling, John M. Stockie, DOI. 10.1137/10080991X
|
|
11
11
|
|
|
12
12
|
"""
|
|
13
|
+
|
|
13
14
|
from copy import deepcopy
|
|
14
15
|
from dataclasses import dataclass
|
|
15
|
-
from typing import
|
|
16
|
+
from typing import Union
|
|
16
17
|
|
|
17
18
|
import numpy as np
|
|
18
19
|
|
|
19
|
-
import pyelq.support_functions.spatio_temporal_interpolation as sti
|
|
20
20
|
from pyelq.coordinate_system import ENU, LLA
|
|
21
|
+
from pyelq.dispersion_model.dispersion_model import DispersionModel
|
|
21
22
|
from pyelq.gas_species import GasSpecies
|
|
22
|
-
from pyelq.meteorology import Meteorology, MeteorologyGroup
|
|
23
|
+
from pyelq.meteorology.meteorology import Meteorology, MeteorologyGroup
|
|
23
24
|
from pyelq.sensor.beam import Beam
|
|
24
25
|
from pyelq.sensor.satellite import Satellite
|
|
25
26
|
from pyelq.sensor.sensor import Sensor, SensorGroup
|
|
26
|
-
from pyelq.source_map import SourceMap
|
|
27
27
|
|
|
28
28
|
|
|
29
29
|
@dataclass
|
|
30
|
-
class GaussianPlume:
|
|
30
|
+
class GaussianPlume(DispersionModel):
|
|
31
31
|
"""Defines the Gaussian plume dispersion model class.
|
|
32
32
|
|
|
33
33
|
Attributes:
|
|
34
|
-
source_map (Sourcemap): SourceMap object used for the dispersion model
|
|
35
34
|
source_half_width (float): Source half width (radius) to be used in the Gaussian plume model (in meters)
|
|
36
|
-
minimum_contribution (float): All elements in the Gaussian plume coupling smaller than this number will be set
|
|
37
|
-
to 0. Helps to speed up matrix multiplications/matrix inverses, also helps with stability
|
|
38
35
|
|
|
39
36
|
"""
|
|
40
37
|
|
|
41
|
-
source_map: SourceMap
|
|
42
38
|
source_half_width: float = 1
|
|
43
|
-
minimum_contribution: float = 0
|
|
44
39
|
|
|
45
40
|
def compute_coupling(
|
|
46
41
|
self,
|
|
@@ -120,7 +115,7 @@ class GaussianPlume:
|
|
|
120
115
|
self,
|
|
121
116
|
sensor_object: Sensor,
|
|
122
117
|
meteorology: Meteorology,
|
|
123
|
-
gas_object: GasSpecies = None,
|
|
118
|
+
gas_object: Union[GasSpecies, None] = None,
|
|
124
119
|
run_interpolation: bool = True,
|
|
125
120
|
) -> Union[list, np.ndarray]:
|
|
126
121
|
"""Wrapper function to compute the gaussian plume coupling for a single sensor.
|
|
@@ -186,7 +181,7 @@ class GaussianPlume:
|
|
|
186
181
|
)
|
|
187
182
|
|
|
188
183
|
if sensor_object.source_on is not None:
|
|
189
|
-
plume_coupling = plume_coupling * sensor_object.source_on[:, None]
|
|
184
|
+
plume_coupling = plume_coupling * np.where(sensor_object.source_on != 0, 1, 0)[:, None]
|
|
190
185
|
|
|
191
186
|
return plume_coupling
|
|
192
187
|
|
|
@@ -255,150 +250,6 @@ class GaussianPlume:
|
|
|
255
250
|
|
|
256
251
|
return plume_coupling
|
|
257
252
|
|
|
258
|
-
def calculate_gas_density(
|
|
259
|
-
self, meteorology: Meteorology, sensor_object: Sensor, gas_object: Union[GasSpecies, None]
|
|
260
|
-
) -> np.ndarray:
|
|
261
|
-
"""Helper function to calculate the gas density using ideal gas law.
|
|
262
|
-
|
|
263
|
-
https://en.wikipedia.org/wiki/Ideal_gas
|
|
264
|
-
|
|
265
|
-
When a gas object is passed as input we calculate the density according to that gas. We check if the
|
|
266
|
-
meteorology object has a temperature and/or pressure value and use those accordingly. Otherwise, we use Standard
|
|
267
|
-
Temperature and Pressure (STP).
|
|
268
|
-
|
|
269
|
-
We interpolate the temperature and pressure values to the source locations/times such that this is consistent
|
|
270
|
-
with the other calculations, i.e. we only do spatial interpolation when the sensor is a Satellite object
|
|
271
|
-
and temporal interpolation otherwise.
|
|
272
|
-
|
|
273
|
-
When no gas_object is passed in we just set the gas density value to 1.
|
|
274
|
-
|
|
275
|
-
Args:
|
|
276
|
-
meteorology (Meteorology): Meteorology object potentially containing temperature or pressure values
|
|
277
|
-
sensor_object (Sensor): Sensor object containing information about where to interpolate to
|
|
278
|
-
gas_object (Union[GasSpecies, None]): Gas species object which actually calculates the correct density
|
|
279
|
-
|
|
280
|
-
Returns:
|
|
281
|
-
gas_density (np.ndarray): Numpy array of shape [1 x nof_sources] (Satellite sensor)
|
|
282
|
-
or [nof_observations x 1] (otherwise) containing the gas density values to use
|
|
283
|
-
|
|
284
|
-
"""
|
|
285
|
-
if not isinstance(gas_object, GasSpecies):
|
|
286
|
-
if isinstance(sensor_object, Satellite):
|
|
287
|
-
return np.ones((1, self.source_map.nof_sources))
|
|
288
|
-
return np.ones((sensor_object.nof_observations, 1))
|
|
289
|
-
|
|
290
|
-
temperature_interpolated = self.interpolate_meteorology(
|
|
291
|
-
meteorology=meteorology, variable_name="temperature", sensor_object=sensor_object
|
|
292
|
-
)
|
|
293
|
-
if temperature_interpolated is None:
|
|
294
|
-
temperature_interpolated = np.array([[273.15]])
|
|
295
|
-
|
|
296
|
-
pressure_interpolated = self.interpolate_meteorology(
|
|
297
|
-
meteorology=meteorology, variable_name="pressure", sensor_object=sensor_object
|
|
298
|
-
)
|
|
299
|
-
if pressure_interpolated is None:
|
|
300
|
-
pressure_interpolated = np.array([[101.325]])
|
|
301
|
-
|
|
302
|
-
gas_density = gas_object.gas_density(temperature=temperature_interpolated, pressure=pressure_interpolated)
|
|
303
|
-
|
|
304
|
-
return gas_density
|
|
305
|
-
|
|
306
|
-
def interpolate_all_meteorology(
|
|
307
|
-
self, sensor_object: Sensor, meteorology: Meteorology, gas_object: GasSpecies, run_interpolation: bool
|
|
308
|
-
):
|
|
309
|
-
"""Function which carries out interpolation of all meteorological information.
|
|
310
|
-
|
|
311
|
-
The flag run_interpolation determines whether the interpolation should be carried out. If this
|
|
312
|
-
is set to be False, the meteorological parameters are simply set to the values stored on the
|
|
313
|
-
meteorology object (i.e. we assume that the meteorology has already been interpolated). This
|
|
314
|
-
functionality is required to avoid wasted computation in the case of e.g. a reversible jump run.
|
|
315
|
-
|
|
316
|
-
Args:
|
|
317
|
-
sensor_object (Sensor): object containing locations/times onto which met information should
|
|
318
|
-
be interpolated.
|
|
319
|
-
meteorology (Meteorology): object containing meteorology information for interpolation.
|
|
320
|
-
gas_object (GasSpecies): object containing gas information.
|
|
321
|
-
run_interpolation (bool): logical indicating whether the meteorology information needs to be interpolated.
|
|
322
|
-
|
|
323
|
-
Returns:
|
|
324
|
-
gas_density (np.ndarray): numpy array of shape [n_data x 1] of gas densities.
|
|
325
|
-
u_interpolated (np.ndarray): numpy array of shape [n_data x 1] of northerly wind components.
|
|
326
|
-
v_interpolated (np.ndarray): numpy array of shape [n_data x 1] of easterly wind components.
|
|
327
|
-
wind_turbulence_horizontal (np.ndarray): numpy array of shape [n_data x 1] of horizontal turbulence
|
|
328
|
-
parameters.
|
|
329
|
-
wind_turbulence_vertical (np.ndarray): numpy array of shape [n_data x 1] of vertical turbulence
|
|
330
|
-
parameters.
|
|
331
|
-
|
|
332
|
-
"""
|
|
333
|
-
if run_interpolation:
|
|
334
|
-
gas_density = self.calculate_gas_density(
|
|
335
|
-
meteorology=meteorology, sensor_object=sensor_object, gas_object=gas_object
|
|
336
|
-
)
|
|
337
|
-
u_interpolated = self.interpolate_meteorology(
|
|
338
|
-
meteorology=meteorology, variable_name="u_component", sensor_object=sensor_object
|
|
339
|
-
)
|
|
340
|
-
v_interpolated = self.interpolate_meteorology(
|
|
341
|
-
meteorology=meteorology, variable_name="v_component", sensor_object=sensor_object
|
|
342
|
-
)
|
|
343
|
-
wind_turbulence_horizontal = self.interpolate_meteorology(
|
|
344
|
-
meteorology=meteorology, variable_name="wind_turbulence_horizontal", sensor_object=sensor_object
|
|
345
|
-
)
|
|
346
|
-
wind_turbulence_vertical = self.interpolate_meteorology(
|
|
347
|
-
meteorology=meteorology, variable_name="wind_turbulence_vertical", sensor_object=sensor_object
|
|
348
|
-
)
|
|
349
|
-
else:
|
|
350
|
-
gas_density = gas_object.gas_density(temperature=meteorology.temperature, pressure=meteorology.pressure)
|
|
351
|
-
gas_density = gas_density.reshape((gas_density.size, 1))
|
|
352
|
-
u_interpolated = meteorology.u_component.reshape((meteorology.u_component.size, 1))
|
|
353
|
-
v_interpolated = meteorology.v_component.reshape((meteorology.v_component.size, 1))
|
|
354
|
-
wind_turbulence_horizontal = meteorology.wind_turbulence_horizontal.reshape(
|
|
355
|
-
(meteorology.wind_turbulence_horizontal.size, 1)
|
|
356
|
-
)
|
|
357
|
-
wind_turbulence_vertical = meteorology.wind_turbulence_vertical.reshape(
|
|
358
|
-
(meteorology.wind_turbulence_vertical.size, 1)
|
|
359
|
-
)
|
|
360
|
-
|
|
361
|
-
return gas_density, u_interpolated, v_interpolated, wind_turbulence_horizontal, wind_turbulence_vertical
|
|
362
|
-
|
|
363
|
-
def interpolate_meteorology(
|
|
364
|
-
self, meteorology: Meteorology, variable_name: str, sensor_object: Sensor
|
|
365
|
-
) -> Union[np.ndarray, None]:
|
|
366
|
-
"""Helper function to interpolate meteorology variables.
|
|
367
|
-
|
|
368
|
-
This function interpolates meteorological variables to times in Sensor or Sources in sourcemap. It also
|
|
369
|
-
calculates the wind speed and mathematical angle between the u- and v-components which in turn gets used in the
|
|
370
|
-
calculation of the Gaussian plume.
|
|
371
|
-
|
|
372
|
-
When the input sensor object is a Satellite type we use spatial interpolation using the interpolation method
|
|
373
|
-
from the coordinate system class as this takes care of the coordinate systems.
|
|
374
|
-
When the input sensor object is of another time we use temporal interpolation (assumption is spatial uniformity
|
|
375
|
-
for all observations over a small(er) area).
|
|
376
|
-
|
|
377
|
-
Args:
|
|
378
|
-
meteorology (Meteorology): Meteorology object containing u- and v-components of wind including their
|
|
379
|
-
spatial location
|
|
380
|
-
variable_name (str): String name of an attribute in the meteorology input object which needs to be
|
|
381
|
-
interpolated
|
|
382
|
-
sensor_object (Sensor): Sensor object containing information about where to interpolate to
|
|
383
|
-
|
|
384
|
-
Returns:
|
|
385
|
-
variable_interpolated (np.ndarray): Interpolated values
|
|
386
|
-
|
|
387
|
-
"""
|
|
388
|
-
variable = getattr(meteorology, variable_name)
|
|
389
|
-
if variable is None:
|
|
390
|
-
return None
|
|
391
|
-
|
|
392
|
-
if isinstance(sensor_object, Satellite):
|
|
393
|
-
variable_interpolated = meteorology.location.interpolate(variable, self.source_map.location)
|
|
394
|
-
variable_interpolated = variable_interpolated.reshape(1, self.source_map.nof_sources)
|
|
395
|
-
else:
|
|
396
|
-
variable_interpolated = sti.interpolate(
|
|
397
|
-
time_in=meteorology.time, values_in=variable, time_out=sensor_object.time
|
|
398
|
-
)
|
|
399
|
-
variable_interpolated = variable_interpolated.reshape(sensor_object.nof_observations, 1)
|
|
400
|
-
return variable_interpolated
|
|
401
|
-
|
|
402
253
|
def compute_coupling_satellite(
|
|
403
254
|
self,
|
|
404
255
|
sensor_object: Sensor,
|
|
@@ -559,38 +410,6 @@ class GaussianPlume:
|
|
|
559
410
|
|
|
560
411
|
return plume_coupling
|
|
561
412
|
|
|
562
|
-
@staticmethod
|
|
563
|
-
def compute_coverage(
|
|
564
|
-
couplings: np.ndarray, threshold_function: Callable, coverage_threshold: float = 6, **kwargs
|
|
565
|
-
) -> Union[np.ndarray, dict]:
|
|
566
|
-
"""Returns a logical vector that indicates which sources in the couplings are, or are not, within the coverage.
|
|
567
|
-
|
|
568
|
-
The 'coverage' is the area inside which all sources are well covered by wind data. E.g. If wind exclusively
|
|
569
|
-
blows towards East, then all sources to the East of any sensor are 'invisible', and are not within the coverage.
|
|
570
|
-
|
|
571
|
-
Couplings are returned in hr/kg. Some threshold function defines the largest allowed coupling value. This is
|
|
572
|
-
used to calculate estimated emission rates in kg/hr. Any emissions which are greater than the value of
|
|
573
|
-
'coverage_threshold' are defined as not within the coverage.
|
|
574
|
-
|
|
575
|
-
Args:
|
|
576
|
-
couplings (np.ndarray): Array of coupling values. Dimensions: n_datapoints x n_sources.
|
|
577
|
-
threshold_function (Callable): Callable function which returns some single value that defines the
|
|
578
|
-
maximum or 'threshold' coupling. For example: np.quantile(., q=0.95)
|
|
579
|
-
coverage_threshold (float, optional): The threshold value of the estimated emission rate which is
|
|
580
|
-
considered to be within the coverage. Defaults to 6 kg/hr.
|
|
581
|
-
kwargs (dict, optional): Keyword arguments required for the threshold function.
|
|
582
|
-
|
|
583
|
-
Returns:
|
|
584
|
-
coverage (Union[np.ndarray, dict]): A logical array specifying which sources are within the coverage.
|
|
585
|
-
|
|
586
|
-
"""
|
|
587
|
-
coupling_threshold = threshold_function(couplings, **kwargs)
|
|
588
|
-
no_warning_threshold = np.where(coupling_threshold <= 1e-100, 1, coupling_threshold)
|
|
589
|
-
no_warning_estimated_emission_rates = np.where(coupling_threshold <= 1e-100, np.inf, 1 / no_warning_threshold)
|
|
590
|
-
coverage = no_warning_estimated_emission_rates < coverage_threshold
|
|
591
|
-
|
|
592
|
-
return coverage
|
|
593
|
-
|
|
594
413
|
|
|
595
414
|
def _create_enu_sensor_array(
|
|
596
415
|
inclusion_idx: np.ndarray, sensor_object: Sensor, source_map_location_lla: LLA, current_source: int
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 Shell Global Solutions International B.V. All Rights Reserved.
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
5
|
+
# -*- coding: utf-8 -*-
|
|
6
|
+
"""Site layout module.
|
|
7
|
+
|
|
8
|
+
This module defines the SiteLayout class, which represents the layout of a site, giving the option to include obstacles
|
|
9
|
+
(e.g. buildings, tanks, equipment) as cylinders obstructing the flow field.
|
|
10
|
+
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import Union
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
from scipy import spatial
|
|
18
|
+
|
|
19
|
+
from pyelq.coordinate_system import ENU
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class SiteLayout:
|
|
24
|
+
"""Class for site layout defining cylindrical obstacles in the environment.
|
|
25
|
+
|
|
26
|
+
These are used with MeteorologyWindfield to calculate the wind field at each grid point with a potential flow
|
|
27
|
+
around the cylindrical obstacles.
|
|
28
|
+
|
|
29
|
+
Attributes:
|
|
30
|
+
cylinder_coordinates (Union[ENU, None]): The coordinates of the cylindrical obstacles in the site layout. The
|
|
31
|
+
east, north represent the the center of the cylinder and the up coordinate represents the cylinder height.
|
|
32
|
+
cylinder_radius (np.ndarray): The radius of the cylindrical obstacles in the site layout.
|
|
33
|
+
id_obstacles (np.ndarray): Boolean array indicating which grid points are within obstacle regions.
|
|
34
|
+
id_obstacles_index (np.ndarray): The indices of the grid points that are within obstacle regions.
|
|
35
|
+
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
cylinder_coordinates: Union[ENU, None]
|
|
39
|
+
cylinder_radius: np.ndarray
|
|
40
|
+
|
|
41
|
+
id_obstacles: np.ndarray = field(init=False)
|
|
42
|
+
id_obstacles_index: np.ndarray = field(init=False)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def nof_cylinders(self) -> int:
|
|
46
|
+
"""Int: Returns the number of cylinders in the site layout."""
|
|
47
|
+
if self.cylinder_coordinates is None:
|
|
48
|
+
return 0
|
|
49
|
+
return self.cylinder_coordinates.nof_observations
|
|
50
|
+
|
|
51
|
+
def find_index_obstacles(self, grid_coordinates: ENU):
|
|
52
|
+
"""Find the indices of the grid_coordinates that are within the radius of the obstacles.
|
|
53
|
+
|
|
54
|
+
This method uses a KDTree to efficiently find the indices of the grid points that are within the radius of the
|
|
55
|
+
cylindrical obstacles. It also checks the height of the grid points against the height of the obstacles.
|
|
56
|
+
If the height of the grid point is greater than the height of the obstacle, it is not considered an obstacle.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
grid_coordinates (ENU): The coordinates of the grid points to check.
|
|
60
|
+
|
|
61
|
+
"""
|
|
62
|
+
if self.cylinder_coordinates is None or self.cylinder_coordinates.nof_observations == 0:
|
|
63
|
+
self.id_obstacles = np.zeros((grid_coordinates.nof_observations, 1), dtype=bool)
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
self.check_reference_coordinates(grid_coordinates)
|
|
67
|
+
|
|
68
|
+
grid_coordinates_array = grid_coordinates.to_array(dim=2)
|
|
69
|
+
tree = spatial.KDTree(grid_coordinates_array)
|
|
70
|
+
indices = tree.query_ball_point(x=self.cylinder_coordinates.to_array(dim=2), r=self.cylinder_radius.flatten())
|
|
71
|
+
|
|
72
|
+
if grid_coordinates.up is not None:
|
|
73
|
+
|
|
74
|
+
for i, height in enumerate(self.cylinder_coordinates.up):
|
|
75
|
+
indices[i] = np.array(indices[i])
|
|
76
|
+
if len(indices[i]) > 0:
|
|
77
|
+
indices[i] = indices[i][grid_coordinates.up[indices[i]].flatten() <= height]
|
|
78
|
+
indices_conc = np.unique(np.concatenate(indices, axis=0)).astype(int)
|
|
79
|
+
self.id_obstacles_index = indices_conc
|
|
80
|
+
self.id_obstacles = np.zeros((grid_coordinates.nof_observations, 1), dtype=bool)
|
|
81
|
+
self.id_obstacles[[indices_conc]] = True
|
|
82
|
+
|
|
83
|
+
def check_reference_coordinates(self, grid_coordinates: ENU):
|
|
84
|
+
"""Check if the reference coordinates of the grid and cylinder coordinates match.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
grid_coordinates (ENU): The coordinates of the grid points to check.
|
|
88
|
+
|
|
89
|
+
Raises:
|
|
90
|
+
ValueError: If the reference coordinates do not match.
|
|
91
|
+
"""
|
|
92
|
+
if grid_coordinates.ref_altitude != self.cylinder_coordinates.ref_altitude:
|
|
93
|
+
raise ValueError("Grid coordinates and cylinder coordinates must have the same reference altitude.")
|
|
94
|
+
if grid_coordinates.ref_longitude != self.cylinder_coordinates.ref_longitude:
|
|
95
|
+
raise ValueError("Grid coordinates and cylinder coordinates must have the same reference longitude.")
|
|
96
|
+
if grid_coordinates.ref_latitude != self.cylinder_coordinates.ref_latitude:
|
|
97
|
+
raise ValueError("Grid coordinates and cylinder coordinates must have the same reference latitude.")
|
pyelq/dlm.py
CHANGED
|
@@ -9,11 +9,12 @@ This module provides a class definition for the Dynamic Linear Models following
|
|
|
9
9
|
'Bayesian Forecasting and Dynamic Models' (2nd ed), Springer New York, NY, Chapter 4, https://doi.org/10.1007/b98971
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
from dataclasses import dataclass, field
|
|
13
14
|
from typing import Tuple, Union
|
|
14
15
|
|
|
15
16
|
import numpy as np
|
|
16
|
-
from scipy.stats import chi2
|
|
17
|
+
from scipy.stats import chi2, multivariate_normal
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
@dataclass
|
|
@@ -119,23 +120,18 @@ class DLM:
|
|
|
119
120
|
mean_state_noise = np.zeros(self.nof_state_parameters)
|
|
120
121
|
mean_observation_noise = np.zeros(self.nof_observables)
|
|
121
122
|
|
|
122
|
-
random_generator = np.random.default_rng(seed=None)
|
|
123
|
-
|
|
124
123
|
for i in range(nof_timesteps):
|
|
125
124
|
if i == 0:
|
|
126
|
-
state[:, [i]] = (
|
|
127
|
-
self.
|
|
128
|
-
|
|
129
|
-
)
|
|
125
|
+
state[:, [i]] = self.g_matrix @ init_state + multivariate_normal.rvs(
|
|
126
|
+
mean=mean_state_noise, cov=self.w_matrix, size=1
|
|
127
|
+
).reshape((-1, 1))
|
|
130
128
|
else:
|
|
131
|
-
state[:, [i]] = (
|
|
132
|
-
self.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
+ random_generator.multivariate_normal(mean_observation_noise, self.v_matrix, size=1).T
|
|
138
|
-
)
|
|
129
|
+
state[:, [i]] = self.g_matrix @ state[:, [i - 1]] + multivariate_normal.rvs(
|
|
130
|
+
mean=mean_state_noise, cov=self.w_matrix, size=1
|
|
131
|
+
).reshape((-1, 1))
|
|
132
|
+
obs[:, [i]] = self.f_matrix.T @ state[:, [i]] + multivariate_normal.rvs(
|
|
133
|
+
mean=mean_observation_noise, cov=self.v_matrix, size=1
|
|
134
|
+
).reshape((-1, 1))
|
|
139
135
|
|
|
140
136
|
return state, obs
|
|
141
137
|
|
pyelq/gas_species.py
CHANGED
|
@@ -9,6 +9,7 @@ The superclass for the Gas species classes. It contains a few gas species with i
|
|
|
9
9
|
calculate the density of the gas and do emission rate conversions from m^3/s to kg/hr and back
|
|
10
10
|
|
|
11
11
|
"""
|
|
12
|
+
|
|
12
13
|
from abc import ABC, abstractmethod
|
|
13
14
|
from dataclasses import dataclass, field
|
|
14
15
|
from typing import Union
|