simgeometry 0.1.0__tar.gz → 0.1.1__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 (67) hide show
  1. {simgeometry-0.1.0/src/simgeometry.egg-info → simgeometry-0.1.1}/PKG-INFO +1 -1
  2. {simgeometry-0.1.0 → simgeometry-0.1.1}/pyproject.toml +1 -1
  3. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/domain_decomposition.py +217 -31
  4. {simgeometry-0.1.0 → simgeometry-0.1.1/src/simgeometry.egg-info}/PKG-INFO +1 -1
  5. {simgeometry-0.1.0 → simgeometry-0.1.1}/LICENSE.txt +0 -0
  6. {simgeometry-0.1.0 → simgeometry-0.1.1}/README.md +0 -0
  7. {simgeometry-0.1.0 → simgeometry-0.1.1}/setup.cfg +0 -0
  8. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/__init__.py +0 -0
  9. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/_bounds.py +0 -0
  10. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/_normalize.py +0 -0
  11. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/_particle_boundaries.py +0 -0
  12. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/_types.py +0 -0
  13. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/api.py +0 -0
  14. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/boundingbox.py +0 -0
  15. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/collection.py +0 -0
  16. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/crossings.py +0 -0
  17. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/cut_edges.py +0 -0
  18. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/cylindrical.py +0 -0
  19. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/evaluation.py +0 -0
  20. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/fractions.py +0 -0
  21. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/grid.py +0 -0
  22. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/__init__.py +0 -0
  23. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/_build.py +0 -0
  24. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/_metal.py +0 -0
  25. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/compile.py +0 -0
  26. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_ball/__init__.py +0 -0
  27. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_ball/contains_ball_cuda.cu +0 -0
  28. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_ball/contains_ball_metal.py +0 -0
  29. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_ball/contains_ball_numpy.py +0 -0
  30. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_ball/contains_ball_pyccel.py +0 -0
  31. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_box/__init__.py +0 -0
  32. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_box/contains_box_cuda.cu +0 -0
  33. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_box/contains_box_metal.py +0 -0
  34. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_box/contains_box_numpy.py +0 -0
  35. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_box/contains_box_pyccel.py +0 -0
  36. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_polygon_2d/__init__.py +0 -0
  37. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_cuda.cu +0 -0
  38. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_metal.py +0 -0
  39. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_numpy.py +0 -0
  40. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/kernels/contains_polygon_2d/contains_polygon_2d_pyccel.py +0 -0
  41. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/mesh.py +0 -0
  42. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/__init__.py +0 -0
  43. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/aperture2d.py +0 -0
  44. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/aperture3d.py +0 -0
  45. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/circle.py +0 -0
  46. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/cuboid.py +0 -0
  47. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/function_object.py +0 -0
  48. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/line.py +0 -0
  49. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/polygon.py +0 -0
  50. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/polyline.py +0 -0
  51. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/rectangle.py +0 -0
  52. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/sphere.py +0 -0
  53. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/objects/triangle.py +0 -0
  54. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/operations.py +0 -0
  55. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/panels.py +0 -0
  56. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/plotting.py +0 -0
  57. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/py.typed +0 -0
  58. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/queries.py +0 -0
  59. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/sampling.py +0 -0
  60. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/shapes.py +0 -0
  61. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/simulation_domain.py +0 -0
  62. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/utils.py +0 -0
  63. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry/visualization.py +0 -0
  64. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry.egg-info/SOURCES.txt +0 -0
  65. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry.egg-info/dependency_links.txt +0 -0
  66. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry.egg-info/requires.txt +0 -0
  67. {simgeometry-0.1.0 → simgeometry-0.1.1}/src/simgeometry.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simgeometry
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Shapes, grids, and simulation domains for numerical simulations
5
5
  Author: Max
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ requires = [ "setuptools", "wheel" ]
4
4
 
5
5
  [project]
6
6
  name = "simgeometry"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "Shapes, grids, and simulation domains for numerical simulations"
9
9
  readme = "README.md"
10
10
  keywords = [ "geometry", "grids", "simulation", "mpi" ]
@@ -1,7 +1,7 @@
1
1
  """Process grid of the MPI ranks and the physical subdomain each one owns.
2
2
 
3
3
  The process grid decides which rank owns which part of the domain. It is used for
4
- particles (:meth:`DomainDecomposition.owner_rank_for_position`) and, through
4
+ particles (:meth:`DomainDecomposition.owner_ranks`) and, through
5
5
  :meth:`DomainDecomposition.layout`, for the :class:`mpiarray.DistributedArray` of the
6
6
  fields, so that markers and fields agree on who owns what.
7
7
  """
@@ -10,11 +10,82 @@ from __future__ import annotations
10
10
 
11
11
  import math
12
12
  from collections.abc import Sequence
13
+ from typing import Any, Literal, Protocol, runtime_checkable
13
14
 
15
+ import cunumpy as xp
14
16
  import mpiarray as mpa
15
17
  import numpy as np
16
18
  from maybempi import MPI
17
19
 
20
+ Layout = Literal["component_major", "particle_major"]
21
+ """Axis order of a positions array: ``(ndim, n)`` or ``(n, ndim)``."""
22
+
23
+
24
+ @runtime_checkable
25
+ class PositionDecomposition(Protocol):
26
+ """What a particle container needs from a decomposition of physical space.
27
+
28
+ The bounds of the domain are already bound in, so a container only passes
29
+ positions. ``positions`` have shape ``(ndim, n)`` (component-major). This is
30
+ the structural interface of the object returned by
31
+ :meth:`DomainDecomposition.position_decomposition`; other decompositions
32
+ can implement it without importing simgeometry.
33
+ """
34
+
35
+ comm: Any
36
+ rank: int
37
+ size: int
38
+
39
+ def owners(self, positions: Any) -> Any:
40
+ """Return the rank owning each position, as an integer array of shape ``(n,)``."""
41
+ ...
42
+
43
+ def owned_mask(self, positions: Any) -> Any:
44
+ """Return a boolean array: which positions this rank owns."""
45
+ ...
46
+
47
+
48
+ class _BoundPositionDecomposition:
49
+ """A :class:`DomainDecomposition` with the grid bounds bound in."""
50
+
51
+ def __init__(
52
+ self,
53
+ decomposition: DomainDecomposition,
54
+ lower_bound: tuple[float, ...],
55
+ upper_bound: tuple[float, ...],
56
+ num_gridpoints: tuple[int, ...],
57
+ ) -> None:
58
+ decomposition._check_grid(lower_bound, upper_bound, num_gridpoints)
59
+ self._decomposition = decomposition
60
+ self._grid = (
61
+ tuple(float(v) for v in lower_bound),
62
+ tuple(float(v) for v in upper_bound),
63
+ tuple(int(v) for v in num_gridpoints),
64
+ )
65
+
66
+ @property
67
+ def comm(self) -> MPI.Comm | None:
68
+ """Return the communicator of the decomposition."""
69
+ return self._decomposition.comm
70
+
71
+ @property
72
+ def rank(self) -> int:
73
+ """Return this rank."""
74
+ return self._decomposition.mpi_rank
75
+
76
+ @property
77
+ def size(self) -> int:
78
+ """Return the number of ranks."""
79
+ return self._decomposition.mpi_size
80
+
81
+ def owners(self, positions: Any, layout: Layout = "component_major") -> Any:
82
+ """Return the owning rank of every position (see ``DomainDecomposition.owner_ranks``)."""
83
+ return self._decomposition.owner_ranks(positions, *self._grid, layout=layout)
84
+
85
+ def owned_mask(self, positions: Any, layout: Layout = "component_major") -> Any:
86
+ """Return which positions this rank owns (see ``DomainDecomposition.owned_mask``)."""
87
+ return self._decomposition.owned_mask(positions, *self._grid, layout=layout)
88
+
18
89
 
19
90
  class DomainDecomposition:
20
91
  """Represent the MPI process grid of a decomposed domain."""
@@ -307,8 +378,8 @@ class DomainDecomposition:
307
378
  ``num_nodes`` nodes of the axis (``n - 1`` cells over ``[lower,
308
379
  upper]``, ``n`` on a periodic axis). Process coordinate ``c`` owns
309
380
  ``[edges[c], edges[c + 1])``, the cells whose lower node it owns; the
310
- last one also owns ``upper``. This is the split
311
- ``owner_rank_for_position`` uses.
381
+ last one also owns ``upper`` and everything beyond, the first
382
+ everything below ``lower`` (see :meth:`owner_ranks`).
312
383
  """
313
384
  num_procs = self.proc_sizes[dim]
314
385
  if num_procs == 1 or self.replicated:
@@ -342,42 +413,157 @@ class DomainDecomposition:
342
413
  upper.append(edges[proc_coord[dim] + 1])
343
414
  return tuple(lower), tuple(upper)
344
415
 
345
- def owner_rank_for_position(
416
+ def _check_grid(
346
417
  self,
347
- position: Sequence[float] | np.ndarray,
348
- lower_bound: tuple[float, ...],
349
- upper_bound: tuple[float, ...],
350
- num_gridpoints: tuple[int, ...],
351
- ) -> int:
352
- """Return the rank owning a physical position in a decomposed grid."""
418
+ lower_bound: Sequence[float],
419
+ upper_bound: Sequence[float],
420
+ num_gridpoints: Sequence[int],
421
+ ) -> None:
422
+ """Raise if the grid description does not have ``ndim`` entries."""
353
423
  if len(lower_bound) != self.ndim or len(upper_bound) != self.ndim:
354
424
  raise ValueError("Bounds must match decomposition dimensions")
355
425
  if len(num_gridpoints) != self.ndim:
356
426
  raise ValueError("num_gridpoints must match decomposition dimensions")
357
427
 
358
- proc_coord = []
359
- for dim, (lower, upper, num_procs) in enumerate(
360
- zip(
361
- lower_bound,
362
- upper_bound,
363
- self.proc_sizes,
364
- ),
365
- ):
366
- coord = num_procs - 1
367
- value = float(position[dim])
428
+ def _components(self, positions: Any, layout: Layout) -> Any:
429
+ """Return ``positions`` as a float ``(ndim, n)`` array on the active backend."""
430
+ if layout not in ("component_major", "particle_major"):
431
+ raise ValueError(
432
+ f"layout must be 'component_major' or 'particle_major', got {layout!r}"
433
+ )
434
+ positions = xp.asarray(positions, dtype=float)
435
+ if positions.ndim != 2:
436
+ raise ValueError(f"positions must be 2D, got shape {positions.shape}")
437
+ if layout == "particle_major":
438
+ positions = positions.T
439
+ if positions.shape[0] != self.ndim:
440
+ raise ValueError(
441
+ f"positions have {positions.shape[0]} components for layout "
442
+ f"{layout!r}, expected {self.ndim}"
443
+ )
444
+ return positions
445
+
446
+ def owner_ranks(
447
+ self,
448
+ positions: Any,
449
+ lower_bound: Sequence[float],
450
+ upper_bound: Sequence[float],
451
+ num_gridpoints: Sequence[int],
452
+ layout: Layout = "component_major",
453
+ ) -> Any:
454
+ """Return the rank owning each position, vectorised.
455
+
456
+ ``positions`` is a NumPy or CuPy array (whatever the active cunumpy
457
+ backend is) of shape ``(ndim, n)`` for ``layout="component_major"``
458
+ (the default, the layout of the marker arrays) or ``(n, ndim)`` for
459
+ ``layout="particle_major"``. The layout is never guessed, so ``n ==
460
+ ndim`` is not ambiguous. The result is an ``int64`` array of shape
461
+ ``(n,)`` in the same memory as ``positions``; ranks are row-major over
462
+ :attr:`proc_sizes`.
463
+
464
+ Along each axis the subdomains are the blocks of
465
+ :meth:`subdomain_edges`: block ``c`` owns ``[edges[c], edges[c + 1])``.
466
+ **Out-of-range rule**: a position below the lower bound belongs to the
467
+ first block and one at or above the upper edge (including exactly the
468
+ upper bound) to the last block, that is, the block index is clipped into
469
+ ``[0, proc_sizes - 1]`` per axis. A position outside the box is thus
470
+ owned by the nearest subdomain, so it is never shipped across the whole
471
+ domain and exactly one rank owns it; deciding what happens to it (kill,
472
+ reflect, wrap on a periodic axis) is the particle boundary condition's
473
+ job and must happen before the lookup. NaN positions go to the last
474
+ block. Without a decomposed axis every position belongs to rank 0.
475
+ """
476
+ self._check_grid(lower_bound, upper_bound, num_gridpoints)
477
+ positions = self._components(positions, layout)
478
+ owners = xp.zeros(positions.shape[1], dtype=xp.int64)
479
+ for dim, num_procs in enumerate(self.proc_sizes):
480
+ if num_procs == 1 or self.replicated:
481
+ continue
368
482
  edges = self.subdomain_edges(
369
- dim, float(lower), float(upper), int(num_gridpoints[dim])
483
+ dim,
484
+ float(lower_bound[dim]),
485
+ float(upper_bound[dim]),
486
+ int(num_gridpoints[dim]),
370
487
  )
371
- for candidate_coord in range(num_procs):
372
- subdomain_lower = edges[candidate_coord]
373
- subdomain_upper = edges[candidate_coord + 1]
374
- if subdomain_lower <= value < subdomain_upper or (
375
- candidate_coord == num_procs - 1 and value == subdomain_upper
376
- ):
377
- coord = candidate_coord
378
- break
379
- proc_coord.append(coord)
380
- return self.rank_from_proc_coord(tuple(proc_coord))
488
+ # The inner edges split the axis; searchsorted already yields
489
+ # 0 below the first and num_procs - 1 at or above the last.
490
+ coord = xp.searchsorted(
491
+ xp.asarray(edges[1:-1], dtype=float), positions[dim], side="right"
492
+ )
493
+ owners += coord * math.prod(self.proc_sizes[dim + 1 :])
494
+ return owners
495
+
496
+ def owned_mask(
497
+ self,
498
+ positions: Any,
499
+ lower_bound: Sequence[float],
500
+ upper_bound: Sequence[float],
501
+ num_gridpoints: Sequence[int],
502
+ layout: Layout = "component_major",
503
+ ) -> Any:
504
+ """Return which positions this rank owns, by the rule of :meth:`owner_ranks`.
505
+
506
+ Equal to ``owner_ranks(...) == mpi_rank`` (every rank owns everything
507
+ when no axis is decomposed), but only two comparisons per decomposed
508
+ axis against this rank's own edges, which is much cheaper than finding
509
+ every owner when most positions stay put.
510
+ """
511
+ self._check_grid(lower_bound, upper_bound, num_gridpoints)
512
+ positions = self._components(positions, layout)
513
+ owned = xp.ones(positions.shape[1], dtype=bool)
514
+ coords = self.proc_coord
515
+ for dim, num_procs in enumerate(self.proc_sizes):
516
+ if num_procs == 1 or self.replicated:
517
+ continue
518
+ edges = self.subdomain_edges(
519
+ dim,
520
+ float(lower_bound[dim]),
521
+ float(upper_bound[dim]),
522
+ int(num_gridpoints[dim]),
523
+ )
524
+ coord = coords[dim]
525
+ column = positions[dim]
526
+ last = num_procs - 1
527
+ if coord > 0:
528
+ # ``~(<)`` rather than ``>=`` keeps NaN on the last block.
529
+ owned &= (
530
+ ~(column < edges[coord])
531
+ if coord == last
532
+ else (column >= edges[coord])
533
+ )
534
+ if coord < last:
535
+ owned &= column < edges[coord + 1]
536
+ return owned
537
+
538
+ def owner_rank_for_position(
539
+ self,
540
+ position: Sequence[float] | np.ndarray,
541
+ lower_bound: tuple[float, ...],
542
+ upper_bound: tuple[float, ...],
543
+ num_gridpoints: tuple[int, ...],
544
+ ) -> int:
545
+ """Return the rank owning one physical position (see :meth:`owner_ranks`)."""
546
+ self._check_grid(lower_bound, upper_bound, num_gridpoints)
547
+ point = xp.asarray(position, dtype=float).reshape(-1, 1)
548
+ owners = self.owner_ranks(point, lower_bound, upper_bound, num_gridpoints)
549
+ return int(owners[0])
550
+
551
+ def position_decomposition(
552
+ self,
553
+ lower_bound: Sequence[float],
554
+ upper_bound: Sequence[float],
555
+ num_gridpoints: Sequence[int],
556
+ ) -> PositionDecomposition:
557
+ """Return this decomposition with the grid bounds bound in.
558
+
559
+ The result implements :class:`PositionDecomposition` (``comm``,
560
+ ``rank``, ``size``, ``owners(positions)``, ``owned_mask(positions)``),
561
+ which is what a particle container needs; it does not change when the
562
+ bounds argument sequences are mutated afterwards.
563
+ """
564
+ return _BoundPositionDecomposition(
565
+ self, tuple(lower_bound), tuple(upper_bound), tuple(num_gridpoints)
566
+ )
381
567
 
382
568
  @property
383
569
  def comm(self) -> MPI.Comm | None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: simgeometry
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: Shapes, grids, and simulation domains for numerical simulations
5
5
  Author: Max
6
6
  License-Expression: MIT
File without changes
File without changes
File without changes