feectools 0.1.9__tar.gz → 0.1.11__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 (110) hide show
  1. {feectools-0.1.9/feectools.egg-info → feectools-0.1.11}/PKG-INFO +1 -1
  2. feectools-0.1.11/feectools/__init__.py +4 -0
  3. {feectools-0.1.9 → feectools-0.1.11}/feectools/api/settings.py +8 -5
  4. feectools-0.1.11/feectools/ddm/mpi.py +225 -0
  5. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/partition.py +41 -2
  6. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/basic.py +1 -1
  7. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/direct_solvers.py +5 -2
  8. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/solvers.py +222 -2
  9. {feectools-0.1.9 → feectools-0.1.11/feectools.egg-info}/PKG-INFO +1 -1
  10. {feectools-0.1.9 → feectools-0.1.11}/pyproject.toml +1 -1
  11. feectools-0.1.9/feectools/ddm/mpi.py +0 -114
  12. feectools-0.1.9/feectools/utilities/__init__.py +0 -0
  13. {feectools-0.1.9 → feectools-0.1.11}/AUTHORS +0 -0
  14. {feectools-0.1.9 → feectools-0.1.11}/LICENSE +0 -0
  15. {feectools-0.1.9 → feectools-0.1.11}/README.md +0 -0
  16. {feectools-0.1.9/feectools → feectools-0.1.11/feectools/accelerate}/__init__.py +0 -0
  17. {feectools-0.1.9 → feectools-0.1.11}/feectools/accelerate/accelerate.py +0 -0
  18. {feectools-0.1.9 → feectools-0.1.11}/feectools/accelerate/compile_psydac.mk +0 -0
  19. {feectools-0.1.9/feectools/accelerate → feectools-0.1.11/feectools/api}/__init__.py +0 -0
  20. {feectools-0.1.9 → feectools-0.1.11}/feectools/api/essential_bc.py +0 -0
  21. {feectools-0.1.9 → feectools-0.1.11}/feectools/api/fem_bilinear_form.py +0 -0
  22. {feectools-0.1.9 → feectools-0.1.11}/feectools/api/fem_common.py +0 -0
  23. {feectools-0.1.9 → feectools-0.1.11}/feectools/api/fem_sum_form.py +0 -0
  24. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/__init__.py +0 -0
  25. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/bsplines.py +0 -0
  26. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/bsplines_kernels.py +0 -0
  27. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/field_evaluation_kernels.py +0 -0
  28. {feectools-0.1.9/feectools/api → feectools-0.1.11/feectools/core/tests}/__init__.py +0 -0
  29. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/tests/test_bsplines.py +0 -0
  30. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/tests/test_bsplines_kernel.py +0 -0
  31. {feectools-0.1.9 → feectools-0.1.11}/feectools/core/tests/test_bsplines_pyccel.py +0 -0
  32. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/__init__.py +0 -0
  33. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/basic.py +0 -0
  34. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/blocking_data_exchanger.py +0 -0
  35. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/cart.py +0 -0
  36. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/interface_data_exchanger.py +0 -0
  37. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/nonblocking_data_exchanger.py +0 -0
  38. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/petsc.py +0 -0
  39. {feectools-0.1.9/feectools/core → feectools-0.1.11/feectools/ddm}/tests/__init__.py +0 -0
  40. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/tests/test_cart_1d.py +0 -0
  41. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/tests/test_cart_2d.py +0 -0
  42. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/tests/test_cart_3d.py +0 -0
  43. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/tests/test_multicart_2d.py +0 -0
  44. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/tests/test_partition.py +0 -0
  45. {feectools-0.1.9 → feectools-0.1.11}/feectools/ddm/utilities.py +0 -0
  46. {feectools-0.1.9/feectools/ddm/tests → feectools-0.1.11/feectools/feec}/__init__.py +0 -0
  47. {feectools-0.1.9 → feectools-0.1.11}/feectools/feec/derivatives.py +0 -0
  48. {feectools-0.1.9 → feectools-0.1.11}/feectools/feec/dof_kernels.py +0 -0
  49. {feectools-0.1.9 → feectools-0.1.11}/feectools/feec/global_geometric_projectors.py +0 -0
  50. {feectools-0.1.9 → feectools-0.1.11}/feectools/feec/hodge.py +0 -0
  51. {feectools-0.1.9/feectools/feec → feectools-0.1.11/feectools/fem}/__init__.py +0 -0
  52. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/basic.py +0 -0
  53. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/grid.py +0 -0
  54. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/lst_preconditioner.py +0 -0
  55. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/partitioning.py +0 -0
  56. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/projectors.py +0 -0
  57. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/splines.py +0 -0
  58. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tensor.py +0 -0
  59. {feectools-0.1.9/feectools/fem → feectools-0.1.11/feectools/fem/tests}/__init__.py +0 -0
  60. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/analytical_profiles_1d.py +0 -0
  61. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/analytical_profiles_base.py +0 -0
  62. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/splines_error_bounds.py +0 -0
  63. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_dirichlet_projectors.py +0 -0
  64. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_spline_histopolation.py +0 -0
  65. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_spline_interpolation.py +0 -0
  66. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_splines.py +0 -0
  67. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_splines_par.py +0 -0
  68. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_tensor.py +0 -0
  69. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/test_vector_spaces.py +0 -0
  70. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/tests/utilities.py +0 -0
  71. {feectools-0.1.9 → feectools-0.1.11}/feectools/fem/vector.py +0 -0
  72. {feectools-0.1.9/feectools/fem/tests → feectools-0.1.11/feectools/linalg}/__init__.py +0 -0
  73. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/block.py +0 -0
  74. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/fft.py +0 -0
  75. {feectools-0.1.9/feectools/linalg → feectools-0.1.11/feectools/linalg/kernels}/__init__.py +0 -0
  76. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/axpy_kernels.py +0 -0
  77. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/inner_kernels.py +0 -0
  78. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/matvec_kernels.py +0 -0
  79. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/stencil2IJV_kernels.py +0 -0
  80. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/stencil2coo_kernels.py +0 -0
  81. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kernels/transpose_kernels.py +0 -0
  82. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/kron.py +0 -0
  83. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/memory.py +0 -0
  84. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/sparse.py +0 -0
  85. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/stencil.py +0 -0
  86. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/stencil_dot_kernels.py +0 -0
  87. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/stencil_transpose_kernels.py +0 -0
  88. {feectools-0.1.9/feectools/linalg/kernels → feectools-0.1.11/feectools/linalg/tests}/__init__.py +0 -0
  89. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_block.py +0 -0
  90. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_fft.py +0 -0
  91. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_kron_stencil_matrix.py +0 -0
  92. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_linalg.py +0 -0
  93. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_matrix_free.py +0 -0
  94. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_solvers.py +0 -0
  95. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_stencil_interface_matrix.py +0 -0
  96. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_stencil_vector.py +0 -0
  97. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/test_stencil_vector_space.py +0 -0
  98. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/tests/utilities.py +0 -0
  99. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/topetsc.py +0 -0
  100. {feectools-0.1.9 → feectools-0.1.11}/feectools/linalg/utilities.py +0 -0
  101. {feectools-0.1.9/feectools/linalg/tests → feectools-0.1.11/feectools/utilities}/__init__.py +0 -0
  102. {feectools-0.1.9 → feectools-0.1.11}/feectools/utilities/quadratures.py +0 -0
  103. {feectools-0.1.9 → feectools-0.1.11}/feectools/utilities/utils.py +0 -0
  104. {feectools-0.1.9 → feectools-0.1.11}/feectools/version.py +0 -0
  105. {feectools-0.1.9 → feectools-0.1.11}/feectools.egg-info/SOURCES.txt +0 -0
  106. {feectools-0.1.9 → feectools-0.1.11}/feectools.egg-info/dependency_links.txt +0 -0
  107. {feectools-0.1.9 → feectools-0.1.11}/feectools.egg-info/entry_points.txt +0 -0
  108. {feectools-0.1.9 → feectools-0.1.11}/feectools.egg-info/requires.txt +0 -0
  109. {feectools-0.1.9 → feectools-0.1.11}/feectools.egg-info/top_level.txt +0 -0
  110. {feectools-0.1.9 → feectools-0.1.11}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feectools
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Slimmed-down fork of Psydac (https://github.com/pyccel/psydac) with less functionality and fewer dependencies.
5
5
  Author-email: Psydac development team <psydac@googlegroups.com>
6
6
  Maintainer-email: Stefan Possanner <stefan.possanner@ipp.mpg.de>, Max Lindqvist <max.lindqvist@ipp.mpg.de>, Yaman Güçlü <yaman.guclu@gmail.com>, Martin Campos Pinto <martin.campos-pinto@ipp.mpg.de>, Ahmed Ratnani <ratnaniahmed@gmail.com>
@@ -0,0 +1,4 @@
1
+ # MPI switch, read once when feectools.ddm.mpi is first imported:
2
+ # None -> use mpi4py if it is available (default)
3
+ # False -> do not import mpi4py, use MockMPI (serial runs; saves ~1 s of import time)
4
+ use_mpi = None
@@ -43,13 +43,16 @@ PSYDAC_BACKEND_NVPYCCEL = {'name': 'pyccel',
43
43
  'openmp' : False}
44
44
  # ...
45
45
 
46
- # Get gfortran version
47
- gfortran_version_output = subprocess.check_output(['gfortran', '--version']).decode('utf-8') # nosec B603, B607
48
- gfortran_version_string = re.search(r"(\d+\.\d+\.\d+)", gfortran_version_output).group()
49
- gfortran_version = Version(gfortran_version_string)
46
+ def _get_gfortran_version():
47
+ """Version of the installed gfortran. Only queried where it is needed (see below), since it spawns a subprocess."""
48
+ gfortran_version_output = subprocess.check_output(['gfortran', '--version']).decode('utf-8') # nosec B603, B607
49
+ gfortran_version_string = re.search(r"(\d+\.\d+\.\d+)", gfortran_version_output).group()
50
+ return Version(gfortran_version_string)
50
51
 
51
52
  # Platform-dependent flags
52
- if platform.system() == "Darwin" and platform.machine() == 'arm64' and gfortran_version >= Version("14"):
53
+ # (the gfortran version is only relevant on Apple silicon; evaluating it lazily avoids running
54
+ # a subprocess on every import and lets feectools be imported without gfortran on other platforms)
55
+ if platform.system() == "Darwin" and platform.machine() == 'arm64' and _get_gfortran_version() >= Version("14"):
53
56
 
54
57
  # Apple silicon requires architecture-specific flags (see https://github.com/pyccel/psydac/pull/411)
55
58
  # which are only available on GCC version >= 14
@@ -0,0 +1,225 @@
1
+ """Detection of whether the process was launched by an MPI launcher.
2
+
3
+ Importing ``mpi4py.MPI`` calls ``MPI_Init``, and any collective (``bcast``,
4
+ ``Barrier``, ...) issued afterwards costs something even on a single process.
5
+ A plain ``python script.py`` run should therefore never touch MPI at all, even
6
+ when mpi4py happens to be installed. This module answers the only question
7
+ that decides it: was this process started by ``mpirun``/``mpiexec``/``srun``
8
+ (or an equivalent launcher)?
9
+
10
+ The answer is read from the environment the launcher sets up, so it is
11
+ available before mpi4py is imported.
12
+ """
13
+
14
+ import os
15
+ import sys
16
+
17
+ from dataclasses import dataclass
18
+ from time import time
19
+ from typing import TYPE_CHECKING
20
+
21
+
22
+ # Per-process variables exported by the process managers behind the common
23
+ # launchers. Each is set only for processes started *by* the launcher, so the
24
+ # presence of any one of them means "this rank belongs to an MPI job".
25
+ # SLURM_PROCID is deliberately absent: it is also set for the script of a
26
+ # plain `sbatch` job, which is not an MPI launch. `srun` is covered by the
27
+ # PMI/PMIX variables its MPI plugin exports.
28
+ _LAUNCHER_ENV_VARS = (
29
+ "OMPI_COMM_WORLD_RANK", # Open MPI (and derivatives: Spectrum, ...)
30
+ "PMI_RANK", # MPICH, Intel MPI, MS-MPI, Cray, srun (pmi2)
31
+ "PMIX_RANK", # PMIx, used by srun --mpi=pmix and Open MPI 5
32
+ "MV2_COMM_WORLD_RANK", # MVAPICH2
33
+ "MPI_LOCALRANKID", # Hydra (mpiexec.hydra)
34
+ "ALPS_APP_PE", # Cray ALPS aprun
35
+ "PALS_RANKID", # Cray PALS palsrun
36
+ )
37
+
38
+ # Escape hatch: force the decision either way without touching code, e.g. for
39
+ # a launcher whose variables are not listed above.
40
+ _OVERRIDE_ENV_VAR = "STRUPHY_MPI"
41
+
42
+ _TRUE_VALUES = ("1", "true", "yes", "on")
43
+ _FALSE_VALUES = ("0", "false", "no", "off")
44
+
45
+
46
+ def _override() -> bool | None:
47
+ """Value of ``STRUPHY_MPI``, or None if unset/unrecognized."""
48
+ value = os.environ.get(_OVERRIDE_ENV_VAR)
49
+ if value is None:
50
+ return None
51
+ value = value.strip().lower()
52
+ if value in _TRUE_VALUES:
53
+ return True
54
+ if value in _FALSE_VALUES:
55
+ return False
56
+ return None
57
+
58
+
59
+ def launched_under_mpi() -> bool:
60
+ """Whether this process was started by an MPI launcher.
61
+
62
+ Returns
63
+ -------
64
+ bool
65
+ True if a launcher's per-rank environment variable is present, or if
66
+ the application itself already initialized MPI (in which case using
67
+ the communicator is free). ``STRUPHY_MPI=0``/``1`` overrides
68
+ the detection.
69
+ """
70
+ override = _override()
71
+ if override is not None:
72
+ return override
73
+
74
+ if any(var in os.environ for var in _LAUNCHER_ENV_VARS):
75
+ return True
76
+
77
+ # The application may have initialized MPI itself (embedded interpreter,
78
+ # or an explicit `from mpi4py import MPI`). Only inspect mpi4py if it is
79
+ # already imported: importing it here is exactly what must be avoided.
80
+ mpi_module = sys.modules.get("mpi4py.MPI")
81
+ if mpi_module is not None:
82
+ try:
83
+ return bool(mpi_module.Is_initialized())
84
+ except AttributeError:
85
+ return False
86
+
87
+ return False
88
+
89
+
90
+ # Might not be needed
91
+ class MPICommWrapper:
92
+ def __init__(self, use_mpi=True):
93
+ self.use_mpi = use_mpi
94
+ if use_mpi:
95
+ from mpi4py import MPI
96
+
97
+ self.comm = MPI.COMM_WORLD
98
+ else:
99
+ self.comm = MockComm()
100
+
101
+ def __getattr__(self, name):
102
+ return getattr(self.comm, name)
103
+
104
+
105
+ class MockComm:
106
+ def __getattr__(self, name):
107
+ # Return a function that does nothing and returns None
108
+ def dummy(*args, **kwargs):
109
+ return None
110
+
111
+ return dummy
112
+
113
+ # Override some functions
114
+ def Get_rank(self):
115
+ return 0
116
+
117
+ def Get_size(self):
118
+ return 1
119
+
120
+ def Barrier(self):
121
+ return
122
+
123
+
124
+ class MPIwrapper:
125
+ def __init__(
126
+ self,
127
+ use_mpi: bool = False,
128
+ verbose: bool = False,
129
+ ):
130
+ self.use_mpi = use_mpi
131
+ if use_mpi:
132
+ from mpi4py import MPI
133
+
134
+ self._MPI = MPI
135
+ if verbose:
136
+ print("MPI is enabled")
137
+ else:
138
+ self._MPI = MockMPI()
139
+ if verbose:
140
+ print("MPI is NOT enabled")
141
+
142
+ @property
143
+ def MPI(self):
144
+ return self._MPI
145
+
146
+
147
+ class MockMPI:
148
+ def __getattr__(self, name):
149
+ # Return a function that does nothing and returns None
150
+ def dummy(*args, **kwargs):
151
+ return None
152
+
153
+ return dummy
154
+
155
+ # Override some functions
156
+ @property
157
+ def COMM_WORLD(self):
158
+ return MockComm()
159
+
160
+ # def comm_Get_rank(self):
161
+ # return 0
162
+
163
+ # def comm_Get_size(self):
164
+ # return 1
165
+
166
+
167
+ def _mpi_disabled():
168
+ """True if the user or the host application opted out of MPI.
169
+
170
+ Importing mpi4py initializes MPI, which takes close to a second. Serial runs that
171
+ never use MPI can skip it entirely, in two ways:
172
+
173
+ * export ``FEECTOOLS_MPI=0`` before starting Python, or
174
+ * set ``feectools.use_mpi = False`` before this module is first imported
175
+ (in-process, so it is not inherited by subprocesses such as ``mpirun``).
176
+
177
+ The MockMPI wrapper below is then used, exactly as if mpi4py were not installed.
178
+ """
179
+ import os
180
+
181
+ import feectools
182
+
183
+ if getattr(feectools, 'use_mpi', None) is False:
184
+ return True
185
+ return os.environ.get('FEECTOOLS_MPI', '').strip().lower() in ('0', 'false', 'no', 'off')
186
+
187
+
188
+ if launched_under_mpi():
189
+ try:
190
+ # Disable MPI when using CuPy due to known segfault issues with OpenMPI + CUDA
191
+ import os
192
+ if os.environ.get('ARRAY_BACKEND') == 'cupy':
193
+ raise ImportError("MPI disabled when using CuPy backend")
194
+
195
+ if _mpi_disabled():
196
+ raise ImportError("MPI disabled (feectools.use_mpi = False or FEECTOOLS_MPI=0)")
197
+
198
+ from mpi4py import MPI
199
+
200
+ _comm = MPI.COMM_WORLD
201
+ # rank = _comm.Get_rank()
202
+ # size = _comm.Get_size()
203
+ mpi_enabled = True
204
+ except ImportError:
205
+ # mpi4py not installed
206
+ mpi_enabled = False
207
+ except Exception:
208
+ # mpi4py installed but not running under mpirun
209
+ mpi_enabled = False
210
+ else:
211
+ mpi_enabled = False
212
+
213
+ # TODO: add environment variable for mpi use
214
+ mpi_wrapper = MPIwrapper(
215
+ use_mpi=mpi_enabled,
216
+ verbose=False,
217
+ )
218
+
219
+ # TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime.
220
+ if TYPE_CHECKING:
221
+ from mpi4py import MPI
222
+
223
+ mpi = MPI
224
+ else:
225
+ mpi = mpi_wrapper.MPI
@@ -2,10 +2,49 @@ import cunumpy as xp
2
2
  import numpy as np
3
3
  import numpy.ma as ma
4
4
 
5
- from sympy.ntheory import factorint
5
+ __all__ = ('compute_dims', 'partition_procs_per_patch')
6
+
7
+ #==============================================================================
8
+ def factorint(n, multiple=False):
9
+ """
10
+ Prime factorization of an integer by trial division.
6
11
 
12
+ Drop-in replacement for the subset of ``sympy.ntheory.factorint`` used here.
13
+ Importing sympy takes ~1 s and is not needed for the small integers
14
+ (process counts, number of grid points) that are factorized in this module.
7
15
 
8
- __all__ = ('compute_dims', 'partition_procs_per_patch')
16
+ Parameters
17
+ ----------
18
+ n : int
19
+ Integer to factorize.
20
+
21
+ multiple : bool
22
+ If False (default), return a dict {prime: multiplicity}.
23
+ If True, return the list of primes in ascending order, repeated
24
+ according to their multiplicity.
25
+ """
26
+ n = int(n)
27
+ factors = {}
28
+
29
+ # same conventions as sympy for non-positive input
30
+ if n == 0:
31
+ factors[0] = 1
32
+ else:
33
+ if n < 0:
34
+ factors[-1] = 1
35
+ n = -n
36
+ p = 2
37
+ while p * p <= n:
38
+ while n % p == 0:
39
+ factors[p] = factors.get(p, 0) + 1
40
+ n //= p
41
+ p += 1 if p == 2 else 2
42
+ if n > 1:
43
+ factors[n] = factors.get(n, 0) + 1
44
+
45
+ if multiple:
46
+ return [p for p in sorted(factors) for _ in range(factors[p])]
47
+ return factors
9
48
 
10
49
  #==============================================================================
11
50
  def partition_procs_per_patch(npts, size):
@@ -756,7 +756,7 @@ class ScaledLinearOperator(LinearOperator):
756
756
  self._scalar = c
757
757
 
758
758
  def toarray(self):
759
- return self._scalar * self._operator.toarray()
759
+ return self._scalar * self._operator.toarray
760
760
 
761
761
  def tosparse(self):
762
762
  return self._scalar * self._operator.tosparse().tocsr()
@@ -6,9 +6,7 @@
6
6
  from abc import abstractmethod
7
7
  import cunumpy as xp
8
8
  from cunumpy.xp import array_backend
9
- from scipy.linalg.lapack import dgbtrf, dgbtrs, sgbtrf, sgbtrs, cgbtrf, cgbtrs, zgbtrf, zgbtrs
10
9
  from scipy.sparse import spmatrix, dia_matrix
11
- from scipy.sparse.linalg import splu
12
10
 
13
11
  from feectools.linalg.basic import LinearSolver
14
12
 
@@ -53,6 +51,9 @@ class BandedSolver(LinearSolver):
53
51
  self._l = l
54
52
  self._transposed = transposed
55
53
 
54
+ # imported here: scipy.linalg costs ~0.3 s to import and is only needed once a solver is built
55
+ from scipy.linalg.lapack import dgbtrf, dgbtrs, sgbtrf, sgbtrs, cgbtrf, cgbtrs, zgbtrf, zgbtrs
56
+
56
57
  # ... LU factorization
57
58
  if bmat.dtype == xp.float32:
58
59
  self._factor_function = sgbtrf
@@ -186,6 +187,8 @@ class SparseSolver (LinearSolver):
186
187
 
187
188
  assert isinstance(spmat, spmatrix)
188
189
 
190
+ from scipy.sparse.linalg import splu # deferred, see BandedSolver
191
+
189
192
  self._space = xp.ndarray
190
193
  self._splu = splu(spmat.tocsc())
191
194
  self._transposed = transposed
@@ -4,12 +4,14 @@ This module provides iterative solvers and preconditioners.
4
4
 
5
5
  """
6
6
  import cunumpy as xp
7
- from math import sqrt
7
+ from math import sqrt, inf
8
8
 
9
9
  from feectools.utilities.utils import is_real
10
10
  from feectools.linalg.utilities import _sym_ortho
11
11
  from feectools.linalg.basic import (Vector, LinearOperator,
12
12
  InverseLinearOperator, IdentityOperator, ScaledLinearOperator)
13
+ from feectools.linalg.block import BlockVector, BlockVectorSpace
14
+
13
15
 
14
16
  __all__ = (
15
17
  'inverse',
@@ -20,7 +22,7 @@ __all__ = (
20
22
  'PBiConjugateGradientStabilized',
21
23
  'MinimumResidual',
22
24
  'LSMR',
23
- 'GMRES'
25
+ 'GMRES',
24
26
  )
25
27
 
26
28
  #===============================================================================
@@ -64,6 +66,8 @@ def inverse(A, solver, **kwargs):
64
66
  'minres' : MinimumResidual,
65
67
  'lsmr' : LSMR,
66
68
  'gmres' : GMRES,
69
+ 'uzawa' : UzawaSolver,
70
+ 'schur' : SchurComplementSolver
67
71
  }
68
72
 
69
73
  # Check solver input
@@ -1912,3 +1916,219 @@ class GMRES(InverseLinearOperator):
1912
1916
  def dot(self, b, out=None):
1913
1917
  return self.solve(b, out=out)
1914
1918
 
1919
+
1920
+ class UzawaSolver(InverseLinearOperator):
1921
+ def __init__(self, A, *, A11, A22, B1, B2,
1922
+ x0=None, tol=1e-6, maxiter=1000, verbose=False, recycle=False,
1923
+ inner_tol=1e-7, inner_maxiter=1000):
1924
+
1925
+
1926
+ self._options = {
1927
+ "x0": x0, "tol": tol, "maxiter": maxiter,
1928
+ "verbose": verbose, "recycle": recycle,
1929
+ }
1930
+ super().__init__(A, **self._options)
1931
+
1932
+ self._inner_tol = inner_tol if inner_tol is not None else tol
1933
+ self._inner_maxiter = inner_maxiter
1934
+
1935
+ self._A11 = A11
1936
+ self._A22 = A22
1937
+ self._B1 = B1
1938
+ self._B2 = B2
1939
+
1940
+ self._A11inv = self._inner_solve(A11, tol=self._inner_tol, maxiter=self._inner_maxiter)
1941
+ self._A22inv = self._inner_solve(A22, tol=self._inner_tol, maxiter=self._inner_maxiter)
1942
+
1943
+ self._info = None
1944
+
1945
+ @staticmethod
1946
+ def _inner_solve(A, tol=1e-10, maxiter=200):
1947
+ """Iterative GMRES inverse — works for non-symmetric positive definite operators."""
1948
+ from feectools.linalg.solvers import inverse
1949
+ return inverse(A, "gmres", tol=tol, maxiter=maxiter, recycle=True)
1950
+
1951
+ def update_A11(self, A11):
1952
+ """Recompute A11 inverse when dt changes."""
1953
+ self._A11 = A11
1954
+ self._A11inv = self._inner_solve(A11, tol=self._inner_tol, maxiter=self._inner_maxiter)
1955
+
1956
+ def solve(self, b, out=None):
1957
+
1958
+ A11 = self._A11
1959
+ A22 = self._A22
1960
+ B1 = self._B1
1961
+ B2 = self._B2
1962
+ A11inv = self._A11inv
1963
+ A22inv = self._A22inv
1964
+
1965
+ tol = self._options["tol"]
1966
+ maxiter = self._options["maxiter"]
1967
+ verbose = self._options["verbose"]
1968
+ recycle = self._options["recycle"]
1969
+
1970
+ F = b[0]
1971
+ g = b[1]
1972
+ f_u = F[0]
1973
+ f_ue = F[1]
1974
+
1975
+ # initial guess
1976
+ x0 = self._options["x0"]
1977
+ if x0 is not None:
1978
+ u = x0[0][0].copy()
1979
+ ue = x0[0][1].copy()
1980
+ p = x0[1].copy()
1981
+ else:
1982
+ u = A11.domain.zeros()
1983
+ ue = A22.domain.zeros()
1984
+ p = B1.codomain.zeros()
1985
+
1986
+
1987
+ if verbose:
1988
+ print("Uzawa solver:")
1989
+ print("+---------+---------------------+")
1990
+ print("+ Iter. # | L2-norm of residual |")
1991
+ print("+---------+---------------------+")
1992
+ template = "| {:7d} | {:19.2e} |"
1993
+
1994
+ for iteration in range(1, maxiter + 1):
1995
+
1996
+ # solve A11 * u = f_u - B1^T * p
1997
+ rhs_u = f_u - B1.T.dot(p)
1998
+ u = A11inv.dot(rhs_u)
1999
+
2000
+ # solve A22 * ue = f_ue - B2^T * p
2001
+ rhs_ue = f_ue - B2.T.dot(p)
2002
+ ue = A22inv.dot(rhs_ue)
2003
+
2004
+ # constraint residual: R = B1*u + B2*ue - g
2005
+ R = B1.dot(u) + B2.dot(ue) - g
2006
+ residual_norm = sqrt(R.inner(R).real)
2007
+
2008
+ if verbose:
2009
+ print(template.format(iteration, residual_norm))
2010
+
2011
+ if residual_norm < tol:
2012
+ break
2013
+
2014
+ # pressure update: steepest descent step size
2015
+ S_R = B1.dot(A11inv.dot(B1.T.dot(R))) + B2.dot(A22inv.dot(B2.T.dot(R)))
2016
+ alpha = R.inner(R).real / R.inner(S_R).real
2017
+ p += alpha * R
2018
+
2019
+ if verbose:
2020
+ print("+---------+---------------------+")
2021
+
2022
+ self._info = {
2023
+ 'niter' : iteration,
2024
+ 'success' : residual_norm < tol,
2025
+ 'res_norm': residual_norm,
2026
+ }
2027
+
2028
+ if recycle:
2029
+ block_u = BlockVector(self.domain.spaces[0], blocks=[u, ue])
2030
+ self._options["x0"] = BlockVector(self.domain, blocks=[block_u, p])
2031
+
2032
+ if out is not None:
2033
+ out[0][0] = u
2034
+ out[0][1] = ue
2035
+ out[1] = p
2036
+ return out
2037
+
2038
+ block_u = BlockVector(BlockVectorSpace(A11.domain, A22.domain), blocks=[u, ue])
2039
+ return BlockVector(self.domain, blocks=[block_u, p])
2040
+
2041
+ def dot(self, b, out=None):
2042
+ return self.solve(b, out=out)
2043
+
2044
+
2045
+ class SchurComplementSolver(InverseLinearOperator):
2046
+ def __init__(self, A, *, A11, A22, B1, B2,
2047
+ x0=None, tol=1e-6, maxiter=1000, verbose=False, recycle=False,
2048
+ inner_tol=1e-7, inner_maxiter=1000):
2049
+
2050
+ self._options = {
2051
+ "x0": x0, "tol": tol, "maxiter": maxiter,
2052
+ "verbose": verbose, "recycle": recycle,
2053
+ }
2054
+ super().__init__(A, **self._options)
2055
+
2056
+ self._inner_tol = inner_tol if inner_tol is not None else tol
2057
+ self._inner_maxiter = inner_maxiter
2058
+
2059
+ self._A11 = A11
2060
+ self._A22 = A22
2061
+ self._B1 = B1
2062
+ self._B2 = B2
2063
+
2064
+ self._A11inv = self._make_inner_solver(A11, self._inner_tol, self._inner_maxiter)
2065
+ self._A22inv = self._make_inner_solver(A22, self._inner_tol, self._inner_maxiter)
2066
+
2067
+ self._Sinv = self._build_schur_solver()
2068
+
2069
+ self._info = None
2070
+
2071
+ def _make_inner_solver(self, A, tol, maxiter):
2072
+ from feectools.linalg.solvers import inverse
2073
+ return inverse(A, "gmres", tol=tol, maxiter=maxiter, recycle=True)
2074
+
2075
+ def _build_schur_operator(self):
2076
+ return (self._B1 @ self._A11inv @ self._B1.T
2077
+ + self._B2 @ self._A22inv @ self._B2.T)
2078
+
2079
+ def _build_schur_solver(self):
2080
+ from feectools.linalg.solvers import inverse
2081
+ S = self._build_schur_operator()
2082
+ return inverse(S, "gmres",
2083
+ tol=self._options["tol"],
2084
+ maxiter=self._options["maxiter"],
2085
+ verbose=self._options["verbose"],
2086
+ recycle=self._options["recycle"])
2087
+
2088
+ def update_A11(self, A11):
2089
+ """Recompute A11 inverse and update Schur operator when dt changes."""
2090
+ self._A11 = A11
2091
+ self._A11inv = self._make_inner_solver(A11, self._inner_tol, self._inner_maxiter)
2092
+ # update operator in-place so Sinv keeps its recycled state
2093
+ self._Sinv.linop = self._build_schur_operator()
2094
+
2095
+ def solve(self, b, out=None):
2096
+
2097
+ A11inv = self._A11inv
2098
+ A22inv = self._A22inv
2099
+ B1 = self._B1
2100
+ B2 = self._B2
2101
+ Sinv = self._Sinv
2102
+
2103
+ F = b[0]
2104
+ g = b[1]
2105
+ f_u = F[0]
2106
+ f_ue = F[1]
2107
+
2108
+ # Schur complement RHS: B1*u_f + B2*ue_f - g
2109
+ rhs_p = B1.dot(A11inv.dot(f_u)) + B2.dot(A22inv.dot(f_ue)) - g
2110
+
2111
+ # solve S * p = rhs_p
2112
+ p = Sinv.dot(rhs_p)
2113
+
2114
+ # recover velocities
2115
+ u = A11inv.dot(f_u - B1.T.dot(p))
2116
+ ue = A22inv.dot(f_ue - B2.T.dot(p))
2117
+
2118
+ self._info = Sinv.get_info()
2119
+
2120
+ if self._options["recycle"]:
2121
+ block_u = BlockVector(self.domain.spaces[0], blocks=[u, ue])
2122
+ self._options["x0"] = BlockVector(self.domain, blocks=[block_u, p])
2123
+
2124
+ if out is not None:
2125
+ out[0][0] = u
2126
+ out[0][1] = ue
2127
+ out[1] = p
2128
+ return out
2129
+
2130
+ block_u = BlockVector(self.domain.spaces[0], blocks=[u, ue])
2131
+ return BlockVector(self.domain, blocks=[block_u, p])
2132
+
2133
+ def dot(self, b, out=None):
2134
+ return self.solve(b, out=out)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: feectools
3
- Version: 0.1.9
3
+ Version: 0.1.11
4
4
  Summary: Slimmed-down fork of Psydac (https://github.com/pyccel/psydac) with less functionality and fewer dependencies.
5
5
  Author-email: Psydac development team <psydac@googlegroups.com>
6
6
  Maintainer-email: Stefan Possanner <stefan.possanner@ipp.mpg.de>, Max Lindqvist <max.lindqvist@ipp.mpg.de>, Yaman Güçlü <yaman.guclu@gmail.com>, Martin Campos Pinto <martin.campos-pinto@ipp.mpg.de>, Ahmed Ratnani <ratnaniahmed@gmail.com>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "feectools"
7
- version = "0.1.9"
7
+ version = "0.1.11"
8
8
  description = "Slimmed-down fork of Psydac (https://github.com/pyccel/psydac) with less functionality and fewer dependencies."
9
9
  readme = "README.md"
10
10
  requires-python = ">= 3.10"
@@ -1,114 +0,0 @@
1
- from dataclasses import dataclass
2
- from time import time
3
- from typing import TYPE_CHECKING
4
-
5
-
6
- # Might not be needed
7
- class MPICommWrapper:
8
- def __init__(self, use_mpi=True):
9
- self.use_mpi = use_mpi
10
- if use_mpi:
11
- from mpi4py import MPI
12
-
13
- self.comm = MPI.COMM_WORLD
14
- else:
15
- self.comm = MockComm()
16
-
17
- def __getattr__(self, name):
18
- return getattr(self.comm, name)
19
-
20
-
21
- class MockComm:
22
- def __getattr__(self, name):
23
- # Return a function that does nothing and returns None
24
- def dummy(*args, **kwargs):
25
- return None
26
-
27
- return dummy
28
-
29
- # Override some functions
30
- def Get_rank(self):
31
- return 0
32
-
33
- def Get_size(self):
34
- return 1
35
-
36
- def Barrier(self):
37
- return
38
-
39
-
40
- class MPIwrapper:
41
- def __init__(
42
- self,
43
- use_mpi: bool = False,
44
- verbose: bool = False,
45
- ):
46
- self.use_mpi = use_mpi
47
- if use_mpi:
48
- from mpi4py import MPI
49
-
50
- self._MPI = MPI
51
- if verbose:
52
- print("MPI is enabled")
53
- else:
54
- self._MPI = MockMPI()
55
- if verbose:
56
- print("MPI is NOT enabled")
57
-
58
- @property
59
- def MPI(self):
60
- return self._MPI
61
-
62
-
63
- class MockMPI:
64
- def __getattr__(self, name):
65
- # Return a function that does nothing and returns None
66
- def dummy(*args, **kwargs):
67
- return None
68
-
69
- return dummy
70
-
71
- # Override some functions
72
- @property
73
- def COMM_WORLD(self):
74
- return MockComm()
75
-
76
- # def comm_Get_rank(self):
77
- # return 0
78
-
79
- # def comm_Get_size(self):
80
- # return 1
81
-
82
-
83
- try:
84
- # Disable MPI when using CuPy due to known segfault issues with OpenMPI + CUDA
85
- import os
86
- if os.environ.get('ARRAY_BACKEND') == 'cupy':
87
- raise ImportError("MPI disabled when using CuPy backend")
88
-
89
- from mpi4py import MPI
90
-
91
- _comm = MPI.COMM_WORLD
92
- # rank = _comm.Get_rank()
93
- # size = _comm.Get_size()
94
- mpi_enabled = True
95
- except ImportError:
96
- # mpi4py not installed
97
- mpi_enabled = False
98
- except Exception:
99
- # mpi4py installed but not running under mpirun
100
- mpi_enabled = False
101
-
102
- # TODO: add environment variable for mpi use
103
- mpi_wrapper = MPIwrapper(
104
- use_mpi=mpi_enabled,
105
- verbose=False,
106
- )
107
-
108
- # TYPE_CHECKING is True when type checking (e.g., mypy), but False at runtime.
109
- if TYPE_CHECKING:
110
- from mpi4py import MPI
111
-
112
- mpi = MPI
113
- else:
114
- mpi = mpi_wrapper.MPI
File without changes
File without changes
File without changes
File without changes
File without changes