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.
Files changed (93) hide show
  1. {blobmodel-2.0.0 → blobmodel-2.1.0}/PKG-INFO +3 -1
  2. {blobmodel-2.0.0 → blobmodel-2.1.0}/README.md +2 -0
  3. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/blobs.py +72 -7
  4. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/model.py +130 -18
  5. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/stochasticality.py +25 -3
  6. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/PKG-INFO +3 -1
  7. blobmodel-2.1.0/blobmodel.egg-info/SOURCES.txt +36 -0
  8. {blobmodel-2.0.0 → blobmodel-2.1.0}/pyproject.toml +1 -1
  9. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_docs.py +27 -0
  10. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_docs_scripts.py +1 -0
  11. blobmodel-2.1.0/tests/test_lifetime.py +521 -0
  12. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_speed_up.py +20 -3
  13. blobmodel-2.0.0/.coveragerc +0 -8
  14. blobmodel-2.0.0/.github/workflows/draft-pdf.yml +0 -27
  15. blobmodel-2.0.0/.github/workflows/workflow.yml +0 -46
  16. blobmodel-2.0.0/.gitignore +0 -148
  17. blobmodel-2.0.0/.readthedocs.yml +0 -34
  18. blobmodel-2.0.0/blobmodel.egg-info/SOURCES.txt +0 -89
  19. blobmodel-2.0.0/docs/1d_animation.gif +0 -0
  20. blobmodel-2.0.0/docs/2-sided_pulse_shape.png +0 -0
  21. blobmodel-2.0.0/docs/2d_animation.gif +0 -0
  22. blobmodel-2.0.0/docs/alignment_false.gif +0 -0
  23. blobmodel-2.0.0/docs/alignment_true.gif +0 -0
  24. blobmodel-2.0.0/docs/blob_factory.rst +0 -143
  25. blobmodel-2.0.0/docs/blob_labels.rst +0 -25
  26. blobmodel-2.0.0/docs/blob_shapes.rst +0 -90
  27. blobmodel-2.0.0/docs/blob_tilt.rst +0 -39
  28. blobmodel-2.0.0/docs/blobmodel.rst +0 -61
  29. blobmodel-2.0.0/docs/change_t_drain_plot.png +0 -0
  30. blobmodel-2.0.0/docs/changing_t_drain_plot.py +0 -45
  31. blobmodel-2.0.0/docs/conf.py +0 -37
  32. blobmodel-2.0.0/docs/contributor_guide.rst +0 -103
  33. blobmodel-2.0.0/docs/create_logo.py +0 -79
  34. blobmodel-2.0.0/docs/drainage_time.rst +0 -20
  35. blobmodel-2.0.0/docs/getting_started.rst +0 -69
  36. blobmodel-2.0.0/docs/index.rst +0 -85
  37. blobmodel-2.0.0/docs/install.rst +0 -25
  38. blobmodel-2.0.0/docs/labels_labels.png +0 -0
  39. blobmodel-2.0.0/docs/labels_n.png +0 -0
  40. blobmodel-2.0.0/docs/logo.png +0 -0
  41. blobmodel-2.0.0/docs/model_geometry.rst +0 -52
  42. blobmodel-2.0.0/docs/modules.rst +0 -4
  43. blobmodel-2.0.0/docs/one_dim.rst +0 -25
  44. blobmodel-2.0.0/docs/plot_pulses.py +0 -33
  45. blobmodel-2.0.0/docs/pulse_shapes.png +0 -0
  46. blobmodel-2.0.0/docs/visualize_model.rst +0 -31
  47. blobmodel-2.0.0/docs/xarray_example.png +0 -0
  48. blobmodel-2.0.0/docs/y-periodicity.gif +0 -0
  49. blobmodel-2.0.0/examples/1d_animation.py +0 -24
  50. blobmodel-2.0.0/examples/2_sided_exp_pulse.py +0 -27
  51. blobmodel-2.0.0/examples/2d_animation.py +0 -15
  52. blobmodel-2.0.0/examples/analyse_output.py +0 -23
  53. blobmodel-2.0.0/examples/blob_tilting.py +0 -77
  54. blobmodel-2.0.0/examples/changing_t_drain.py +0 -43
  55. blobmodel-2.0.0/examples/compare_to_analytical_sol.py +0 -41
  56. blobmodel-2.0.0/examples/custom_blobfactory.py +0 -79
  57. blobmodel-2.0.0/examples/point_process.py +0 -122
  58. blobmodel-2.0.0/examples/show_labels.py +0 -21
  59. blobmodel-2.0.0/examples/single_blob.py +0 -18
  60. blobmodel-2.0.0/paper/paper.bib +0 -213
  61. blobmodel-2.0.0/paper/paper.md +0 -140
  62. blobmodel-2.0.0/readme_files/1d_blobs.gif +0 -0
  63. blobmodel-2.0.0/readme_files/2d_blobs.gif +0 -0
  64. blobmodel-2.0.0/readme_files/logo.png +0 -0
  65. blobmodel-2.0.0/readme_files/tmp5kfvrjp5.svg +0 -1
  66. blobmodel-2.0.0/readme_files/tmpp0mi4n8p.svg +0 -1
  67. blobmodel-2.0.0/tests/.gitignore +0 -139
  68. {blobmodel-2.0.0 → blobmodel-2.1.0}/LICENSE +0 -0
  69. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/__init__.py +0 -0
  70. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/blob_shape.py +0 -0
  71. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/distributions.py +0 -0
  72. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/geometry.py +0 -0
  73. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel/plotting.py +0 -0
  74. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/dependency_links.txt +0 -0
  75. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/requires.txt +0 -0
  76. {blobmodel-2.0.0 → blobmodel-2.1.0}/blobmodel.egg-info/top_level.txt +0 -0
  77. {blobmodel-2.0.0 → blobmodel-2.1.0}/setup.cfg +0 -0
  78. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_analytical.py +0 -0
  79. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_blob.py +0 -0
  80. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_blob_factories.py +0 -0
  81. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_changing_t_drain.py +0 -0
  82. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_drain.py +0 -0
  83. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_geometry.py +0 -0
  84. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_imaging_layout.py +0 -0
  85. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_input_validation.py +0 -0
  86. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_labels.py +0 -0
  87. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_one_dim.py +0 -0
  88. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_pulse_shape.py +0 -0
  89. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_seed.py +0 -0
  90. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_show_model.py +0 -0
  91. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_stochasticality.py +0 -0
  92. {blobmodel-2.0.0 → blobmodel-2.1.0}/tests/test_str.py +0 -0
  93. {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.0.0
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, or if ``t_drain``
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 * self._drain(t) * primary_axis_shape * secondary_axis_shape
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
- if isinstance(self.t_drain, np.ndarray):
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 or blob.v_x == 0:
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
- idx_x0 = (blob.t_init - t0) / dt + (self._geometry.x[0] - blob.pos_x0) / (
483
- blob.v_x * dt
484
- )
485
- idx_Lx = idx_x0 + self._geometry.Lx / (blob.v_x * dt)
486
- # Decay length of the blob along the x-axis: the blob-frame widths
487
- # projected onto x through the tilt angle (width_p for an untilted
488
- # blob, width_s at theta = pi/2).
489
- width_x = (
490
- np.abs(np.cos(blob.theta)) * blob.width_p
491
- + np.abs(np.sin(blob.theta)) * blob.width_s
492
- )
493
- margin = (
494
- -width_x * np.log(truncation_error * np.sqrt(np.pi)) / np.abs(blob.v_x * dt)
495
- )
496
- start = int(np.clip(min(idx_x0, idx_Lx) - margin, 0, self._geometry.t.size))
497
- stop = int(np.clip(max(idx_x0, idx_Lx) + margin, 0, self._geometry.t.size))
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
- return start, stop
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.0.0
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "blobmodel"
7
- version = "2.0.0"
7
+ version = "2.1.0"
8
8
  description = "Two dimensional model of propagating blobs"
9
9
  authors = [
10
10
  { name = "gregordecristoforo", email = "gregor.decristoforo@gmail.com" }
@@ -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 (
@@ -18,6 +18,7 @@ SCRIPTS = [
18
18
  "create_logo.py",
19
19
  "plot_pulses.py",
20
20
  "changing_t_drain_plot.py",
21
+ "pulse_lifetime_plot.py",
21
22
  ]
22
23
 
23
24