pygnss 2.1.2__cp314-cp314t-macosx_11_0_arm64.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.
- pygnss/__init__.py +1 -0
- pygnss/_c_ext/src/constants.c +36 -0
- pygnss/_c_ext/src/hatanaka.c +94 -0
- pygnss/_c_ext/src/helpers.c +17 -0
- pygnss/_c_ext/src/klobuchar.c +313 -0
- pygnss/_c_ext/src/mtable_init.c +50 -0
- pygnss/_c_ext.cpython-314t-darwin.so +0 -0
- pygnss/cl.py +148 -0
- pygnss/constants.py +4 -0
- pygnss/decorator.py +47 -0
- pygnss/file.py +36 -0
- pygnss/filter/__init__.py +77 -0
- pygnss/filter/ekf.py +80 -0
- pygnss/filter/models.py +74 -0
- pygnss/filter/particle.py +484 -0
- pygnss/filter/ukf.py +322 -0
- pygnss/geodetic.py +1177 -0
- pygnss/gnss/__init__.py +0 -0
- pygnss/gnss/edit.py +66 -0
- pygnss/gnss/observables.py +43 -0
- pygnss/gnss/residuals.py +43 -0
- pygnss/gnss/types.py +359 -0
- pygnss/hatanaka.py +70 -0
- pygnss/ionex.py +410 -0
- pygnss/iono/__init__.py +47 -0
- pygnss/iono/chapman.py +35 -0
- pygnss/iono/gim.py +131 -0
- pygnss/logger.py +70 -0
- pygnss/nequick.py +57 -0
- pygnss/orbit/__init__.py +0 -0
- pygnss/orbit/kepler.py +63 -0
- pygnss/orbit/tle.py +186 -0
- pygnss/parsers/rtklib/stats.py +166 -0
- pygnss/rinex.py +2161 -0
- pygnss/sinex.py +121 -0
- pygnss/stats.py +75 -0
- pygnss/tensorial.py +50 -0
- pygnss/time.py +350 -0
- pygnss-2.1.2.dist-info/METADATA +129 -0
- pygnss-2.1.2.dist-info/RECORD +44 -0
- pygnss-2.1.2.dist-info/WHEEL +6 -0
- pygnss-2.1.2.dist-info/entry_points.txt +8 -0
- pygnss-2.1.2.dist-info/licenses/LICENSE +21 -0
- pygnss-2.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
"""pygnss.filter.particle
|
|
2
|
+
=================================
|
|
3
|
+
|
|
4
|
+
Lightweight particle filtering utilities for non-linear / non-Gaussian
|
|
5
|
+
state estimation.
|
|
6
|
+
|
|
7
|
+
This module provides:
|
|
8
|
+
|
|
9
|
+
- ``Particles``: a minimal container for particle states and weights.
|
|
10
|
+
- ``multinomial_resample``: a simple inverse-CDF resampler that returns
|
|
11
|
+
equally weighted copies of drawn particles.
|
|
12
|
+
- ``WeightEstimatorInterface`` / ``WeightEstimatorGaussian``: pluggable
|
|
13
|
+
likelihood estimators that map pre-fit residuals to non-negative weights.
|
|
14
|
+
- ``Filter``: a straightforward sequential Monte Carlo (particle) filter
|
|
15
|
+
that uses a user-supplied ``Model`` and a ``StateHandler`` to consume
|
|
16
|
+
state estimates.
|
|
17
|
+
|
|
18
|
+
Design notes
|
|
19
|
+
-----------
|
|
20
|
+
- User models must implement the ``Model`` contract (``propagate_state`` and
|
|
21
|
+
``to_observations``).
|
|
22
|
+
- Weights passed into resampling functions are assumed to be non-negative;
|
|
23
|
+
the filter normalizes weights internally before sampling.
|
|
24
|
+
- The filter applies a post-resample "roughening" step to mitigate sample
|
|
25
|
+
impoverishment. Per-component roughening standard deviations are available
|
|
26
|
+
on the ``Filter`` instance as ``roughening_sigma_pos`` and
|
|
27
|
+
``roughening_sigma_vel`` (position and velocity components respectively).
|
|
28
|
+
|
|
29
|
+
Examples
|
|
30
|
+
--------
|
|
31
|
+
See ``tests/filter/test_model.py`` for an end-to-end usage example with
|
|
32
|
+
synthetic observations.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from abc import ABC, abstractmethod
|
|
36
|
+
import copy
|
|
37
|
+
from dataclasses import dataclass
|
|
38
|
+
import logging
|
|
39
|
+
from typing import Callable, List, Tuple, Optional
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
from . import Model, State, StateHandler, FilterInterface
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass
|
|
46
|
+
class Particles:
|
|
47
|
+
"""Container holding a set of particles and their associated weights.
|
|
48
|
+
|
|
49
|
+
Parameters
|
|
50
|
+
----------
|
|
51
|
+
particles
|
|
52
|
+
List of candidate states (particles). Each state must be compatible
|
|
53
|
+
with the ``Model`` used by the filter (e.g., a NumPy array).
|
|
54
|
+
weights
|
|
55
|
+
List or array of non-negative weights, one per particle. In typical
|
|
56
|
+
usage these are normalized to sum to 1.
|
|
57
|
+
|
|
58
|
+
Notes
|
|
59
|
+
-----
|
|
60
|
+
The container is intentionally minimal. Helper methods are provided to
|
|
61
|
+
retrieve common summaries (e.g., the maximum-likelihood particle).
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
particles: List[State]
|
|
65
|
+
weights: List[float]
|
|
66
|
+
|
|
67
|
+
def get_max_likely(self) -> State:
|
|
68
|
+
"""Return the particle with the highest weight.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
State
|
|
73
|
+
The particle corresponding to ``argmax(weights)``.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
return self.particles[np.argmax(self.weights)]
|
|
77
|
+
|
|
78
|
+
def __len__(self) -> int:
|
|
79
|
+
"""Return the number of particles contained."""
|
|
80
|
+
|
|
81
|
+
return len(self.particles)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
ResampleFunctionSignature = Callable[
|
|
85
|
+
[List[State], np.ndarray, int], Tuple[List[State], np.ndarray]
|
|
86
|
+
]
|
|
87
|
+
ParticleCallbackSignature = Callable[[List[State], np.ndarray], None]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def void_particle_callback(_particles: List[State], _weights: np.ndarray) -> None:
|
|
91
|
+
"""No-op particle callback.
|
|
92
|
+
|
|
93
|
+
This function implements the ``ParticleCallbackSignature`` and performs no
|
|
94
|
+
action. It is provided as the default callback for ``Filter`` when the
|
|
95
|
+
user does not need any per-epoch particle inspection.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def multinomial_resample(
|
|
100
|
+
particles: List[State], weights: np.ndarray, new_n_particles: int | None = None
|
|
101
|
+
) -> Tuple[List[State], np.ndarray]:
|
|
102
|
+
"""Resample particles via inverse-CDF (multinomial) sampling.
|
|
103
|
+
|
|
104
|
+
The function draws ``new_n_particles`` indices from the discrete
|
|
105
|
+
distribution defined by ``weights`` using sorted uniform thresholds and
|
|
106
|
+
the cumulative distribution function (CDF). Returned particles are deep
|
|
107
|
+
copies of the selected inputs and are assigned equal weights
|
|
108
|
+
``1/new_n_particles``.
|
|
109
|
+
|
|
110
|
+
Notes
|
|
111
|
+
-----
|
|
112
|
+
- Input weights are normalized internally. If the weight sum is zero a
|
|
113
|
+
uniform fallback distribution is used to avoid division by zero.
|
|
114
|
+
- This implementation is simple and clear; for large numbers of
|
|
115
|
+
particles consider using ``numpy.random.choice`` with the
|
|
116
|
+
``p=weights`` argument for optimized sampling.
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
----------
|
|
120
|
+
particles : list[State]
|
|
121
|
+
Candidate particle states.
|
|
122
|
+
weights : numpy.ndarray
|
|
123
|
+
Non-negative weights matching ``particles``. They will be
|
|
124
|
+
normalized before sampling.
|
|
125
|
+
new_n_particles : int, optional
|
|
126
|
+
Number of particles to draw. Defaults to ``len(particles)``.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
(list[State], numpy.ndarray)
|
|
131
|
+
Tuple containing the resampled list of particle states (deep copies)
|
|
132
|
+
and an array of equal weights summing to 1.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
# Normalize weights defensively and build CDF
|
|
136
|
+
w = np.asarray(weights, dtype=float)
|
|
137
|
+
w_sum = float(w.sum())
|
|
138
|
+
if w_sum <= 0:
|
|
139
|
+
# Avoid division by zero; fall back to uniform
|
|
140
|
+
w = np.full_like(w, 1.0 / len(w), dtype=float)
|
|
141
|
+
else:
|
|
142
|
+
w = w / w_sum
|
|
143
|
+
|
|
144
|
+
cumulative_distribution = np.cumsum(w)
|
|
145
|
+
|
|
146
|
+
if new_n_particles is None:
|
|
147
|
+
new_n_particles = len(particles)
|
|
148
|
+
|
|
149
|
+
q = np.sort(np.random.uniform(size=new_n_particles))
|
|
150
|
+
|
|
151
|
+
particle_indices = [int(np.argmax(cumulative_distribution > th)) for th in q]
|
|
152
|
+
|
|
153
|
+
new_particles = [copy.deepcopy(particles[i]) for i in particle_indices]
|
|
154
|
+
|
|
155
|
+
# New weights are set equal to avoid Degenerate particles after resampling
|
|
156
|
+
# As explained in Section 4.2.1 of https://pmc.ncbi.nlm.nih.gov/articles/PMC7826670/pdf/sensors-21-00438.pdf
|
|
157
|
+
new_weights = np.full(new_n_particles, 1.0 / new_n_particles, dtype=float)
|
|
158
|
+
|
|
159
|
+
return new_particles, new_weights
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class WeightEstimatorInterface(ABC):
|
|
163
|
+
"""
|
|
164
|
+
Interface for computing particle weights (likelihoods).
|
|
165
|
+
|
|
166
|
+
Concrete implementations should map a vector of pre-fit residuals to a
|
|
167
|
+
non-negative weight (likelihood). Larger values indicate more plausible
|
|
168
|
+
particles given the observations.
|
|
169
|
+
"""
|
|
170
|
+
|
|
171
|
+
@abstractmethod
|
|
172
|
+
def compute(self, prefits: np.ndarray, **kwargs) -> float:
|
|
173
|
+
"""Compute a non-negative likelihood weight from pre-fit residuals.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
prefits : numpy.ndarray
|
|
178
|
+
Array of pre-fit residuals (measured - modelled observations)
|
|
179
|
+
for the candidate particle.
|
|
180
|
+
**kwargs : dict
|
|
181
|
+
Optional algorithm-specific parameters (for example measurement
|
|
182
|
+
noise statistics) that concrete implementations may accept.
|
|
183
|
+
|
|
184
|
+
Returns
|
|
185
|
+
-------
|
|
186
|
+
float
|
|
187
|
+
A non-negative scalar proportional to the particle's likelihood.
|
|
188
|
+
The filter will normalize these weights across particles before
|
|
189
|
+
resampling.
|
|
190
|
+
"""
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class WeightEstimatorGaussian(WeightEstimatorInterface):
|
|
194
|
+
"""
|
|
195
|
+
Gaussian likelihood weight estimator.
|
|
196
|
+
|
|
197
|
+
The weight is computed as the product of per-measurement Gaussian
|
|
198
|
+
likelihoods assuming i.i.d. errors. A crude bias estimate (the mean
|
|
199
|
+
of the pre-fits) is removed before evaluating the likelihood.
|
|
200
|
+
|
|
201
|
+
Notes
|
|
202
|
+
-----
|
|
203
|
+
The resulting product can suffer from numerical underflow for long
|
|
204
|
+
measurement vectors. In practical applications a log-likelihood sum
|
|
205
|
+
is preferred; this implementation trades numerical robustness for
|
|
206
|
+
simplicity.
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(self):
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
def compute(self, prefits: np.ndarray, **kwargs) -> float:
|
|
213
|
+
r"""Compute a Gaussian product likelihood weight.
|
|
214
|
+
|
|
215
|
+
Parameters
|
|
216
|
+
----------
|
|
217
|
+
prefits : numpy.ndarray
|
|
218
|
+
Vector of pre-fit residuals. A constant bias term is estimated
|
|
219
|
+
and removed before computing the likelihood.
|
|
220
|
+
|
|
221
|
+
Returns
|
|
222
|
+
-------
|
|
223
|
+
float
|
|
224
|
+
Product of per-measurement Gaussian likelihoods.
|
|
225
|
+
|
|
226
|
+
Math
|
|
227
|
+
----
|
|
228
|
+
The weight corresponds to
|
|
229
|
+
|
|
230
|
+
.. math::
|
|
231
|
+
w = \prod_{i=1}^{N} \frac{1}{\sigma \sqrt{2\pi}} \exp\left(-\tfrac{1}{2}\left(\tfrac{x_i-\mu}{\sigma}\right)^2\right)
|
|
232
|
+
|
|
233
|
+
where :math:`x_i` are the bias-corrected pre-fits and :math:`\mu,\sigma`
|
|
234
|
+
are respectively the sample mean and standard deviation of the original
|
|
235
|
+
pre-fits.
|
|
236
|
+
"""
|
|
237
|
+
|
|
238
|
+
# Rough estimation of the hardware bias to remove it from the measurements
|
|
239
|
+
bias = np.average(prefits)
|
|
240
|
+
std = np.std(prefits)
|
|
241
|
+
|
|
242
|
+
# Use Gaussian PDF with mean=bias and sigma=std
|
|
243
|
+
likelihoods = gaussian(prefits, bias, std)
|
|
244
|
+
|
|
245
|
+
weight = np.prod(likelihoods)
|
|
246
|
+
|
|
247
|
+
return weight
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
class Filter(FilterInterface):
|
|
251
|
+
"""Simple particle filter (sequential Monte Carlo).
|
|
252
|
+
|
|
253
|
+
The ``Filter`` implements a basic particle filter algorithm that relies on
|
|
254
|
+
a user-supplied ``Model`` to propagate states and synthesize expected
|
|
255
|
+
observations, a ``WeightEstimatorInterface`` to convert residuals into
|
|
256
|
+
likelihood weights, and a ``StateHandler`` to consume the selected state
|
|
257
|
+
estimate at each epoch.
|
|
258
|
+
|
|
259
|
+
The class performs the standard steps each epoch: propagate particles,
|
|
260
|
+
compute weights from pre-fit residuals, normalize weights, optionally
|
|
261
|
+
resample, apply a configurable post-resample roughening noise, and
|
|
262
|
+
forward the chosen state to the handler.
|
|
263
|
+
|
|
264
|
+
The constructor accepts several optional hooks to customize resampling
|
|
265
|
+
and roughening behavior; see ``__init__`` for parameter details.
|
|
266
|
+
"""
|
|
267
|
+
|
|
268
|
+
def __init__(
|
|
269
|
+
self,
|
|
270
|
+
initial_states: List[State],
|
|
271
|
+
weight_estimator: WeightEstimatorInterface,
|
|
272
|
+
model: Model,
|
|
273
|
+
state_handler: StateHandler,
|
|
274
|
+
resample_threshold: Optional[float] = None,
|
|
275
|
+
resample_function: ResampleFunctionSignature = multinomial_resample,
|
|
276
|
+
particle_callback: ParticleCallbackSignature = void_particle_callback,
|
|
277
|
+
roughening_std: Optional[List[float]] = None,
|
|
278
|
+
logger: Optional[logging.Logger] = None,
|
|
279
|
+
):
|
|
280
|
+
"""Create a Filter instance.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
initial_states : list[State] or numpy.ndarray
|
|
285
|
+
Initial particle set. Can be a list of state arrays or an ndarray
|
|
286
|
+
with shape (N, state_dim). The constructor will convert an
|
|
287
|
+
ndarray into a list of per-particle arrays.
|
|
288
|
+
weight_estimator : WeightEstimatorInterface or type
|
|
289
|
+
Either an instance implementing ``WeightEstimatorInterface`` or
|
|
290
|
+
the class (e.g., ``WeightEstimatorGaussian``). If a class is
|
|
291
|
+
provided the ctor will instantiate it.
|
|
292
|
+
model : Model
|
|
293
|
+
System model providing ``propagate_state`` and ``to_observations``.
|
|
294
|
+
state_handler : StateHandler
|
|
295
|
+
Consumer of the selected state estimate via
|
|
296
|
+
``state_handler.process_state``.
|
|
297
|
+
resample_threshold : float | None, optional
|
|
298
|
+
If provided, resampling will be triggered when the effective
|
|
299
|
+
sample size falls below ``resample_threshold * n_particles``.
|
|
300
|
+
If ``None`` (default) resampling is performed every epoch.
|
|
301
|
+
resample_function : callable, optional
|
|
302
|
+
Resampling function implementing the ``ResampleFunctionSignature``.
|
|
303
|
+
Defaults to ``multinomial_resample``.
|
|
304
|
+
particle_callback : callable, optional
|
|
305
|
+
Callback invoked after weight normalization with signature
|
|
306
|
+
``(particles, weights)`` for visualization or diagnostics.
|
|
307
|
+
roughening_std : list[float] | None, optional
|
|
308
|
+
If provided, a per-state-dimension standard-deviation vector used
|
|
309
|
+
to add zero-mean Gaussian noise to particles after resampling.
|
|
310
|
+
Its length must match the state dimension. If ``None`` no
|
|
311
|
+
roughening is applied.
|
|
312
|
+
logger : logging.Logger | None, optional
|
|
313
|
+
Logger for debug/tracing messages. If ``None`` the module logger
|
|
314
|
+
is used.
|
|
315
|
+
|
|
316
|
+
Notes
|
|
317
|
+
-----
|
|
318
|
+
The constructor accepts either a ``WeightEstimator`` instance or the
|
|
319
|
+
class and will instantiate the latter for backward compatibility.
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
self.particles = initial_states
|
|
323
|
+
|
|
324
|
+
self.weight_estimator = weight_estimator
|
|
325
|
+
self.model = model
|
|
326
|
+
self.state_handler = state_handler
|
|
327
|
+
self._resample_threshold = resample_threshold
|
|
328
|
+
self._resample: ResampleFunctionSignature = resample_function
|
|
329
|
+
self._particle_callback: ParticleCallbackSignature = particle_callback
|
|
330
|
+
self._roughening_std = roughening_std
|
|
331
|
+
self.logger = logger or logging.getLogger(__name__)
|
|
332
|
+
|
|
333
|
+
# Initial set of weights for the particles. Assuming equally weighted
|
|
334
|
+
n_particles = len(self.particles)
|
|
335
|
+
self.weights = np.array([1.0 / n_particles] * n_particles)
|
|
336
|
+
|
|
337
|
+
# Check that the roughening std is compatible with the state dimension
|
|
338
|
+
if self._roughening_std is not None:
|
|
339
|
+
state_dim = len(self.particles[0])
|
|
340
|
+
if len(self._roughening_std) != state_dim:
|
|
341
|
+
raise ValueError(
|
|
342
|
+
f"Roughening std length {len(self._roughening_std)} does not match "
|
|
343
|
+
f"state dimension {state_dim}."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
def process(self, y_k: np.ndarray, R: np.ndarray, **kwargs):
|
|
347
|
+
"""Process one epoch: propagate, weight, resample, roughen, and report.
|
|
348
|
+
|
|
349
|
+
Performs the sequential Monte Carlo steps for a single observation
|
|
350
|
+
epoch:
|
|
351
|
+
|
|
352
|
+
1. Propagate particles using ``model.propagate_state`` (time update).
|
|
353
|
+
2. Compute pre-fit residuals and map them to weights using
|
|
354
|
+
``weight_estimator.compute``.
|
|
355
|
+
3. Normalize weights and invoke ``particle_callback`` for monitoring
|
|
356
|
+
or visualization.
|
|
357
|
+
4. Resample particles when needed and apply post-resample roughening
|
|
358
|
+
noise (if configured) to mitigate sample impoverishment.
|
|
359
|
+
5. Compute the selected state estimate and forward it to
|
|
360
|
+
``state_handler.process_state`` along with diagnostics.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
y_k : numpy.ndarray
|
|
365
|
+
Observation vector at the current epoch.
|
|
366
|
+
R : numpy.ndarray
|
|
367
|
+
Measurement noise covariance matrix (forwarded via ``**kwargs``
|
|
368
|
+
to estimators that may need it).
|
|
369
|
+
**kwargs : dict
|
|
370
|
+
Additional keyword arguments forwarded to ``model.to_observations``
|
|
371
|
+
and to ``weight_estimator.compute``.
|
|
372
|
+
|
|
373
|
+
Notes
|
|
374
|
+
-----
|
|
375
|
+
The method updates ``self.particles`` and ``self.weights`` in-place
|
|
376
|
+
(resampling replaces the particle set). The selected state that is
|
|
377
|
+
passed to ``state_handler`` is produced by ``_get_solution`` (by
|
|
378
|
+
default the maximum-weight particle).
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
# Time update ----------------------------------------------------------
|
|
382
|
+
particles = [
|
|
383
|
+
self.model.propagate_state(particle) for particle in self.particles
|
|
384
|
+
]
|
|
385
|
+
|
|
386
|
+
for i, particle in enumerate(particles):
|
|
387
|
+
prefits = y_k - self.model.to_observations(particle).y_m
|
|
388
|
+
self.weights[i] = self.weight_estimator.compute(prefits, **kwargs)
|
|
389
|
+
|
|
390
|
+
# Normalize the weights
|
|
391
|
+
self.weights = self.weights / sum(self.weights)
|
|
392
|
+
|
|
393
|
+
self._particle_callback(particles, self.weights, **kwargs)
|
|
394
|
+
|
|
395
|
+
# Commit the propagated particle set before resampling
|
|
396
|
+
# Resampling only if the effective number of particles is low (Algorithm 3 of
|
|
397
|
+
# https://pmc.ncbi.nlm.nih.gov/articles/PMC7826670/pdf/sensors-21-00438.pdf)
|
|
398
|
+
if self._needs_resample(self.weights):
|
|
399
|
+
self.particles, self.weights = self._resample(
|
|
400
|
+
particles, self.weights, len(particles)
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Perform particle "roughening" if need be
|
|
404
|
+
n_particles = len(self.particles)
|
|
405
|
+
noise = np.zeros((n_particles, len(self.particles[0])))
|
|
406
|
+
|
|
407
|
+
if self._roughening_std is not None:
|
|
408
|
+
for dim in range(len(self.particles[0])):
|
|
409
|
+
noise[:, dim] = np.random.normal(
|
|
410
|
+
0.0, self._roughening_std[dim], size=(n_particles,)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
self.particles = self.particles + noise
|
|
414
|
+
|
|
415
|
+
# Compute postfit residuals
|
|
416
|
+
x = self._get_solution()
|
|
417
|
+
|
|
418
|
+
r = y_k - self.model.to_observations(x, **kwargs).y_m
|
|
419
|
+
|
|
420
|
+
self.state_handler.process_state(x, np.eye(len(x)), postfits=r, **kwargs)
|
|
421
|
+
|
|
422
|
+
def _needs_resample(self, weights: np.ndarray) -> bool:
|
|
423
|
+
"""Determine if resampling is needed based on the effective number of particles.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
weights : numpy.ndarray
|
|
428
|
+
Array of particle weights.
|
|
429
|
+
threshold : float
|
|
430
|
+
Resampling threshold as a fraction of the total number of particles.
|
|
431
|
+
Default is 0.25.
|
|
432
|
+
Returns
|
|
433
|
+
-------
|
|
434
|
+
bool
|
|
435
|
+
True if resampling is needed, False otherwise.
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
if self._resample_threshold is None:
|
|
439
|
+
return True
|
|
440
|
+
|
|
441
|
+
else:
|
|
442
|
+
n_particles = len(weights)
|
|
443
|
+
effective_n = 1.0 / np.sum(np.square(weights))
|
|
444
|
+
return effective_n < self._resample_threshold * n_particles
|
|
445
|
+
|
|
446
|
+
def _get_solution(self) -> State:
|
|
447
|
+
"""Return the current state estimate from the particle set.
|
|
448
|
+
|
|
449
|
+
The default implementation returns the maximum-weight particle.
|
|
450
|
+
Alternative selection strategies can be implemented if needed.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
State
|
|
455
|
+
The selected state estimate.
|
|
456
|
+
"""
|
|
457
|
+
|
|
458
|
+
return self.particles[np.argmax(self.weights)]
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def gaussian(x, mu, sigma):
|
|
462
|
+
"""
|
|
463
|
+
Evaluates a Gaussian function (normal distribution).
|
|
464
|
+
|
|
465
|
+
:param x: The input value(s) (scalar or NumPy array).
|
|
466
|
+
:type x: float or numpy.ndarray
|
|
467
|
+
:param mu: The mean (center) of the Gaussian.
|
|
468
|
+
:type mu: float
|
|
469
|
+
:param sigma: The standard deviation (width) of the Gaussian.
|
|
470
|
+
:type sigma: float
|
|
471
|
+
:return: The value(s) of the Gaussian function at x.
|
|
472
|
+
:rtype: float or numpy.ndarray
|
|
473
|
+
|
|
474
|
+
Example
|
|
475
|
+
-------
|
|
476
|
+
>>> import numpy as np
|
|
477
|
+
>>> mean = 0
|
|
478
|
+
>>> std_dev = 1
|
|
479
|
+
>>> x_values = np.linspace(-3, 3, 100)
|
|
480
|
+
>>> y_values = gaussian(x_values, mean, std_dev)
|
|
481
|
+
>>> print(y_values[0:5])
|
|
482
|
+
[0.00443185 0.00530579 0.00632878 0.00752133 0.00890582]
|
|
483
|
+
"""
|
|
484
|
+
return (1.0 / (sigma * np.sqrt(2 * np.pi))) * np.exp(-0.5 * ((x - mu) / sigma) ** 2)
|