blobmodel 2.0.0__tar.gz → 2.1.0__tar.gz
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.
- {blobmodel-2.0.0 → blobmodel-2.1.0}/PKG-INFO +3 -1
- {blobmodel-2.0.0 → blobmodel-2.1.0}/README.md +2 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/blobs.py +72 -7
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/model.py +130 -18
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/stochasticality.py +25 -3
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/PKG-INFO +3 -1
- blobmodel-2.1.0/blobmodel.egg-info/SOURCES.txt +36 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/pyproject.toml +1 -1
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_docs.py +27 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_docs_scripts.py +1 -0
- blobmodel-2.1.0/tests/test_lifetime.py +521 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_speed_up.py +20 -3
- blobmodel-2.0.0/.coveragerc +0 -8
- blobmodel-2.0.0/.github/workflows/draft-pdf.yml +0 -27
- blobmodel-2.0.0/.github/workflows/workflow.yml +0 -46
- blobmodel-2.0.0/.gitignore +0 -148
- blobmodel-2.0.0/.readthedocs.yml +0 -34
- blobmodel-2.0.0/blobmodel.egg-info/SOURCES.txt +0 -89
- blobmodel-2.0.0/docs/1d_animation.gif +0 -0
- blobmodel-2.0.0/docs/2-sided_pulse_shape.png +0 -0
- blobmodel-2.0.0/docs/2d_animation.gif +0 -0
- blobmodel-2.0.0/docs/alignment_false.gif +0 -0
- blobmodel-2.0.0/docs/alignment_true.gif +0 -0
- blobmodel-2.0.0/docs/blob_factory.rst +0 -143
- blobmodel-2.0.0/docs/blob_labels.rst +0 -25
- blobmodel-2.0.0/docs/blob_shapes.rst +0 -90
- blobmodel-2.0.0/docs/blob_tilt.rst +0 -39
- blobmodel-2.0.0/docs/blobmodel.rst +0 -61
- blobmodel-2.0.0/docs/change_t_drain_plot.png +0 -0
- blobmodel-2.0.0/docs/changing_t_drain_plot.py +0 -45
- blobmodel-2.0.0/docs/conf.py +0 -37
- blobmodel-2.0.0/docs/contributor_guide.rst +0 -103
- blobmodel-2.0.0/docs/create_logo.py +0 -79
- blobmodel-2.0.0/docs/drainage_time.rst +0 -20
- blobmodel-2.0.0/docs/getting_started.rst +0 -69
- blobmodel-2.0.0/docs/index.rst +0 -85
- blobmodel-2.0.0/docs/install.rst +0 -25
- blobmodel-2.0.0/docs/labels_labels.png +0 -0
- blobmodel-2.0.0/docs/labels_n.png +0 -0
- blobmodel-2.0.0/docs/logo.png +0 -0
- blobmodel-2.0.0/docs/model_geometry.rst +0 -52
- blobmodel-2.0.0/docs/modules.rst +0 -4
- blobmodel-2.0.0/docs/one_dim.rst +0 -25
- blobmodel-2.0.0/docs/plot_pulses.py +0 -33
- blobmodel-2.0.0/docs/pulse_shapes.png +0 -0
- blobmodel-2.0.0/docs/visualize_model.rst +0 -31
- blobmodel-2.0.0/docs/xarray_example.png +0 -0
- blobmodel-2.0.0/docs/y-periodicity.gif +0 -0
- blobmodel-2.0.0/examples/1d_animation.py +0 -24
- blobmodel-2.0.0/examples/2_sided_exp_pulse.py +0 -27
- blobmodel-2.0.0/examples/2d_animation.py +0 -15
- blobmodel-2.0.0/examples/analyse_output.py +0 -23
- blobmodel-2.0.0/examples/blob_tilting.py +0 -77
- blobmodel-2.0.0/examples/changing_t_drain.py +0 -43
- blobmodel-2.0.0/examples/compare_to_analytical_sol.py +0 -41
- blobmodel-2.0.0/examples/custom_blobfactory.py +0 -79
- blobmodel-2.0.0/examples/point_process.py +0 -122
- blobmodel-2.0.0/examples/show_labels.py +0 -21
- blobmodel-2.0.0/examples/single_blob.py +0 -18
- blobmodel-2.0.0/paper/paper.bib +0 -213
- blobmodel-2.0.0/paper/paper.md +0 -140
- blobmodel-2.0.0/readme_files/1d_blobs.gif +0 -0
- blobmodel-2.0.0/readme_files/2d_blobs.gif +0 -0
- blobmodel-2.0.0/readme_files/logo.png +0 -0
- blobmodel-2.0.0/readme_files/tmp5kfvrjp5.svg +0 -1
- blobmodel-2.0.0/readme_files/tmpp0mi4n8p.svg +0 -1
- blobmodel-2.0.0/tests/.gitignore +0 -139
- {blobmodel-2.0.0 → blobmodel-2.1.0}/LICENSE +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/__init__.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/blob_shape.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/distributions.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/geometry.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/plotting.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/dependency_links.txt +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/requires.txt +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/top_level.txt +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/setup.cfg +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_analytical.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_blob.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_blob_factories.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_changing_t_drain.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_drain.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_geometry.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_imaging_layout.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_input_validation.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_labels.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_one_dim.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_pulse_shape.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_seed.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_show_model.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_stochasticality.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_str.py +0 -0
- {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_vx_or_vy=0.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blobmodel
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Two dimensional model of propagating blobs
|
|
5
5
|
Author-email: gregordecristoforo <gregor.decristoforo@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -131,6 +131,8 @@ bm = Model(
|
|
|
131
131
|
```
|
|
132
132
|
Alternatively, you can specify all blob parameters exactly as you want by writing your own class which inherits from BlobFactory. See `examples/custom_blobfactory.py` as an example or take a look at the [blobmodel documentation](https://blobmodel.readthedocs.io/en/latest/?badge=latest).
|
|
133
133
|
|
|
134
|
+
Blobs can decay exponentially from their initial time with a drainage time `t_drain`, and/or be given a finite pulse lifetime `t_lifetime`, a Gaussian envelope symmetric about the blob's `t_init` so that the blob grows, peaks and decays. Both are arguments of `DefaultBlobFactory` and are applied to every sampled blob; see the [drainage time](https://blobmodel.readthedocs.io/en/latest/drainage_time.html) and [pulse lifetime](https://blobmodel.readthedocs.io/en/latest/pulse_lifetime.html) sections of the documentation.
|
|
135
|
+
|
|
134
136
|
## Contributing
|
|
135
137
|
|
|
136
138
|
Feel free to raise issues about anything. Contributions through pull requests are also very welcome. Please take a look at our [Contributor guide](https://blobmodel.readthedocs.io/en/latest/contributor_guide.html) for further details.
|
|
@@ -85,6 +85,8 @@ bm = Model(
|
|
|
85
85
|
```
|
|
86
86
|
Alternatively, you can specify all blob parameters exactly as you want by writing your own class which inherits from BlobFactory. See `examples/custom_blobfactory.py` as an example or take a look at the [blobmodel documentation](https://blobmodel.readthedocs.io/en/latest/?badge=latest).
|
|
87
87
|
|
|
88
|
+
Blobs can decay exponentially from their initial time with a drainage time `t_drain`, and/or be given a finite pulse lifetime `t_lifetime`, a Gaussian envelope symmetric about the blob's `t_init` so that the blob grows, peaks and decays. Both are arguments of `DefaultBlobFactory` and are applied to every sampled blob; see the [drainage time](https://blobmodel.readthedocs.io/en/latest/drainage_time.html) and [pulse lifetime](https://blobmodel.readthedocs.io/en/latest/pulse_lifetime.html) sections of the documentation.
|
|
89
|
+
|
|
88
90
|
## Contributing
|
|
89
91
|
|
|
90
92
|
Feel free to raise issues about anything. Contributions through pull requests are also very welcome. Please take a look at our [Contributor guide](https://blobmodel.readthedocs.io/en/latest/contributor_guide.html) for further details.
|
|
@@ -13,7 +13,7 @@ class Blob:
|
|
|
13
13
|
the function `discretize_blob`. The contribution of a single blob to a grid defined by `x`, `y` and `t` is given by:
|
|
14
14
|
|
|
15
15
|
.. math::
|
|
16
|
-
a e^{-(t-t_k)/\\tau_\\shortparallel}\\varphi\\left( \\frac{(x-x_k)-v(t-t_k)}{\ell_x}, \\frac{(y-y_k)-w(t-t_k)}{\ell_y} \\right)
|
|
16
|
+
a e^{-(t-t_k)/\\tau_\\shortparallel} e^{-\\left((t-t_k)/\\tau_\\mathrm{d}\\right)^2}\\varphi\\left( \\frac{(x-x_k)-v(t-t_k)}{\ell_x}, \\frac{(y-y_k)-w(t-t_k)}{\ell_y} \\right)
|
|
17
17
|
|
|
18
18
|
Where:
|
|
19
19
|
- :math:`a` is the blob amplitude, `amplitude`.
|
|
@@ -26,11 +26,23 @@ class Blob:
|
|
|
26
26
|
- :math:`t_k` is the time at which the blob is seeded at
|
|
27
27
|
:math:`(x_k, y_k)`, `t_init`.
|
|
28
28
|
- :math:`\\tau_\\shortparallel` is the drainage time, `t_drain`.
|
|
29
|
+
- :math:`\\tau_\\mathrm{d}` is the pulse lifetime, `t_lifetime`. The
|
|
30
|
+
corresponding factor is absent (equal to 1) when `t_lifetime` is
|
|
31
|
+
None, which is the default.
|
|
29
32
|
- :math:`\\varphi` is the blob pulse shape, `blob_shape`.
|
|
30
33
|
|
|
34
|
+
The two temporal factors are different: `t_drain` is a one-sided
|
|
35
|
+
exponential decay *from* :math:`t_k`, growing without bound backwards in
|
|
36
|
+
time, while `t_lifetime` is a Gaussian envelope *symmetric about*
|
|
37
|
+
:math:`t_k`. They are independent and multiply. When a lifetime is set and
|
|
38
|
+
`t_drain` is infinite, :math:`t_k` is the time of maximum amplitude.
|
|
39
|
+
|
|
31
40
|
Additionally, a tilt angle can be provided through `theta`, in which case
|
|
32
41
|
the two arguments of :math:`\\varphi` are rotated by `theta` around the
|
|
33
42
|
blob center.
|
|
43
|
+
|
|
44
|
+
.. versionadded:: 2.1.0
|
|
45
|
+
The `t_lifetime` envelope.
|
|
34
46
|
"""
|
|
35
47
|
|
|
36
48
|
def __init__(
|
|
@@ -50,6 +62,7 @@ class Blob:
|
|
|
50
62
|
shape_parameters_s: Union[dict, None] = None,
|
|
51
63
|
blob_alignment: bool = False,
|
|
52
64
|
theta: Optional[float] = None,
|
|
65
|
+
t_lifetime: Optional[float] = None,
|
|
53
66
|
) -> None:
|
|
54
67
|
"""
|
|
55
68
|
Initialize a single blob.
|
|
@@ -97,14 +110,24 @@ class Blob:
|
|
|
97
110
|
vector). If not None, this angle is used directly and ``blob_alignment``
|
|
98
111
|
is ignored. If None (the default), the angle is determined by
|
|
99
112
|
``blob_alignment``: the velocity phase when it is True, or 0 when False.
|
|
113
|
+
t_lifetime : float, optional
|
|
114
|
+
Pulse lifetime: the blob is multiplied by the Gaussian envelope
|
|
115
|
+
``exp(-((t - t_init) / t_lifetime)**2)``, symmetric about
|
|
116
|
+
``t_init``. Scalar only — unlike ``t_drain`` there is no per-x
|
|
117
|
+
array form. Default None = no envelope; ``np.inf`` is accepted and
|
|
118
|
+
is equivalent to None. Keyword-only in practice: it is last in the
|
|
119
|
+
signature so that positional callers are unaffected.
|
|
120
|
+
|
|
121
|
+
.. versionadded:: 2.1.0
|
|
100
122
|
|
|
101
123
|
Raises
|
|
102
124
|
------
|
|
103
125
|
TypeError
|
|
104
126
|
If ``blob_shape`` is not an ``AbstractBlobShape`` instance.
|
|
105
127
|
ValueError
|
|
106
|
-
If ``width_p`` or ``width_s`` is not positive,
|
|
107
|
-
is not positive (every element, when it is an array)
|
|
128
|
+
If ``width_p`` or ``width_s`` is not positive, if ``t_drain``
|
|
129
|
+
is not positive (every element, when it is an array), or if
|
|
130
|
+
``t_lifetime`` is given and is not positive.
|
|
108
131
|
|
|
109
132
|
"""
|
|
110
133
|
if blob_shape is None:
|
|
@@ -119,6 +142,10 @@ class Blob:
|
|
|
119
142
|
)
|
|
120
143
|
if np.any(np.asarray(t_drain) <= 0):
|
|
121
144
|
raise ValueError(f"t_drain must be positive, got t_drain = {t_drain}.")
|
|
145
|
+
if t_lifetime is not None and not t_lifetime > 0:
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"t_lifetime must be positive, got t_lifetime = {t_lifetime}."
|
|
148
|
+
)
|
|
122
149
|
|
|
123
150
|
self.blob_id = blob_id
|
|
124
151
|
self.blob_shape = blob_shape
|
|
@@ -138,6 +165,9 @@ class Blob:
|
|
|
138
165
|
if np.ndim(t_drain) == 0
|
|
139
166
|
else np.asarray(t_drain, dtype=np.float64)
|
|
140
167
|
)
|
|
168
|
+
self.t_lifetime: Optional[float] = (
|
|
169
|
+
None if t_lifetime is None else float(t_lifetime)
|
|
170
|
+
)
|
|
141
171
|
self.shape_parameters_p = (
|
|
142
172
|
{} if shape_parameters_p is None else shape_parameters_p
|
|
143
173
|
)
|
|
@@ -318,9 +348,46 @@ class Blob:
|
|
|
318
348
|
)
|
|
319
349
|
|
|
320
350
|
return (
|
|
321
|
-
self.amplitude
|
|
351
|
+
self.amplitude
|
|
352
|
+
* self._temporal_factor(t)
|
|
353
|
+
* primary_axis_shape
|
|
354
|
+
* secondary_axis_shape
|
|
322
355
|
)
|
|
323
356
|
|
|
357
|
+
def _temporal_factor(self, t: Union[int, NDArray]) -> NDArray:
|
|
358
|
+
"""
|
|
359
|
+
Combined drain and lifetime factor.
|
|
360
|
+
|
|
361
|
+
Without a lifetime this is exactly `_drain`. With one, the drain and
|
|
362
|
+
envelope exponents are summed before exponentiating: evaluated
|
|
363
|
+
separately, far from `t_init` the drain overflows while the envelope
|
|
364
|
+
underflows, giving ``inf * 0 = nan`` instead of a negligible value.
|
|
365
|
+
|
|
366
|
+
Parameters
|
|
367
|
+
----------
|
|
368
|
+
t : NDArray
|
|
369
|
+
Time coordinates.
|
|
370
|
+
|
|
371
|
+
Returns
|
|
372
|
+
-------
|
|
373
|
+
temporal_factor : NDArray
|
|
374
|
+
``exp(-(t - t_init)/t_drain - ((t - t_init)/t_lifetime)**2)``.
|
|
375
|
+
|
|
376
|
+
"""
|
|
377
|
+
if self.t_lifetime is None:
|
|
378
|
+
return self._drain(t)
|
|
379
|
+
return np.exp(
|
|
380
|
+
self._drain_exponent(t) - ((t - self.t_init) / self.t_lifetime) ** 2
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
def _drain_exponent(self, t: Union[int, NDArray]) -> Any:
|
|
384
|
+
"""
|
|
385
|
+
Exponent of the drain factor, ``-(t - t_init) / t_drain``.
|
|
386
|
+
"""
|
|
387
|
+
if isinstance(self.t_drain, np.ndarray):
|
|
388
|
+
return -(t - self.t_init) / self.t_drain[np.newaxis, :, np.newaxis]
|
|
389
|
+
return -(t - self.t_init) / self.t_drain
|
|
390
|
+
|
|
324
391
|
def _drain(self, t: Union[int, NDArray]) -> NDArray:
|
|
325
392
|
"""
|
|
326
393
|
Calculate the drain factor for the blob.
|
|
@@ -336,9 +403,7 @@ class Blob:
|
|
|
336
403
|
Drain factor.
|
|
337
404
|
|
|
338
405
|
"""
|
|
339
|
-
|
|
340
|
-
return np.exp(-(t - self.t_init) / self.t_drain[np.newaxis, :, np.newaxis])
|
|
341
|
-
return np.exp(-(t - self.t_init) / self.t_drain)
|
|
406
|
+
return np.exp(self._drain_exponent(t))
|
|
342
407
|
|
|
343
408
|
def _blob_trajectory_x(self, t: Union[int, NDArray]) -> Any:
|
|
344
409
|
"""
|
|
@@ -474,29 +474,141 @@ class Model:
|
|
|
474
474
|
-------
|
|
475
475
|
Tuple[int, int]
|
|
476
476
|
Start and stop indices.
|
|
477
|
+
|
|
478
|
+
Notes
|
|
479
|
+
-----
|
|
480
|
+
- Two independent windows are intersected: the one derived from the
|
|
481
|
+
blob crossing the domain (only available when ``v_x != 0``) and,
|
|
482
|
+
for a blob with a finite `t_lifetime`, the one derived from the
|
|
483
|
+
support of its temporal envelope. Either may be absent, in which
|
|
484
|
+
case the other is used alone; with neither the whole time axis is
|
|
485
|
+
returned.
|
|
477
486
|
"""
|
|
478
|
-
if not speed_up
|
|
487
|
+
if not speed_up:
|
|
479
488
|
return 0, self._geometry.t.size
|
|
480
489
|
|
|
490
|
+
n_t = self._geometry.t.size
|
|
491
|
+
start, stop = 0, n_t
|
|
481
492
|
dt, t0 = self._geometry.dt, self._geometry.t[0]
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
493
|
+
# Bound on |blob contribution| / (temporal factor): every shape peaks
|
|
494
|
+
# at or below 1, and periodic-y discretization sums three copies.
|
|
495
|
+
copies = 3 if self._geometry.periodic_y and not self._one_dimensional else 1
|
|
496
|
+
peak = copies * abs(blob.amplitude)
|
|
497
|
+
|
|
498
|
+
if blob.t_lifetime is not None and np.isfinite(blob.t_lifetime):
|
|
499
|
+
# The envelope is symmetric about t_init, so the window is too.
|
|
500
|
+
half_width = self._lifetime_half_width(blob, truncation_error, peak)
|
|
501
|
+
idx_peak = (blob.t_init - t0) / dt
|
|
502
|
+
margin = half_width / dt
|
|
503
|
+
start = max(start, int(np.clip(np.floor(idx_peak - margin), 0, n_t)))
|
|
504
|
+
stop = min(stop, int(np.clip(np.ceil(idx_peak + margin), 0, n_t)))
|
|
505
|
+
|
|
506
|
+
if blob.v_x != 0:
|
|
507
|
+
idx_x0 = (blob.t_init - t0) / dt + (self._geometry.x[0] - blob.pos_x0) / (
|
|
508
|
+
blob.v_x * dt
|
|
509
|
+
)
|
|
510
|
+
idx_Lx = idx_x0 + self._geometry.Lx / (blob.v_x * dt)
|
|
511
|
+
# Decay length of the blob along the x-axis: the blob-frame widths
|
|
512
|
+
# projected onto x through the tilt angle (width_p for an untilted
|
|
513
|
+
# blob, width_s at theta = pi/2).
|
|
514
|
+
width_x = (
|
|
515
|
+
np.abs(np.cos(blob.theta)) * blob.width_p
|
|
516
|
+
+ np.abs(np.sin(blob.theta)) * blob.width_s
|
|
517
|
+
)
|
|
518
|
+
margin = (
|
|
519
|
+
width_x
|
|
520
|
+
* (self._log_temporal_gain(blob, peak) - np.log(truncation_error))
|
|
521
|
+
/ np.abs(blob.v_x * dt)
|
|
522
|
+
)
|
|
523
|
+
start = max(start, int(np.clip(min(idx_x0, idx_Lx) - margin, 0, n_t)))
|
|
524
|
+
stop = min(stop, int(np.clip(max(idx_x0, idx_Lx) + margin, 0, n_t)))
|
|
525
|
+
|
|
526
|
+
# The two windows may not overlap at all: collapse rather than invert.
|
|
527
|
+
return start, max(start, stop)
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def _log_temporal_gain(blob: Blob, peak: float) -> float:
|
|
531
|
+
r"""
|
|
532
|
+
Log of a bound on how much the crossing-window margin must widen.
|
|
533
|
+
|
|
534
|
+
The crossing margin assumes the blob's contribution is at most its
|
|
535
|
+
spatial shape. With a finite `t_lifetime` the temporal factor
|
|
536
|
+
``exp(-dt / t_drain - (dt / tau_d)**2)`` peaks at
|
|
537
|
+
``exp(tau_d**2 / (4 t_drain**2))`` (before `t_init`), and the amplitude
|
|
538
|
+
and ghost copies scale the field too, so the log of `peak` times that
|
|
539
|
+
temporal peak is added to the margin's log-threshold. Clamped at 0 so the margin
|
|
540
|
+
never shrinks below the lifetime-free one. Without a (finite)
|
|
541
|
+
lifetime this returns 0, leaving the pre-existing window unchanged.
|
|
542
|
+
|
|
543
|
+
Parameters
|
|
544
|
+
----------
|
|
545
|
+
blob : Blob
|
|
546
|
+
Blob object.
|
|
547
|
+
peak : float
|
|
548
|
+
Bound on the blob's contribution divided by its temporal factor
|
|
549
|
+
(``|amplitude|`` times the number of periodic-y copies).
|
|
498
550
|
|
|
499
|
-
|
|
551
|
+
Returns
|
|
552
|
+
-------
|
|
553
|
+
float
|
|
554
|
+
Non-negative log-gain.
|
|
555
|
+
"""
|
|
556
|
+
tau_d = blob.t_lifetime
|
|
557
|
+
if tau_d is None or not np.isfinite(tau_d) or peak == 0:
|
|
558
|
+
return 0.0
|
|
559
|
+
t_drain = np.min(blob.t_drain)
|
|
560
|
+
peak_exponent = 0.0 if np.isinf(t_drain) else tau_d**2 / (4 * t_drain**2)
|
|
561
|
+
return float(max(np.log(peak) + peak_exponent, 0.0))
|
|
562
|
+
|
|
563
|
+
@staticmethod
|
|
564
|
+
def _lifetime_half_width(blob: Blob, truncation_error: float, peak: float) -> float:
|
|
565
|
+
r"""
|
|
566
|
+
Half-width in time of the support of a blob's lifetime envelope.
|
|
567
|
+
|
|
568
|
+
The envelope ``exp(-(dt / tau_d)**2)`` alone drops below
|
|
569
|
+
`truncation_error` for ``|dt| >= tau_d * sqrt(-log(error))``. A finite
|
|
570
|
+
`t_drain` makes the drain factor *grow* backwards in time, so the
|
|
571
|
+
product ``exp(-(dt / tau_d)**2 + dt / t_drain)`` is the binding
|
|
572
|
+
constraint; it falls below `truncation_error` for
|
|
573
|
+
|
|
574
|
+
.. math::
|
|
575
|
+
\Delta \geq b + \sqrt{b^2 - \tau_d^2 \log \varepsilon},
|
|
576
|
+
\quad b = \tau_d^2 / (2 \tau_\text{drain}),
|
|
577
|
+
|
|
578
|
+
which reduces to the envelope-only bound at ``t_drain = inf``. The
|
|
579
|
+
smallest element is used for an array-valued `t_drain` (smallest drain
|
|
580
|
+
time = widest window = the conservative choice).
|
|
581
|
+
|
|
582
|
+
`truncation_error` bounds the field itself, so ``epsilon`` is
|
|
583
|
+
``truncation_error / peak``: every blob shape peaks at or below 1 in
|
|
584
|
+
magnitude and periodic-y discretization sums three copies, hence
|
|
585
|
+
``peak * envelope * drain`` bounds the blob's contribution.
|
|
586
|
+
|
|
587
|
+
Parameters
|
|
588
|
+
----------
|
|
589
|
+
blob : Blob
|
|
590
|
+
Blob with a non-None `t_lifetime`.
|
|
591
|
+
truncation_error : float
|
|
592
|
+
Amplitude below which the blob is truncated.
|
|
593
|
+
peak : float
|
|
594
|
+
Bound on the blob's contribution divided by its temporal factor
|
|
595
|
+
(``|amplitude|`` times the number of periodic-y copies).
|
|
596
|
+
|
|
597
|
+
Returns
|
|
598
|
+
-------
|
|
599
|
+
float
|
|
600
|
+
Half-width in time units; `np.inf` for an infinite `t_lifetime`.
|
|
601
|
+
"""
|
|
602
|
+
tau_d = blob.t_lifetime
|
|
603
|
+
if tau_d is None or not np.isfinite(tau_d):
|
|
604
|
+
return np.inf
|
|
605
|
+
if peak == 0:
|
|
606
|
+
return 0.0
|
|
607
|
+
# Clamped so that an error >= 1 (nothing to keep) cannot produce a nan.
|
|
608
|
+
neg_log_error = max(-np.log(truncation_error / peak), 0.0)
|
|
609
|
+
t_drain = np.min(blob.t_drain)
|
|
610
|
+
b = 0.0 if np.isinf(t_drain) else tau_d**2 / (2 * t_drain)
|
|
611
|
+
return float(b + np.sqrt(b**2 + tau_d**2 * neg_log_error))
|
|
500
612
|
|
|
501
613
|
def _reset_fields(self):
|
|
502
614
|
"""Reset the density and labels fields."""
|
|
@@ -183,6 +183,7 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
183
183
|
"ws": (DistributionEnum.deg, 1.0),
|
|
184
184
|
"vx": (DistributionEnum.deg, 1.0),
|
|
185
185
|
"vy": (DistributionEnum.zeros, 0.0),
|
|
186
|
+
"posx": (DistributionEnum.zeros, 0.0),
|
|
186
187
|
"spp": (DistributionEnum.deg, 0.5),
|
|
187
188
|
"sps": (DistributionEnum.deg, 0.5),
|
|
188
189
|
}
|
|
@@ -192,6 +193,7 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
192
193
|
t_drain: Union[float, NDArray, int] = np.inf,
|
|
193
194
|
blob_alignment: bool = False,
|
|
194
195
|
seed: Union[int, np.random.Generator, None] = None,
|
|
196
|
+
t_lifetime: Union[float, None] = None,
|
|
195
197
|
) -> None:
|
|
196
198
|
"""
|
|
197
199
|
Default implementation of BlobFactory.
|
|
@@ -225,14 +227,26 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
225
227
|
By default None, i.e. a freshly seeded generator (non-reproducible).
|
|
226
228
|
Note that a seed passed to `Model` takes precedence: it replaces
|
|
227
229
|
this factory's generator via `set_rng`.
|
|
230
|
+
t_lifetime : float, optional
|
|
231
|
+
Pulse lifetime of the blobs: every sampled blob is multiplied by
|
|
232
|
+
the Gaussian envelope ``exp(-((t - t_init) / t_lifetime)**2)``,
|
|
233
|
+
symmetric about its own `t_init`. Scalar only. By default None,
|
|
234
|
+
i.e. no envelope. Last in the signature so that positional callers
|
|
235
|
+
are unaffected.
|
|
236
|
+
|
|
237
|
+
.. versionadded:: 2.1.0
|
|
228
238
|
|
|
229
239
|
Raises
|
|
230
240
|
------
|
|
231
241
|
ValueError
|
|
232
|
-
If `t_drain` is not positive.
|
|
242
|
+
If `t_drain` or `t_lifetime` is not positive.
|
|
233
243
|
"""
|
|
234
244
|
if np.any(np.asarray(t_drain) <= 0):
|
|
235
245
|
raise ValueError(f"t_drain must be positive, got t_drain = {t_drain}.")
|
|
246
|
+
if t_lifetime is not None and not t_lifetime > 0:
|
|
247
|
+
raise ValueError(
|
|
248
|
+
f"t_lifetime must be positive, got t_lifetime = {t_lifetime}."
|
|
249
|
+
)
|
|
236
250
|
|
|
237
251
|
# Per-parameter distribution (None for custom callables) and sampler.
|
|
238
252
|
self._dists: dict = {}
|
|
@@ -242,6 +256,7 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
242
256
|
self.set_sampler(parameter, dist, free_parameter)
|
|
243
257
|
|
|
244
258
|
self.t_drain = t_drain
|
|
259
|
+
self.t_lifetime = None if t_lifetime is None else float(t_lifetime)
|
|
245
260
|
self.blob_alignment = blob_alignment
|
|
246
261
|
self.theta_setter: Union[Callable[[], float], None] = None
|
|
247
262
|
self.rng = np.random.default_rng(seed)
|
|
@@ -264,6 +279,11 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
264
279
|
- "ws": width in the secondary (perpendicular) blob direction
|
|
265
280
|
- "vx": velocity in the x-direction
|
|
266
281
|
- "vy": velocity in the y-direction
|
|
282
|
+
- "posx": blob position in the x-direction at ``t_init``
|
|
283
|
+
(`DistributionEnum.zeros` by default, i.e. every blob is seeded
|
|
284
|
+
at x = 0; the factory does not know the domain, so seeding
|
|
285
|
+
blobs across it means supplying a callable, e.g.
|
|
286
|
+
``lambda rng, n: rng.uniform(-pad, Lx + pad, n)``)
|
|
267
287
|
- "spp": pulse shape parameter in the principal direction
|
|
268
288
|
- "sps": pulse shape parameter in the secondary direction
|
|
269
289
|
sampler : DistributionEnum or Callable[[np.random.Generator, int], np.ndarray]
|
|
@@ -371,7 +391,8 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
371
391
|
Creates a list of Blobs used in the Model.
|
|
372
392
|
|
|
373
393
|
Every blob is given the factory's `t_drain` (a constructor argument,
|
|
374
|
-
`np.inf` — no draining — by default)
|
|
394
|
+
`np.inf` — no draining — by default) and its `t_lifetime` (also a
|
|
395
|
+
constructor argument, None — no envelope — by default).
|
|
375
396
|
|
|
376
397
|
Parameters
|
|
377
398
|
----------
|
|
@@ -407,10 +428,10 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
407
428
|
vys = self._draw_random_variables("vy", num_blobs)
|
|
408
429
|
spxs = self._draw_random_variables("spp", num_blobs)
|
|
409
430
|
spys = self._draw_random_variables("sps", num_blobs)
|
|
431
|
+
posxs = self._draw_random_variables("posx", num_blobs)
|
|
410
432
|
# For now, only a lambda parameter is implemented
|
|
411
433
|
spxs_dict = [{"lam": s} for s in spxs]
|
|
412
434
|
spys_dict = [{"lam": s} for s in spys]
|
|
413
|
-
posxs = np.zeros(num_blobs)
|
|
414
435
|
posys = self.rng.uniform(low=0.0, high=Ly, size=num_blobs)
|
|
415
436
|
t_inits = self.rng.uniform(low=0, high=T, size=num_blobs)
|
|
416
437
|
|
|
@@ -427,6 +448,7 @@ class DefaultBlobFactory(BlobFactory):
|
|
|
427
448
|
pos_y0=posys[i],
|
|
428
449
|
t_init=t_inits[i],
|
|
429
450
|
t_drain=self.t_drain,
|
|
451
|
+
t_lifetime=self.t_lifetime,
|
|
430
452
|
shape_parameters_p=spxs_dict[i],
|
|
431
453
|
shape_parameters_s=spys_dict[i],
|
|
432
454
|
blob_alignment=self.blob_alignment,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: blobmodel
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Two dimensional model of propagating blobs
|
|
5
5
|
Author-email: gregordecristoforo <gregor.decristoforo@gmail.com>
|
|
6
6
|
License: MIT License
|
|
@@ -131,6 +131,8 @@ bm = Model(
|
|
|
131
131
|
```
|
|
132
132
|
Alternatively, you can specify all blob parameters exactly as you want by writing your own class which inherits from BlobFactory. See `examples/custom_blobfactory.py` as an example or take a look at the [blobmodel documentation](https://blobmodel.readthedocs.io/en/latest/?badge=latest).
|
|
133
133
|
|
|
134
|
+
Blobs can decay exponentially from their initial time with a drainage time `t_drain`, and/or be given a finite pulse lifetime `t_lifetime`, a Gaussian envelope symmetric about the blob's `t_init` so that the blob grows, peaks and decays. Both are arguments of `DefaultBlobFactory` and are applied to every sampled blob; see the [drainage time](https://blobmodel.readthedocs.io/en/latest/drainage_time.html) and [pulse lifetime](https://blobmodel.readthedocs.io/en/latest/pulse_lifetime.html) sections of the documentation.
|
|
135
|
+
|
|
134
136
|
## Contributing
|
|
135
137
|
|
|
136
138
|
Feel free to raise issues about anything. Contributions through pull requests are also very welcome. Please take a look at our [Contributor guide](https://blobmodel.readthedocs.io/en/latest/contributor_guide.html) for further details.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
blobmodel/__init__.py
|
|
5
|
+
blobmodel/blob_shape.py
|
|
6
|
+
blobmodel/blobs.py
|
|
7
|
+
blobmodel/distributions.py
|
|
8
|
+
blobmodel/geometry.py
|
|
9
|
+
blobmodel/model.py
|
|
10
|
+
blobmodel/plotting.py
|
|
11
|
+
blobmodel/stochasticality.py
|
|
12
|
+
blobmodel.egg-info/PKG-INFO
|
|
13
|
+
blobmodel.egg-info/SOURCES.txt
|
|
14
|
+
blobmodel.egg-info/dependency_links.txt
|
|
15
|
+
blobmodel.egg-info/requires.txt
|
|
16
|
+
blobmodel.egg-info/top_level.txt
|
|
17
|
+
tests/test_analytical.py
|
|
18
|
+
tests/test_blob.py
|
|
19
|
+
tests/test_blob_factories.py
|
|
20
|
+
tests/test_changing_t_drain.py
|
|
21
|
+
tests/test_docs.py
|
|
22
|
+
tests/test_docs_scripts.py
|
|
23
|
+
tests/test_drain.py
|
|
24
|
+
tests/test_geometry.py
|
|
25
|
+
tests/test_imaging_layout.py
|
|
26
|
+
tests/test_input_validation.py
|
|
27
|
+
tests/test_labels.py
|
|
28
|
+
tests/test_lifetime.py
|
|
29
|
+
tests/test_one_dim.py
|
|
30
|
+
tests/test_pulse_shape.py
|
|
31
|
+
tests/test_seed.py
|
|
32
|
+
tests/test_show_model.py
|
|
33
|
+
tests/test_speed_up.py
|
|
34
|
+
tests/test_stochasticality.py
|
|
35
|
+
tests/test_str.py
|
|
36
|
+
tests/test_vx_or_vy=0.py
|
|
@@ -38,6 +38,33 @@ def test_drainage_time():
|
|
|
38
38
|
# PLACEHOLDER drainage_time_1
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def test_pulse_lifetime():
|
|
42
|
+
# PLACEHOLDER pulse_lifetime_0
|
|
43
|
+
from blobmodel import DefaultBlobFactory, Geometry, Model
|
|
44
|
+
|
|
45
|
+
geometry = Geometry(Nx=100, Ny=1, Lx=10, Ly=0, dt=0.1, T=100, periodic_y=False)
|
|
46
|
+
|
|
47
|
+
# Blobs peak at their t_init and live for a time t_lifetime. Seeding them
|
|
48
|
+
# uniformly over an interval wider than the domain keeps the process
|
|
49
|
+
# homogeneous over the observed region.
|
|
50
|
+
pad = 10.0
|
|
51
|
+
blob_factory = DefaultBlobFactory(t_lifetime=1.0).set_sampler(
|
|
52
|
+
"posx",
|
|
53
|
+
lambda rng, n: rng.uniform(
|
|
54
|
+
geometry.x0 - pad, geometry.x0 + geometry.Lx + pad, n
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
tmp = Model(
|
|
59
|
+
geometry=geometry,
|
|
60
|
+
blob_factory=blob_factory,
|
|
61
|
+
num_blobs=100,
|
|
62
|
+
one_dimensional=True,
|
|
63
|
+
)
|
|
64
|
+
tmp.make_realization()
|
|
65
|
+
# PLACEHOLDER pulse_lifetime_1
|
|
66
|
+
|
|
67
|
+
|
|
41
68
|
def test_one_dim():
|
|
42
69
|
# PLACEHOLDER one_dim_0
|
|
43
70
|
from blobmodel import (
|