blobmodel 1.2.2__tar.gz → 2.0.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 (108) hide show
  1. blobmodel-2.0.0/.github/workflows/workflow.yml +46 -0
  2. {blobmodel-1.2.2 → blobmodel-2.0.0}/.readthedocs.yml +6 -4
  3. {blobmodel-1.2.2 → blobmodel-2.0.0}/PKG-INFO +19 -18
  4. {blobmodel-1.2.2 → blobmodel-2.0.0}/README.md +9 -11
  5. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel/__init__.py +8 -2
  6. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel/blob_shape.py +76 -22
  7. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel/blobs.py +128 -69
  8. blobmodel-2.0.0/blobmodel/distributions.py +70 -0
  9. blobmodel-2.0.0/blobmodel/geometry.py +191 -0
  10. blobmodel-2.0.0/blobmodel/model.py +560 -0
  11. blobmodel-2.0.0/blobmodel/plotting.py +204 -0
  12. blobmodel-2.0.0/blobmodel/stochasticality.py +467 -0
  13. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel.egg-info/PKG-INFO +19 -18
  14. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel.egg-info/SOURCES.txt +7 -3
  15. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel.egg-info/requires.txt +5 -0
  16. blobmodel-2.0.0/docs/blob_factory.rst +143 -0
  17. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/blob_shapes.rst +13 -4
  18. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/blob_tilt.rst +4 -4
  19. blobmodel-2.0.0/docs/changing_t_drain_plot.py +45 -0
  20. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/contributor_guide.rst +5 -2
  21. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/create_logo.py +7 -13
  22. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/drainage_time.rst +2 -1
  23. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/getting_started.rst +11 -6
  24. blobmodel-2.0.0/docs/model_geometry.rst +52 -0
  25. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/one_dim.rst +2 -1
  26. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/plot_pulses.py +2 -2
  27. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/1d_animation.py +4 -10
  28. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/2_sided_exp_pulse.py +5 -14
  29. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/2d_animation.py +5 -11
  30. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/analyse_output.py +3 -8
  31. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/blob_tilting.py +25 -34
  32. blobmodel-2.0.0/examples/changing_t_drain.py +43 -0
  33. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/compare_to_analytical_sol.py +9 -13
  34. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/custom_blobfactory.py +19 -16
  35. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/point_process.py +5 -15
  36. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/show_labels.py +3 -10
  37. {blobmodel-1.2.2 → blobmodel-2.0.0}/examples/single_blob.py +3 -9
  38. {blobmodel-1.2.2 → blobmodel-2.0.0}/pyproject.toml +10 -3
  39. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_analytical.py +11 -12
  40. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_blob.py +142 -10
  41. blobmodel-2.0.0/tests/test_blob_factories.py +164 -0
  42. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_changing_t_drain.py +10 -12
  43. blobmodel-2.0.0/tests/test_docs.py +332 -0
  44. blobmodel-2.0.0/tests/test_docs_scripts.py +29 -0
  45. blobmodel-2.0.0/tests/test_geometry.py +250 -0
  46. blobmodel-2.0.0/tests/test_imaging_layout.py +80 -0
  47. blobmodel-2.0.0/tests/test_input_validation.py +239 -0
  48. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_labels.py +23 -11
  49. blobmodel-2.0.0/tests/test_one_dim.py +94 -0
  50. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_pulse_shape.py +52 -3
  51. blobmodel-2.0.0/tests/test_seed.py +123 -0
  52. blobmodel-2.0.0/tests/test_show_model.py +129 -0
  53. blobmodel-2.0.0/tests/test_speed_up.py +349 -0
  54. blobmodel-2.0.0/tests/test_stochasticality.py +154 -0
  55. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_str.py +5 -11
  56. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_vx_or_vy=0.py +14 -23
  57. blobmodel-1.2.2/.github/workflows/workflow.yml +0 -73
  58. blobmodel-1.2.2/blobmodel/distributions.py +0 -77
  59. blobmodel-1.2.2/blobmodel/geometry.py +0 -80
  60. blobmodel-1.2.2/blobmodel/model.py +0 -369
  61. blobmodel-1.2.2/blobmodel/plotting.py +0 -162
  62. blobmodel-1.2.2/blobmodel/stochasticality.py +0 -252
  63. blobmodel-1.2.2/docs/blob_factory.rst +0 -76
  64. blobmodel-1.2.2/docs/changing_t_drain_plot.py +0 -50
  65. blobmodel-1.2.2/docs/model_geometry.rst +0 -30
  66. blobmodel-1.2.2/docs/requirements.txt +0 -136
  67. blobmodel-1.2.2/examples/changing_t_drain.py +0 -54
  68. blobmodel-1.2.2/requirements.txt +0 -136
  69. blobmodel-1.2.2/tests/test_docs.py +0 -266
  70. blobmodel-1.2.2/tests/test_model.py +0 -81
  71. blobmodel-1.2.2/tests/test_one_dim.py +0 -71
  72. blobmodel-1.2.2/tests/test_show_model.py +0 -52
  73. blobmodel-1.2.2/tests/test_stochasticality.py +0 -42
  74. {blobmodel-1.2.2 → blobmodel-2.0.0}/.coveragerc +0 -0
  75. {blobmodel-1.2.2 → blobmodel-2.0.0}/.github/workflows/draft-pdf.yml +0 -0
  76. {blobmodel-1.2.2 → blobmodel-2.0.0}/.gitignore +0 -0
  77. {blobmodel-1.2.2 → blobmodel-2.0.0}/LICENSE +0 -0
  78. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel.egg-info/dependency_links.txt +0 -0
  79. {blobmodel-1.2.2 → blobmodel-2.0.0}/blobmodel.egg-info/top_level.txt +0 -0
  80. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/1d_animation.gif +0 -0
  81. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/2-sided_pulse_shape.png +0 -0
  82. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/2d_animation.gif +0 -0
  83. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/alignment_false.gif +0 -0
  84. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/alignment_true.gif +0 -0
  85. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/blob_labels.rst +0 -0
  86. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/blobmodel.rst +0 -0
  87. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/change_t_drain_plot.png +0 -0
  88. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/conf.py +0 -0
  89. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/index.rst +0 -0
  90. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/install.rst +0 -0
  91. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/labels_labels.png +0 -0
  92. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/labels_n.png +0 -0
  93. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/logo.png +0 -0
  94. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/modules.rst +0 -0
  95. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/pulse_shapes.png +0 -0
  96. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/visualize_model.rst +0 -0
  97. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/xarray_example.png +0 -0
  98. {blobmodel-1.2.2 → blobmodel-2.0.0}/docs/y-periodicity.gif +0 -0
  99. {blobmodel-1.2.2 → blobmodel-2.0.0}/paper/paper.bib +0 -0
  100. {blobmodel-1.2.2 → blobmodel-2.0.0}/paper/paper.md +0 -0
  101. {blobmodel-1.2.2 → blobmodel-2.0.0}/readme_files/1d_blobs.gif +0 -0
  102. {blobmodel-1.2.2 → blobmodel-2.0.0}/readme_files/2d_blobs.gif +0 -0
  103. {blobmodel-1.2.2 → blobmodel-2.0.0}/readme_files/logo.png +0 -0
  104. {blobmodel-1.2.2 → blobmodel-2.0.0}/readme_files/tmp5kfvrjp5.svg +0 -0
  105. {blobmodel-1.2.2 → blobmodel-2.0.0}/readme_files/tmpp0mi4n8p.svg +0 -0
  106. {blobmodel-1.2.2 → blobmodel-2.0.0}/setup.cfg +0 -0
  107. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/.gitignore +0 -0
  108. {blobmodel-1.2.2 → blobmodel-2.0.0}/tests/test_drain.py +0 -0
@@ -0,0 +1,46 @@
1
+ name: Tests
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ strategy:
9
+ fail-fast: false
10
+ matrix:
11
+ python-version: ['3.10', '3.11']
12
+
13
+ steps:
14
+ - uses: actions/checkout@v4
15
+
16
+ - name: Set up Python ${{ matrix.python-version }}
17
+ uses: actions/setup-python@v5
18
+ with:
19
+ python-version: ${{ matrix.python-version }}
20
+ cache: 'pip'
21
+ cache-dependency-path: pyproject.toml
22
+
23
+ - name: Install dependencies
24
+ run: |
25
+ python -m pip install .[dev]
26
+
27
+ - name: Check formatting with black
28
+ if: matrix.python-version == '3.10'
29
+ run: |
30
+ black --version
31
+ black --check .
32
+
33
+ - name: Type checking with mypy
34
+ if: matrix.python-version == '3.10'
35
+ run: |
36
+ mypy --ignore-missing-imports blobmodel
37
+
38
+ - name: Test with pytest
39
+ run: |
40
+ pytest --cov --cov-report=xml
41
+
42
+ - name: Upload coverage to Codecov
43
+ if: matrix.python-version == '3.10'
44
+ uses: codecov/codecov-action@v4
45
+ with:
46
+ token: ${{ secrets.CODECOV_TOKEN }}
@@ -24,9 +24,11 @@ sphinx:
24
24
  # - pdf
25
25
  # - epub
26
26
 
27
- # Optional but recommended, declare the Python requirements required
28
- # to build your documentation
29
- # See https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html
27
+ # Install the package itself (autodoc imports it) together with the
28
+ # docs extra declared in pyproject.toml
30
29
  python:
31
30
  install:
32
- - requirements: docs/requirements.txt
31
+ - method: pip
32
+ path: .
33
+ extra_requirements:
34
+ - docs
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: blobmodel
3
- Version: 1.2.2
3
+ Version: 2.0.0
4
4
  Summary: Two dimensional model of propagating blobs
5
5
  Author-email: gregordecristoforo <gregor.decristoforo@gmail.com>
6
6
  License: MIT License
@@ -25,7 +25,7 @@ License: MIT License
25
25
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
26
  SOFTWARE.
27
27
 
28
- Requires-Python: >=3
28
+ Requires-Python: >=3.10
29
29
  Description-Content-Type: text/markdown
30
30
  License-File: LICENSE
31
31
  Requires-Dist: numpy
@@ -34,11 +34,14 @@ Requires-Dist: tqdm
34
34
  Requires-Dist: nptyping
35
35
  Requires-Dist: matplotlib
36
36
  Requires-Dist: netcdf4
37
- Requires-Dist: pytest-cov
38
- Requires-Dist: black
39
- Requires-Dist: mypy
40
- Requires-Dist: sphinx
41
- Requires-Dist: sphinx-rtd-theme
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest; extra == "dev"
39
+ Requires-Dist: pytest-cov; extra == "dev"
40
+ Requires-Dist: black; extra == "dev"
41
+ Requires-Dist: mypy; extra == "dev"
42
+ Provides-Extra: docs
43
+ Requires-Dist: sphinx; extra == "docs"
44
+ Requires-Dist: sphinx-rtd-theme; extra == "docs"
42
45
  Dynamic: license-file
43
46
 
44
47
  # blobmodel <img align="left" src="https://github.com/uit-cosmo/blobmodel/blob/main/readme_files/logo.png" width="120px" >
@@ -97,9 +100,11 @@ python -m pip install -e .
97
100
  Create a grid on which the blobs are discretized using the `Model` class. The `make_realization()` method computes the output as an xarray dataset which can also be written out as a `netcdf` file if the argument `file_name` is specified. A simple example is shown below:
98
101
 
99
102
  ```Python
100
- from blobmodel import Model, show_model
103
+ from blobmodel import Geometry, Model, show_model
101
104
 
102
- bm = Model(Nx=200, Ny=100, Lx=10, Ly=10, dt=0.1, T=20, num_blobs=100)
105
+ bm = Model(
106
+ geometry=Geometry(Nx=200, Ny=100, Lx=10, Ly=10, dt=0.1, T=20), num_blobs=100
107
+ )
103
108
 
104
109
  ds = bm.make_realization(file_name="example.nc")
105
110
  ```
@@ -110,21 +115,17 @@ show_model(ds)
110
115
  You can specify the blob parameters with a BlobFactory class. The DefaultBlobFactory class has some of the most common distribution functions implemented. An example would look like this:
111
116
 
112
117
  ```Python
113
- from blobmodel import DefaultBlobFactory, DistributionEnum, Model
118
+ from blobmodel import DefaultBlobFactory, DistributionEnum, Geometry, Model
114
119
 
115
120
  # use DefaultBlobFactory to define distribution functions of random variables
116
- my_blob_factory = DefaultBlobFactory(A_dist=DistributionEnum.normal, A_parameter=5)
121
+ my_blob_factory = DefaultBlobFactory(t_drain=100).set_sampler(
122
+ "amplitude", DistributionEnum.normal, free_parameter=5
123
+ )
117
124
 
118
125
  # pass on my_blob_factory when creating the Model
119
126
  bm = Model(
120
- Nx=100,
121
- Ny=100,
122
- Lx=10,
123
- Ly=10,
124
- dt=0.1,
125
- T=20,
127
+ geometry=Geometry(Nx=100, Ny=100, Lx=10, Ly=10, dt=0.1, T=20),
126
128
  blob_factory=my_blob_factory,
127
- t_drain=100,
128
129
  num_blobs=100,
129
130
  )
130
131
  ```
@@ -54,9 +54,11 @@ python -m pip install -e .
54
54
  Create a grid on which the blobs are discretized using the `Model` class. The `make_realization()` method computes the output as an xarray dataset which can also be written out as a `netcdf` file if the argument `file_name` is specified. A simple example is shown below:
55
55
 
56
56
  ```Python
57
- from blobmodel import Model, show_model
57
+ from blobmodel import Geometry, Model, show_model
58
58
 
59
- bm = Model(Nx=200, Ny=100, Lx=10, Ly=10, dt=0.1, T=20, num_blobs=100)
59
+ bm = Model(
60
+ geometry=Geometry(Nx=200, Ny=100, Lx=10, Ly=10, dt=0.1, T=20), num_blobs=100
61
+ )
60
62
 
61
63
  ds = bm.make_realization(file_name="example.nc")
62
64
  ```
@@ -67,21 +69,17 @@ show_model(ds)
67
69
  You can specify the blob parameters with a BlobFactory class. The DefaultBlobFactory class has some of the most common distribution functions implemented. An example would look like this:
68
70
 
69
71
  ```Python
70
- from blobmodel import DefaultBlobFactory, DistributionEnum, Model
72
+ from blobmodel import DefaultBlobFactory, DistributionEnum, Geometry, Model
71
73
 
72
74
  # use DefaultBlobFactory to define distribution functions of random variables
73
- my_blob_factory = DefaultBlobFactory(A_dist=DistributionEnum.normal, A_parameter=5)
75
+ my_blob_factory = DefaultBlobFactory(t_drain=100).set_sampler(
76
+ "amplitude", DistributionEnum.normal, free_parameter=5
77
+ )
74
78
 
75
79
  # pass on my_blob_factory when creating the Model
76
80
  bm = Model(
77
- Nx=100,
78
- Ny=100,
79
- Lx=10,
80
- Ly=10,
81
- dt=0.1,
82
- T=20,
81
+ geometry=Geometry(Nx=100, Ny=100, Lx=10, Ly=10, dt=0.1, T=20),
83
82
  blob_factory=my_blob_factory,
84
- t_drain=100,
85
83
  num_blobs=100,
86
84
  )
87
85
  ```
@@ -1,7 +1,13 @@
1
- from .model import Model
1
+ from .model import Model, to_imaging_dataset
2
2
  from .blobs import Blob
3
3
  from .plotting import show_model
4
- from .stochasticality import BlobFactory, DefaultBlobFactory
4
+ from .stochasticality import (
5
+ BlobFactory,
6
+ BlobListFactory,
7
+ CallableBlobFactory,
8
+ DefaultBlobFactory,
9
+ ParameterSampler,
10
+ )
5
11
  from .geometry import Geometry
6
12
  from .blob_shape import AbstractBlobShape, BlobShapeImpl, BlobShapeEnum
7
13
  from .distributions import DistributionEnum
@@ -78,6 +78,19 @@ def _get_lorentz_shape(theta: np.ndarray, **kwargs) -> np.ndarray:
78
78
  def _get_double_exponential_shape(theta: np.ndarray, **kwargs) -> np.ndarray:
79
79
  """Compute the double-exponential pulse shape.
80
80
 
81
+ The shape is ``exp(-theta / lam)`` for ``theta >= 0`` and
82
+ ``exp(theta / (1 - lam))`` for ``theta < 0``, i.e. ``lam`` is the
83
+ e-folding fraction of the leading (``theta >= 0``) side of the pulse.
84
+ For a blob propagating with ``v_x > 0`` observed at a fixed position,
85
+ ``lam`` is therefore the temporal *rise* fraction of the measured pulse,
86
+ matching the asymmetry-parameter convention of the FPP literature.
87
+
88
+ .. versionchanged:: 2.0.0
89
+ The convention was flipped: ``lam`` previously weighted the trailing
90
+ (``theta < 0``) side, so ``lam`` was the temporal *fall* fraction.
91
+ Code written against older versions that passed ``1 - lam`` to
92
+ compensate should now pass ``lam`` directly.
93
+
81
94
  Parameters
82
95
  ----------
83
96
  theta : np.ndarray
@@ -85,18 +98,29 @@ def _get_double_exponential_shape(theta: np.ndarray, **kwargs) -> np.ndarray:
85
98
  kwargs
86
99
  Additional keyword arguments.
87
100
  lam : float
88
- Asymmetry parameter controlling the shape.
101
+ Asymmetry parameter controlling the shape, in the interval [0, 1].
102
+ The limits are the one-sided shapes: lam = 0 is nonzero only for
103
+ theta < 0 (a pure temporal decay for v_x > 0), lam = 1 is nonzero
104
+ only for theta >= 0 (a pure temporal rise for v_x > 0).
89
105
 
90
106
  Returns
91
107
  -------
92
108
  np.ndarray
93
109
  Array representing the double-exponential pulse shape.
110
+
111
+ Raises
112
+ ------
113
+ ValueError
114
+ If lam lies outside the interval [0, 1].
94
115
  """
95
116
  lam = kwargs["lam"]
96
- assert (lam > 0.0) & (lam < 1.0)
117
+ if not 0.0 <= lam <= 1.0:
118
+ raise ValueError(f"lam must be in the interval [0, 1], got lam = {lam}.")
97
119
  kern = np.zeros(shape=np.shape(theta))
98
- kern[theta < 0] = np.exp(theta[theta < 0] / lam)
99
- kern[theta >= 0] = np.exp(-theta[theta >= 0] / (1 - lam))
120
+ if lam < 1.0:
121
+ kern[theta < 0] = np.exp(theta[theta < 0] / (1 - lam))
122
+ if lam > 0.0:
123
+ kern[theta >= 0] = np.exp(-theta[theta >= 0] / lam)
100
124
  return kern
101
125
 
102
126
 
@@ -171,43 +195,73 @@ def _get_dipole_shape(theta: np.ndarray, **kwargs) -> np.ndarray:
171
195
 
172
196
 
173
197
  class BlobShapeImpl(AbstractBlobShape):
174
- """Implementation of the AbstractPulseShape class."""
198
+ """Implementation of the AbstractBlobShape class."""
175
199
 
176
200
  def __init__(
177
201
  self,
178
- pulse_shape_p=BlobShapeEnum.gaussian,
179
- pulse_shape_s=BlobShapeEnum.gaussian,
202
+ pulse_shape_p: BlobShapeEnum = BlobShapeEnum.gaussian,
203
+ pulse_shape_s: BlobShapeEnum = BlobShapeEnum.gaussian,
180
204
  ):
181
205
  """Initialize the BlobShapeImpl object.
182
206
 
183
- Attributes
207
+ Parameters
184
208
  ----------
185
- pulse_shape_p : str, optional
186
- Type of pulse shape in the principal direction, by default "gauss".
187
- pulse_shape_s : str, optional
188
- Type of pulse shape in the secondary direction, by default "gauss".
209
+ pulse_shape_p : BlobShapeEnum, optional
210
+ Type of pulse shape in the principal direction,
211
+ by default BlobShapeEnum.gaussian.
212
+ pulse_shape_s : BlobShapeEnum, optional
213
+ Type of pulse shape in the secondary direction,
214
+ by default BlobShapeEnum.gaussian.
215
+
216
+ Raises
217
+ ------
218
+ NotImplementedError
219
+ If a pulse shape is not a member of BlobShapeEnum with an
220
+ implemented shape function.
189
221
  """
190
222
  if (
191
- pulse_shape_p not in BlobShapeImpl.__GENERATORS.keys()
192
- or pulse_shape_s not in BlobShapeImpl.__GENERATORS.keys()
223
+ pulse_shape_p not in BlobShapeImpl.__GENERATORS
224
+ or pulse_shape_s not in BlobShapeImpl.__GENERATORS
193
225
  ):
194
226
  raise NotImplementedError(
195
227
  f"{self.__class__.__name__}.blob_shape not implemented"
196
228
  )
197
- self.get_blob_shape_p = BlobShapeImpl.__GENERATORS.get(pulse_shape_p)
198
- self.get_blob_shape_s = BlobShapeImpl.__GENERATORS.get(pulse_shape_s)
229
+ self._shape_p = BlobShapeImpl.__GENERATORS[pulse_shape_p]
230
+ self._shape_s = BlobShapeImpl.__GENERATORS[pulse_shape_s]
199
231
 
200
232
  def get_blob_shape_p(self, theta: np.ndarray, **kwargs) -> np.ndarray:
233
+ """Compute the pulse shape in the principal direction.
234
+
235
+ Parameters
236
+ ----------
237
+ theta : np.ndarray
238
+ Array of theta values.
239
+ kwargs
240
+ Additional keyword arguments passed to the shape function.
241
+
242
+ Returns
243
+ -------
244
+ np.ndarray
245
+ Array representing the pulse shape in the principal direction.
201
246
  """
202
- Compute the pulse shape in the principal direction.
203
- """
204
- raise NotImplementedError
247
+ return self._shape_p(theta, **kwargs)
205
248
 
206
249
  def get_blob_shape_s(self, theta: np.ndarray, **kwargs) -> np.ndarray:
250
+ """Compute the pulse shape in the secondary direction.
251
+
252
+ Parameters
253
+ ----------
254
+ theta : np.ndarray
255
+ Array of theta values.
256
+ kwargs
257
+ Additional keyword arguments passed to the shape function.
258
+
259
+ Returns
260
+ -------
261
+ np.ndarray
262
+ Array representing the pulse shape in the secondary direction.
207
263
  """
208
- Compute the pulse shape in the secondary direction.
209
- """
210
- raise NotImplementedError
264
+ return self._shape_s(theta, **kwargs)
211
265
 
212
266
  __GENERATORS = {
213
267
  BlobShapeEnum.exp: _get_exponential_shape,
@@ -1,10 +1,9 @@
1
1
  """This module defines a Blob class and related functions for discretizing and manipulating blobs."""
2
2
 
3
- import warnings
4
- from typing import Union, Any
3
+ from typing import Union, Any, Optional
5
4
  from nptyping import NDArray
6
5
  import numpy as np
7
- from .blob_shape import AbstractBlobShape
6
+ from .blob_shape import AbstractBlobShape, BlobShapeImpl
8
7
  import cmath
9
8
 
10
9
 
@@ -14,7 +13,7 @@ class Blob:
14
13
  the function `discretize_blob`. The contribution of a single blob to a grid defined by `x`, `y` and `t` is given by:
15
14
 
16
15
  .. math::
17
- a e^{-(t-t_k)/\\tau_\\shortparallel}\\varphi\\left( \\frac{x-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}\\varphi\\left( \\frac{(x-x_k)-v(t-t_k)}{\ell_x}, \\frac{(y-y_k)-w(t-t_k)}{\ell_y} \\right)
18
17
 
19
18
  Where:
20
19
  - :math:`a` is the blob amplitude, `amplitude`.
@@ -22,76 +21,105 @@ class Blob:
22
21
  - :math:`\ell_y` is the blob width in the secondary direction, `width_s`.
23
22
  - :math:`v` is the horizontal blob velocity.
24
23
  - :math:`w` is the vertical blob velocity.
25
- - :math:`t_k` is the blob arriving time at the position x=0, `t_init`.
24
+ - :math:`x_k` and :math:`y_k` are the blob position at time :math:`t_k`,
25
+ `pos_x0` and `pos_y0`.
26
+ - :math:`t_k` is the time at which the blob is seeded at
27
+ :math:`(x_k, y_k)`, `t_init`.
26
28
  - :math:`\\tau_\\shortparallel` is the drainage time, `t_drain`.
27
29
  - :math:`\\varphi` is the blob pulse shape, `blob_shape`.
28
30
 
29
- Additionally, a tilt angle can be provided through `theta`.
31
+ Additionally, a tilt angle can be provided through `theta`, in which case
32
+ the two arguments of :math:`\\varphi` are rotated by `theta` around the
33
+ blob center.
30
34
  """
31
35
 
32
36
  def __init__(
33
37
  self,
34
- blob_id: int,
35
- blob_shape: AbstractBlobShape,
36
- amplitude: float,
37
- width_p: float,
38
- width_s: float,
39
- v_x: float,
40
- v_y: float,
41
- pos_x0: float,
42
- pos_y0: float,
43
- t_init: float,
44
- t_drain: Union[float, NDArray],
38
+ blob_id: int = 0,
39
+ blob_shape: Optional[AbstractBlobShape] = None,
40
+ amplitude: float = 1.0,
41
+ width_p: float = 1.0,
42
+ width_s: float = 1.0,
43
+ v_x: float = 1.0,
44
+ v_y: float = 0.0,
45
+ pos_x0: float = 0.0,
46
+ pos_y0: float = 0.0,
47
+ t_init: float = 0.0,
48
+ t_drain: Union[float, NDArray] = np.inf,
45
49
  shape_parameters_p: Union[dict, None] = None,
46
50
  shape_parameters_s: Union[dict, None] = None,
47
51
  blob_alignment: bool = False,
48
- theta: float = 0,
52
+ theta: Optional[float] = None,
49
53
  ) -> None:
50
54
  """
51
55
  Initialize a single blob.
52
56
 
53
57
  Parameters
54
58
  ----------
55
- blob_id : int
56
- Identifier for the blob.
57
- blob_shape : AbstractBlobShape
58
- Shape of the blob.
59
- amplitude : float
60
- Amplitude of the blob.
61
- width_p : float
62
- Width of the blob in the propagation direction.
63
- width_s : float
64
- Width of the blob in the perpendicular direction.
65
- v_x : float
66
- Velocity of the blob in the x-direction.
67
- v_y : float
68
- Velocity of the blob in the y-direction.
69
- pos_x0 : float
70
- Initial position of the blob in the x-direction.
71
- pos_y0 : float
72
- Initial position of the blob in the y-direction.
73
- t_init : float
74
- Initial time of the blob.
75
- t_drain : Union[float, NDArray]
76
- Time scale for the blob to drain.
59
+ blob_id : int, optional
60
+ Identifier for the blob (default 0). Purely metadata: blob labels
61
+ in ``Model`` (``labels="individual"``) are assigned from each
62
+ blob's position in the factory output, not from ``blob_id``.
63
+ blob_shape : AbstractBlobShape, optional
64
+ Shape of the blob. Default None, which means a Gaussian shape in
65
+ both directions, ``BlobShapeImpl()``.
66
+ amplitude : float, optional
67
+ Amplitude of the blob. Default 1.
68
+ width_p : float, optional
69
+ Width of the blob in the propagation direction. Default 1.
70
+ width_s : float, optional
71
+ Width of the blob in the perpendicular direction. Default 1.
72
+ v_x : float, optional
73
+ Velocity of the blob in the x-direction. Default 1.
74
+ v_y : float, optional
75
+ Velocity of the blob in the y-direction. Default 0.
76
+ pos_x0 : float, optional
77
+ Initial position of the blob in the x-direction. Default 0.
78
+ pos_y0 : float, optional
79
+ Initial position of the blob in the y-direction. Default 0.
80
+ t_init : float, optional
81
+ Initial time of the blob. Default 0.
82
+ t_drain : Union[float, NDArray], optional
83
+ Time scale for the blob to drain. Either a scalar or a 1D
84
+ array-like of length Nx (one value per grid point in the
85
+ x-direction); array-likes are stored as float numpy arrays.
86
+ Default ``np.inf`` = no draining.
77
87
  shape_parameters_p : dict
78
88
  Additional shape parameters for the propagation direction.
79
89
  shape_parameters_s : dict
80
90
  Additional shape parameters for the perpendicular direction.
81
91
  blob_alignment : bool, optional
82
- If blob_alignment == True, the blob shapes are rotated in the propagation direction of the blob
83
- If blob_alignment == False, the blob shapes are independent of the propagation direction
84
- theta : float
85
- Blob rotation with respect to the x-axis.
86
-
87
- it is computed according to blob_alignment. If set to a no None value,
88
- the blob alignment flag is ignored. Important: the blob angle is measured with respect to the x axis, not with
89
- respect to the velocity vector.
92
+ Only used when ``theta is None``. If True, the blob shape is rotated to
93
+ align with the blob propagation direction; if False, the blob shape is
94
+ axis-aligned (no rotation). Default False.
95
+ theta : float or None, optional
96
+ Blob rotation angle with respect to the x-axis (not the velocity
97
+ vector). If not None, this angle is used directly and ``blob_alignment``
98
+ is ignored. If None (the default), the angle is determined by
99
+ ``blob_alignment``: the velocity phase when it is True, or 0 when False.
100
+
101
+ Raises
102
+ ------
103
+ TypeError
104
+ If ``blob_shape`` is not an ``AbstractBlobShape`` instance.
105
+ 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).
90
108
 
91
109
  """
92
- assert isinstance(blob_shape, AbstractBlobShape)
110
+ if blob_shape is None:
111
+ blob_shape = BlobShapeImpl()
112
+ if not isinstance(blob_shape, AbstractBlobShape):
113
+ raise TypeError(
114
+ f"blob_shape must be an AbstractBlobShape, got {type(blob_shape).__name__}."
115
+ )
116
+ if width_p <= 0 or width_s <= 0:
117
+ raise ValueError(
118
+ f"Blob widths must be positive, got width_p = {width_p} and width_s = {width_s}."
119
+ )
120
+ if np.any(np.asarray(t_drain) <= 0):
121
+ raise ValueError(f"t_drain must be positive, got t_drain = {t_drain}.")
93
122
 
94
- self.int = int
95
123
  self.blob_id = blob_id
96
124
  self.blob_shape = blob_shape
97
125
  self.amplitude = amplitude
@@ -102,7 +130,14 @@ class Blob:
102
130
  self.pos_x0 = pos_x0
103
131
  self.pos_y0 = pos_y0
104
132
  self.t_init = t_init
105
- self.t_drain = t_drain
133
+ # Normalize to float scalar or float array so downstream code can
134
+ # rely on np.ndim and numpy indexing (lists and numpy integer
135
+ # scalars would otherwise fail deep inside _drain).
136
+ self.t_drain: Union[float, np.ndarray] = (
137
+ float(t_drain)
138
+ if np.ndim(t_drain) == 0
139
+ else np.asarray(t_drain, dtype=np.float64)
140
+ )
106
141
  self.shape_parameters_p = (
107
142
  {} if shape_parameters_p is None else shape_parameters_p
108
143
  )
@@ -110,9 +145,19 @@ class Blob:
110
145
  {} if shape_parameters_s is None else shape_parameters_s
111
146
  )
112
147
  self.blob_alignment = blob_alignment
113
- self._theta = theta
114
- if blob_alignment:
148
+ if theta is not None:
149
+ self._theta = theta
150
+ elif blob_alignment:
115
151
  self._theta = cmath.phase(self.v_x + self.v_y * 1j)
152
+ else:
153
+ self._theta = 0
154
+
155
+ @property
156
+ def theta(self) -> float:
157
+ """float: Tilt angle used when discretizing the blob, measured from
158
+ the x-axis (read-only). Resolved at construction from the explicit
159
+ ``theta`` argument or, when that is None, from ``blob_alignment``."""
160
+ return self._theta
116
161
 
117
162
  def discretize_blob(
118
163
  self,
@@ -122,6 +167,7 @@ class Blob:
122
167
  Ly: float,
123
168
  periodic_y: bool = False,
124
169
  one_dimensional: bool = False,
170
+ y0: float = 0,
125
171
  ) -> NDArray:
126
172
  """
127
173
  Discretize blob on grid. If one_dimensional the secondary pulse shape is ignored.
@@ -129,17 +175,28 @@ class Blob:
129
175
  Parameters
130
176
  ----------
131
177
  x : NDArray
132
- Grid coordinates in the x-direction.
178
+ Grid coordinates in the x-direction, as an array broadcastable to
179
+ shape (Ny, Nx, Nt): either a full meshgrid array or a 1D
180
+ coordinate array reshaped as ``x[np.newaxis, :, np.newaxis]``.
133
181
  y : NDArray
134
- Grid coordinates in the y-direction.
182
+ Grid coordinates in the y-direction, as an array broadcastable to
183
+ shape (Ny, Nx, Nt): either a full meshgrid array or a 1D
184
+ coordinate array reshaped as ``y[:, np.newaxis, np.newaxis]``.
135
185
  t : NDArray
136
- Time coordinates.
186
+ Time coordinates, as an array broadcastable to shape (Ny, Nx, Nt):
187
+ either a full meshgrid array or a 1D coordinate array reshaped as
188
+ ``t[np.newaxis, np.newaxis, :]``. A scalar t (single time point)
189
+ is also accepted.
137
190
  Ly : float
138
191
  Length of domain in the y-direction.
139
192
  periodic_y : bool, optional
140
193
  Flag indicating periodicity in the y-direction (default: False).
141
194
  one_dimensional : bool, optional
142
195
  Flag indicating a one-dimensional blob (default: False).
196
+ y0 : float, optional
197
+ Origin of the domain in the y-direction (default: 0). Only used
198
+ when ``periodic_y`` is True, where the blob position is wrapped
199
+ into the domain ``[y0, y0 + Ly)``.
143
200
 
144
201
  Notes
145
202
  -----
@@ -149,25 +206,27 @@ class Blob:
149
206
  Returns
150
207
  -------
151
208
  discretized_blob : NDArray
152
- Discretized blob on a 3D array with dimensions (x, y, t).
209
+ Discretized blob on a 3D array with dimensions (y, x, t),
210
+ i.e. shape (Ny, Nx, Nt).
153
211
 
154
- """
155
- # If one_dimensional, then Ly should be 0.
156
- assert (one_dimensional and Ly == 0) or not one_dimensional
212
+ Raises
213
+ ------
214
+ ValueError
215
+ If ``one_dimensional`` is True and ``Ly`` is not 0.
157
216
 
158
- if (self.width_s > Ly / 3 or self.width_p > Ly / 3) and periodic_y:
159
- warnings.warn(
160
- "blob width big compared to Ly, mirrored blobs might become apparent."
161
- )
217
+ """
218
+ if one_dimensional and Ly != 0:
219
+ raise ValueError(f"One dimensional blobs require Ly == 0, got Ly = {Ly}.")
162
220
 
163
221
  if not periodic_y or one_dimensional:
164
222
  return self._single_blob(
165
223
  x, y, t, Ly, periodic_y, one_dimensional=one_dimensional
166
224
  )
167
225
 
168
- time = t if type(t) in [int, float] else t[0][0]
226
+ time = t if np.ndim(t) == 0 else t[0][0]
169
227
  vertical_prop = self.v_y * (time - self.t_init) + self.pos_y0
170
- number_of_y_propagations = vertical_prop // Ly
228
+ # Wrap the blob position into the domain [y0, y0 + Ly).
229
+ number_of_y_propagations = (vertical_prop - y0) // Ly
171
230
 
172
231
  # Sum of a centered blob is two "ghost blobs" at vertical positions +-Ly.
173
232
  return (
@@ -277,9 +336,9 @@ class Blob:
277
336
  Drain factor.
278
337
 
279
338
  """
280
- if isinstance(self.t_drain, (int, float)):
281
- return np.exp(-(t - self.t_init) / float(self.t_drain))
282
- return np.exp(-(t - self.t_init) / self.t_drain[np.newaxis, :, np.newaxis])
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)
283
342
 
284
343
  def _blob_trajectory_x(self, t: Union[int, NDArray]) -> Any:
285
344
  """