rbflab 0.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 (89) hide show
  1. rbflab-0.1.0/CHANGELOG.md +12 -0
  2. rbflab-0.1.0/LICENSE +21 -0
  3. rbflab-0.1.0/MANIFEST.in +7 -0
  4. rbflab-0.1.0/PKG-INFO +85 -0
  5. rbflab-0.1.0/README.md +129 -0
  6. rbflab-0.1.0/README_PYPI.md +49 -0
  7. rbflab-0.1.0/RELEASE_CHECKLIST.md +51 -0
  8. rbflab-0.1.0/cpp/README.md +60 -0
  9. rbflab-0.1.0/cpp/cuda_lhi.cu +78 -0
  10. rbflab-0.1.0/cpp/eigen_solver.hpp +92 -0
  11. rbflab-0.1.0/cpp/generated_hybrid.hpp +2101 -0
  12. rbflab-0.1.0/cpp/generated_imq.hpp +493 -0
  13. rbflab-0.1.0/cpp/lhi_mpfr.cpp +78 -0
  14. rbflab-0.1.0/cpp/operators_local.hpp +62 -0
  15. rbflab-0.1.0/cpp/prepare_dependencies.sh +9 -0
  16. rbflab-0.1.0/cpp/real.hpp +49 -0
  17. rbflab-0.1.0/cpp/real_double.hpp +36 -0
  18. rbflab-0.1.0/cpp/scalar_local.hpp +68 -0
  19. rbflab-0.1.0/docs/CAPABILITIES.md +24 -0
  20. rbflab-0.1.0/docs/INSTALL.md +74 -0
  21. rbflab-0.1.0/docs/VISUALIZATION.md +48 -0
  22. rbflab-0.1.0/docs/assets/heat_diffusion.gif +0 -0
  23. rbflab-0.1.0/docs/assets/heat_diffusion.png +0 -0
  24. rbflab-0.1.0/docs/assets/navier_stokes_cavity.gif +0 -0
  25. rbflab-0.1.0/docs/assets/navier_stokes_cavity.png +0 -0
  26. rbflab-0.1.0/docs/assets/stokes_velocity.png +0 -0
  27. rbflab-0.1.0/docs/guides/LID_DRIVEN_CAVITY.md +29 -0
  28. rbflab-0.1.0/examples/build_cpp_backend.py +42 -0
  29. rbflab-0.1.0/examples/custom_kernel.py +36 -0
  30. rbflab-0.1.0/examples/generate_cpp_hybrid.py +33 -0
  31. rbflab-0.1.0/examples/make_gallery.py +72 -0
  32. rbflab-0.1.0/examples/navier_stokes_cavity.py +128 -0
  33. rbflab-0.1.0/examples/operators_3d.py +39 -0
  34. rbflab-0.1.0/examples/stokes_spaces.py +63 -0
  35. rbflab-0.1.0/examples/symbolic_boundary.py +32 -0
  36. rbflab-0.1.0/examples/symbolic_heat.py +28 -0
  37. rbflab-0.1.0/examples/symbolic_poisson.py +46 -0
  38. rbflab-0.1.0/pyproject.toml +41 -0
  39. rbflab-0.1.0/python/rbflab/__init__.py +70 -0
  40. rbflab-0.1.0/python/rbflab/assembly.py +73 -0
  41. rbflab-0.1.0/python/rbflab/block_methods.py +292 -0
  42. rbflab-0.1.0/python/rbflab/cuda_backend.py +85 -0
  43. rbflab-0.1.0/python/rbflab/descriptor.py +36 -0
  44. rbflab-0.1.0/python/rbflab/diagnostics.py +56 -0
  45. rbflab-0.1.0/python/rbflab/differentiable_lhi.py +147 -0
  46. rbflab-0.1.0/python/rbflab/discrete_operators.py +227 -0
  47. rbflab-0.1.0/python/rbflab/eigen_build.py +15 -0
  48. rbflab-0.1.0/python/rbflab/evolution.py +378 -0
  49. rbflab-0.1.0/python/rbflab/geometry.py +190 -0
  50. rbflab-0.1.0/python/rbflab/hermite.py +39 -0
  51. rbflab-0.1.0/python/rbflab/kernel_compiler.py +283 -0
  52. rbflab-0.1.0/python/rbflab/kernels.py +187 -0
  53. rbflab-0.1.0/python/rbflab/legacy_cpp.py +120 -0
  54. rbflab-0.1.0/python/rbflab/lhi_backends.py +293 -0
  55. rbflab-0.1.0/python/rbflab/lhi_stokes.py +367 -0
  56. rbflab-0.1.0/python/rbflab/mesh_adapters.py +43 -0
  57. rbflab-0.1.0/python/rbflab/methods.py +302 -0
  58. rbflab-0.1.0/python/rbflab/nodal.py +233 -0
  59. rbflab-0.1.0/python/rbflab/operator_backends.py +128 -0
  60. rbflab-0.1.0/python/rbflab/operators.py +168 -0
  61. rbflab-0.1.0/python/rbflab/precision.py +263 -0
  62. rbflab-0.1.0/python/rbflab/problems.py +101 -0
  63. rbflab-0.1.0/python/rbflab/rbf_fd.py +148 -0
  64. rbflab-0.1.0/python/rbflab/rbf_ra.py +76 -0
  65. rbflab-0.1.0/python/rbflab/reference.py +131 -0
  66. rbflab-0.1.0/python/rbflab/scalar_backends.py +186 -0
  67. rbflab-0.1.0/python/rbflab/scalar_fd.py +55 -0
  68. rbflab-0.1.0/python/rbflab/scaling.py +30 -0
  69. rbflab-0.1.0/python/rbflab/space_stokes.py +328 -0
  70. rbflab-0.1.0/python/rbflab/spaces.py +126 -0
  71. rbflab-0.1.0/python/rbflab/sparse_precision.py +175 -0
  72. rbflab-0.1.0/python/rbflab/stencils.py +86 -0
  73. rbflab-0.1.0/python/rbflab/stokes.py +166 -0
  74. rbflab-0.1.0/python/rbflab/stokes_polynomials.py +53 -0
  75. rbflab-0.1.0/python/rbflab/stokes_ra.py +98 -0
  76. rbflab-0.1.0/python/rbflab/strategies.py +45 -0
  77. rbflab-0.1.0/python/rbflab/symbolic.py +275 -0
  78. rbflab-0.1.0/python/rbflab/symbolic_kernel.py +172 -0
  79. rbflab-0.1.0/python/rbflab/symbolic_system.py +149 -0
  80. rbflab-0.1.0/python/rbflab/time_data.py +62 -0
  81. rbflab-0.1.0/python/rbflab/torch_backend.py +277 -0
  82. rbflab-0.1.0/python/rbflab/unsteady_stokes.py +167 -0
  83. rbflab-0.1.0/python/rbflab/viz.py +216 -0
  84. rbflab-0.1.0/python/rbflab.egg-info/PKG-INFO +85 -0
  85. rbflab-0.1.0/python/rbflab.egg-info/SOURCES.txt +87 -0
  86. rbflab-0.1.0/python/rbflab.egg-info/dependency_links.txt +1 -0
  87. rbflab-0.1.0/python/rbflab.egg-info/requires.txt +21 -0
  88. rbflab-0.1.0/python/rbflab.egg-info/top_level.txt +1 -0
  89. rbflab-0.1.0/setup.cfg +4 -0
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Curated six symbolic and numerical starting examples.
6
+ - Added `unit_box_grid` for small 2D/3D problems without Gmsh.
7
+ - Documented installation, optional backends, precision stages, and current
8
+ capability limits.
9
+ - Added optional 2D field plots and GIF animation, with reproducible README
10
+ images generated from numerical heat and Stokes solutions.
11
+ - Added a standalone Re=100 staggered RBF-FD Navier–Stokes cavity example and
12
+ an optional nodal-velocity GIF/streamline plotting helper.
rbflab-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Louis Breton
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,7 @@
1
+ include README.md README_PYPI.md CHANGELOG.md LICENSE RELEASE_CHECKLIST.md
2
+ include docs/INSTALL.md docs/CAPABILITIES.md docs/VISUALIZATION.md docs/guides/LID_DRIVEN_CAVITY.md
3
+ recursive-include docs/assets *.png *.gif
4
+ recursive-include examples *.py
5
+ recursive-include cpp *.cpp *.hpp *.cu *.sh *.md
6
+ prune tests
7
+ global-exclude __pycache__ *.py[cod] *.exe
rbflab-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,85 @@
1
+ Metadata-Version: 2.4
2
+ Name: rbflab
3
+ Version: 0.1.0
4
+ Summary: RBFLAB: radial basis functions for interpolation and PDEs
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/LDBreton/RBFLAB
7
+ Project-URL: Repository, https://github.com/LDBreton/RBFLAB
8
+ Project-URL: Issues, https://github.com/LDBreton/RBFLAB/issues
9
+ Keywords: radial basis functions,meshless,PDE,RBF-FD,collocation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.11
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
16
+ Requires-Python: >=3.11
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy>=1.26
20
+ Requires-Dist: scipy>=1.11
21
+ Requires-Dist: sympy>=1.12
22
+ Requires-Dist: mpmath>=1.3
23
+ Requires-Dist: threadpoolctl>=3.5
24
+ Provides-Extra: torch
25
+ Requires-Dist: torch>=2.2; extra == "torch"
26
+ Provides-Extra: fast
27
+ Requires-Dist: gmpy2>=2.2; extra == "fast"
28
+ Provides-Extra: mesh
29
+ Requires-Dist: gmsh>=4.12; extra == "mesh"
30
+ Provides-Extra: test
31
+ Requires-Dist: pytest>=8; extra == "test"
32
+ Provides-Extra: examples
33
+ Requires-Dist: matplotlib>=3.8; extra == "examples"
34
+ Requires-Dist: pillow>=10; extra == "examples"
35
+ Dynamic: license-file
36
+
37
+ # RBFLAB
38
+
39
+ RBFLAB is a Python library for radial basis function interpolation and PDEs on
40
+ point clouds. It supports global collocation, local Hermite interpolation
41
+ (LHI), RBF-FD, symbolic linear operators and boundary conditions, and
42
+ divergence-free velocity spaces. Scalar operators work in 2D and 3D.
43
+
44
+ Source and examples: https://github.com/LDBreton/RBFLAB
45
+
46
+ Install the Python core with:
47
+
48
+ ```sh
49
+ python -m pip install rbflab
50
+ ```
51
+
52
+ Optional extras include `rbflab[examples]` for Matplotlib plots and GIFs,
53
+ `rbflab[mesh]` for Gmsh, and `rbflab[torch]` for the documented CPU Float64
54
+ tensor backend. The C++ double/MPFR backends are source-build options.
55
+
56
+ Here is a symbolic Poisson problem:
57
+
58
+ ```python
59
+ import sympy as sp
60
+ import rbflab as rbf
61
+
62
+ model = rbf.SymbolicScalar(2)
63
+ u = model.field
64
+ x, y = model.coordinates
65
+ exact = sp.sin(sp.pi*x) * sp.sin(sp.pi*y)
66
+ lhs = -model.laplacian(u)
67
+ problem = model.stationary(
68
+ sp.Eq(lhs, lhs.subs(u, exact).doit()),
69
+ boundary=[model.bc("boundary", sp.Eq(u, 0))],
70
+ )
71
+ cloud = rbf.unit_box_grid(5)
72
+ solution = problem.solve(
73
+ cloud, rbf.GlobalCollocation(rbf.IMQ(2), scheme="asymmetric")
74
+ )
75
+ print(solution.evaluate([[0.3, 0.4]]))
76
+ ```
77
+
78
+ The source repository also contains short heat, Stokes, 3D operator, custom
79
+ kernel, and Re=100 lid-driven cavity examples. The cavity example is a
80
+ showcase with a documented nonzero discrete divergence defect; it does not
81
+ validate a general Navier–Stokes solver. LHI off-node reconstruction has its
82
+ own accuracy limitations, and MPFR local weights do not automatically make a
83
+ global sparse solve extended precision.
84
+
85
+ RBFLAB is licensed under MIT.
rbflab-0.1.0/README.md ADDED
@@ -0,0 +1,129 @@
1
+ # RBFLAB
2
+
3
+ RBFLAB solves interpolation and linear PDE problems on point clouds with radial
4
+ basis functions. Write an equation with SymPy, choose global collocation,
5
+ local Hermite interpolation (LHI), or RBF-FD, then inspect the result.
6
+
7
+ The default installation uses Python, NumPy, and SciPy. C++ and PyTorch are
8
+ optional numerical backends for supported local methods. The package supports
9
+ 2D and 3D scalar methods and divergence-free approximation spaces.
10
+
11
+ ![Three snapshots of a numerical heat pulse evolving on a point cloud](docs/assets/heat_diffusion.png)
12
+
13
+ *The heat pulse comes from a symbolic PDE solved with global RBF collocation;
14
+ the faint dots in the first panel are the collocation nodes. The display grid
15
+ only samples the computed solution.*
16
+
17
+ ## Install
18
+
19
+ From this source checkout:
20
+
21
+ ```sh
22
+ python -m pip install .
23
+ ```
24
+
25
+ The default installation does not require Gmsh, a compiler, or PyTorch. See
26
+ [the installation guide](docs/INSTALL.md) for optional features. A PyPI
27
+ release will add `python -m pip install rbflab`.
28
+
29
+ ## A symbolic PDE in a few lines
30
+
31
+ ```python
32
+ import sympy as sp
33
+ import rbflab as rbf
34
+
35
+ model = rbf.SymbolicScalar(2)
36
+ u = model.field
37
+ x, y = model.coordinates
38
+ exact = sp.sin(sp.pi*x) * sp.sin(sp.pi*y)
39
+ lhs = -model.laplacian(u)
40
+ problem = model.stationary(
41
+ sp.Eq(lhs, lhs.subs(u, exact).doit()),
42
+ boundary=[model.bc("boundary", sp.Eq(u, 0))],
43
+ )
44
+ cloud = rbf.unit_box_grid(5)
45
+ solution = problem.solve(cloud, rbf.GlobalCollocation(rbf.IMQ(2), scheme="asymmetric"))
46
+ print(solution.evaluate([[0.3, 0.4]]))
47
+ ```
48
+
49
+ Change the last method to `rbf.LHI(rbf.PHS(5), 20, polynomial_degree=2)`
50
+ or `rbf.RBFFD(rbf.PHS(5), 20, polynomial_degree=2)` to compare methods on
51
+ the same equation and cloud. The complete example reports errors against the
52
+ known solution.
53
+
54
+ ## Six starting examples
55
+
56
+ | Example | What it shows |
57
+ |---|---|
58
+ | [Symbolic Poisson](examples/symbolic_poisson.py) | One equation with global, LHI, and RBF-FD |
59
+ | [Mixed boundary](examples/symbolic_boundary.py) | Variable diffusion and a Robin condition |
60
+ | [Heat](examples/symbolic_heat.py) | Symbolic time evolution |
61
+ | [Stokes spaces](examples/stokes_spaces.py) | Divergence-free velocity and pressure gradient |
62
+ | [3D operators](examples/operators_3d.py) | Sparse matrices, local reconstruction, backend choice |
63
+ | [Custom kernel](examples/custom_kernel.py) | Symbolic kernel definition and optional C++ cache |
64
+
65
+ Run, for example, `python examples/symbolic_poisson.py --method lhi` after
66
+ installing the package. The [capability table](docs/CAPABILITIES.md) identifies
67
+ which methods and backends are supported. These small examples use deterministic
68
+ points and do not require mesh generation.
69
+
70
+ ## Plot a result
71
+
72
+ Install the optional plotting tools with `python -m pip install "rbflab[examples]"`.
73
+ The plotting code is separate from the numerical solver:
74
+
75
+ ```python
76
+ from rbflab import viz
77
+
78
+ fig, ax = viz.plot_scalar(solution, title="My solution")
79
+ fig.savefig("solution.png", dpi=160)
80
+ # For a time-dependent result: viz.animate_scalar(trajectory, "heat.gif")
81
+ ```
82
+
83
+ ![Animated heat pulse solved by RBFLAB](docs/assets/heat_diffusion.gif)
84
+
85
+ The [gallery generator](examples/make_gallery.py) rebuilds this animation and
86
+ the figures with `python -m examples.make_gallery`. It also plots the solved
87
+ divergence-free Stokes velocity field:
88
+
89
+ ![Speed and streamlines from the steady Stokes example](docs/assets/stokes_velocity.png)
90
+
91
+ See the short [plotting guide](docs/VISUALIZATION.md) for the plotting calls and
92
+ their 2D scope.
93
+
94
+ ## A moving fluid: Navier–Stokes cavity
95
+
96
+ From a source checkout, the [cavity example](examples/navier_stokes_cavity.py)
97
+ solves a Re=100 lid-driven flow with staggered RBF-FD and makes an animation
98
+ in one command:
99
+
100
+ ```sh
101
+ python -m examples.navier_stokes_cavity
102
+ ```
103
+
104
+ ![Final Re=100 cavity velocity and streamlines](docs/assets/navier_stokes_cavity.png)
105
+
106
+ ![Re=100 lid-driven cavity startup](docs/assets/navier_stokes_cavity.gif)
107
+
108
+ It uses a unit square built directly from NumPy, PHS7 kernels with degree-three
109
+ polynomials, and a coupled velocity–pressure time step. The
110
+ [cavity guide](docs/guides/LID_DRIVEN_CAVITY.md) reports the refinement comparison and the
111
+ remaining divergence defect. The animation interpolates nodal values solely
112
+ for display. The final frame is also saved as a PNG.
113
+
114
+ The core library lives in `python/rbflab/`; runnable examples are in
115
+ `examples/`, and focused contributor tests are in `tests/`.
116
+
117
+ ## Numerical scope
118
+
119
+ The Stokes example uses a divergence-free velocity kernel: incompressibility
120
+ is built into the approximation space. LHI reconstructs pressure gradients
121
+ locally; it does not automatically produce a globally normalized pressure.
122
+ Off-node LHI evaluation currently uses the nearest stencil and can have a
123
+ different error from nodal values. Report solution error, algebraic residual,
124
+ and PDE residual separately.
125
+
126
+ The cavity showcase is not validation of a general Navier–Stokes solver.
127
+
128
+ See [installation](docs/INSTALL.md), [capabilities](docs/CAPABILITIES.md),
129
+ the [MIT license](LICENSE), and the [release checklist](RELEASE_CHECKLIST.md).
@@ -0,0 +1,49 @@
1
+ # RBFLAB
2
+
3
+ RBFLAB is a Python library for radial basis function interpolation and PDEs on
4
+ point clouds. It supports global collocation, local Hermite interpolation
5
+ (LHI), RBF-FD, symbolic linear operators and boundary conditions, and
6
+ divergence-free velocity spaces. Scalar operators work in 2D and 3D.
7
+
8
+ Source and examples: https://github.com/LDBreton/RBFLAB
9
+
10
+ Install the Python core with:
11
+
12
+ ```sh
13
+ python -m pip install rbflab
14
+ ```
15
+
16
+ Optional extras include `rbflab[examples]` for Matplotlib plots and GIFs,
17
+ `rbflab[mesh]` for Gmsh, and `rbflab[torch]` for the documented CPU Float64
18
+ tensor backend. The C++ double/MPFR backends are source-build options.
19
+
20
+ Here is a symbolic Poisson problem:
21
+
22
+ ```python
23
+ import sympy as sp
24
+ import rbflab as rbf
25
+
26
+ model = rbf.SymbolicScalar(2)
27
+ u = model.field
28
+ x, y = model.coordinates
29
+ exact = sp.sin(sp.pi*x) * sp.sin(sp.pi*y)
30
+ lhs = -model.laplacian(u)
31
+ problem = model.stationary(
32
+ sp.Eq(lhs, lhs.subs(u, exact).doit()),
33
+ boundary=[model.bc("boundary", sp.Eq(u, 0))],
34
+ )
35
+ cloud = rbf.unit_box_grid(5)
36
+ solution = problem.solve(
37
+ cloud, rbf.GlobalCollocation(rbf.IMQ(2), scheme="asymmetric")
38
+ )
39
+ print(solution.evaluate([[0.3, 0.4]]))
40
+ ```
41
+
42
+ The source repository also contains short heat, Stokes, 3D operator, custom
43
+ kernel, and Re=100 lid-driven cavity examples. The cavity example is a
44
+ showcase with a documented nonzero discrete divergence defect; it does not
45
+ validate a general Navier–Stokes solver. LHI off-node reconstruction has its
46
+ own accuracy limitations, and MPFR local weights do not automatically make a
47
+ global sparse solve extended precision.
48
+
49
+ RBFLAB is licensed under MIT.
@@ -0,0 +1,51 @@
1
+ # First PyPI release checklist
2
+
3
+ This directory is a fresh public source snapshot. It has no commit history from the
4
+ private research repository and contains no legacy reference code, experiments,
5
+ or historical research outputs. The README media are regenerated by the
6
+ included examples.
7
+
8
+ ## Prepared
9
+
10
+ - MIT text in `LICENSE` and SPDX metadata in `pyproject.toml`.
11
+ - Core package, optional C++ source, focused examples/tests, and README media.
12
+ - Separate text-only `README_PYPI.md` keeps local image paths out of PyPI
13
+ metadata while the repository URL is not yet known.
14
+ - `.github/workflows/publish-pypi.yml` builds, tests, checks metadata, and
15
+ publishes a matching non-prerelease GitHub release through PyPI Trusted
16
+ Publishing. It has no API token in the repository.
17
+ - Windows Python 3.12: fresh install, source archive and wheel builds, and
18
+ focused tests passed (60 passed; 29 optional checks skipped).
19
+ - WSL Ubuntu Python 3.12: editable install and focused tests passed (60 passed;
20
+ 29 optional checks skipped). Both final distributions passed `twine check`;
21
+ the PyPI README example ran successfully. A fresh Linux wheel install ran
22
+ symbolic Poisson, divergence-free Stokes, and 3D operator examples.
23
+ - The selected source has one Git author in the private working history and no
24
+ embedded third-party license notices; that is evidence, not a legal rights
25
+ determination for any material adapted from older projects.
26
+ - PyPI's `rbflab` JSON endpoint returned HTTP 404 on 2026-10-08. This is not a
27
+ reservation or guarantee of availability.
28
+ - The public repository is `https://github.com/LDBreton/RBFLAB`; package
29
+ metadata points to that URL. The user confirmed MIT release rights for the
30
+ selected code and generated images on 2026-10-08.
31
+ - Its [first GitHub release smoke run](https://github.com/LDBreton/RBFLAB/actions/runs/37827256816)
32
+ passed all six jobs: Python 3.11/3.12
33
+ installed-wheel examples on Windows and Ubuntu, native C++ local operators,
34
+ and gallery generation. The workflows were then updated to current
35
+ Node 24-based actions and a pinned Ubuntu 24.04 runner. The
36
+ [updated six-job run](https://github.com/LDBreton/RBFLAB/actions/runs/37827767996)
37
+ also passed.
38
+ - The GitHub `pypi` environment requires `LDBreton` as a reviewer. A private
39
+ draft `v0.1.0` release contains the wheel and source archive from the
40
+ CI-tested commit.
41
+
42
+ ## Before PyPI upload
43
+
44
+ - Confirm the `rbflab` name remains available on PyPI.
45
+ - In PyPI, configure a pending Trusted Publisher for package `rbflab`, owner
46
+ `LDBreton`, repository `RBFLAB`, workflow `publish-pypi.yml`, and environment
47
+ `pypi`.
48
+ - Review the draft release and publish it only after the Trusted Publisher is
49
+ configured. Publishing the GitHub Release triggers the PyPI workflow; approve
50
+ its `pypi` environment job when ready. Then verify the uploaded distributions
51
+ and update the repository README installation text.
@@ -0,0 +1,60 @@
1
+ # C++ local numerical backend
2
+
3
+ The production LHI local factorization is Eigen `PartialPivLU`. Float64 uses
4
+ `Eigen::Matrix<double,...>`; arbitrary precision uses the existing MPFR `Real`
5
+ wrapper with `Eigen::NumTraits<Real>`. No conversion through double occurs in
6
+ MPFR factorization or triangular solves. Shape policy, kernel assembly,
7
+ symmetric row-max equilibration and reconstruction formulas are unchanged.
8
+
9
+ OpenMP parallelizes stencils. Eigen internal parallelism is disabled. MPFR
10
+ precision is thread-local and set in each worker before allocating matrices.
11
+ A factorization serves all four reconstruction RHS and the optional inverse
12
+ used to estimate the infinity-norm condition number. Exact zero pivots raise
13
+ an error; LU is not a rank-revealing solver and does not regularize ill-conditioned
14
+ matrices. Eigen cannot recover digits lost to an excessively flat Float64 kernel.
15
+
16
+ ## Dependencies and build
17
+
18
+ GCC, OpenMP, MPFR/GMP runtime libraries and development headers are required.
19
+ Eigen 3.4.0 was validated. On Ubuntu/WSL, extract the distribution's
20
+ development packages into the ignored project dependency directory:
21
+
22
+ ```sh
23
+ bash cpp/prepare_dependencies.sh
24
+ ```
25
+
26
+ From the repository root, using its Python environment:
27
+
28
+ ```sh
29
+ python -m examples.build_cpp_backend
30
+ RBFLAB_CPP_TESTS=1 python -m pytest tests/test_discrete_operators.py tests/test_symbolic_kernel.py -q
31
+ ```
32
+
33
+ On PowerShell set `$env:RBFLAB_CPP_TESTS='1'` before invoking pytest.
34
+ The public `CppBackend(threads=..., compute_condition=...)` API is unchanged.
35
+ `Precision(local_digits=None)` selects Float64; `Precision(local_digits=80)`
36
+ selects MPFR for local assembly/solves. Global precision remains separately
37
+ controlled by `global_dtype`.
38
+
39
+ Custom symbolic Stokes kernels compile against the same Eigen solver.
40
+ The adapter and Eigen header content hashes participate in cache identity;
41
+ previous custom binaries cannot be reused accidentally. Build manifests record
42
+ the Eigen header hash. Dependencies and generated executables remain untracked.
43
+ Off-node lazy Python reconstruction and global sparse solvers are unchanged.
44
+
45
+ ## Optional Float64 SVD
46
+
47
+ `CppBackend(local_solver="svd", svd_rcond=None)` selects Eigen JacobiSVD.
48
+ `svd_rcond` is the relative singular-value cutoff; `None` uses Eigen's default
49
+ (matrix dimension times machine epsilon). Set an explicit value such as
50
+ `1e-12` when testing regularization. MPFR currently supports LU only.
51
+ SVD solves the equilibrated system with a truncated pseudoinverse; this can
52
+ change the discretization substantially. It is not an automatic fallback.
53
+
54
+ Diagnostics include each stencil's retained rank, dimension, relative cutoff,
55
+ and condition number from the untruncated computed singular values. If condition
56
+ reporting is enabled, SVD reports the 2-norm condition, whereas LU reports the
57
+ infinity-norm estimate. Tiny singular values of an unresolved Float64 matrix
58
+ cannot certify its mathematical condition number. Lazy off-node reconstruction
59
+ uses NumPy SVD with the same scaling and cutoff; kernel evaluation/rounding may
60
+ differ from C++, as in the existing Python reconstruction path.
@@ -0,0 +1,78 @@
1
+ // Experimental Float64 local Stokes assembly + cuBLAS batched pivoted LU.
2
+ #include <cuda_runtime.h>
3
+ #include <cublas_v2.h>
4
+ #include <vector>
5
+ #include <map>
6
+ #include <fstream>
7
+ #include <iostream>
8
+ #include <iomanip>
9
+ #include <cmath>
10
+ #include <chrono>
11
+ #include <stdexcept>
12
+ #include <algorithm>
13
+ #include <string>
14
+ // RBFLAB_GENERATED_SPACE
15
+ void check(cudaError_t e){if(e!=cudaSuccess)throw std::runtime_error(cudaGetErrorString(e));}
16
+ void blas(cublasStatus_t e){if(e!=CUBLAS_STATUS_SUCCESS)throw std::runtime_error("cuBLAS status "+std::to_string(e));}
17
+ using Clock=std::chrono::steady_clock;
18
+ double elapsed(Clock::time_point t){return std::chrono::duration<double>(Clock::now()-t).count();}
19
+ struct Node{int op;double x,y;};
20
+ struct Task{double mu,x,y;std::vector<double> v,p;std::vector<Node> nodes;};
21
+ template<class T>struct Device{
22
+ T* p=nullptr;size_t count;
23
+ explicit Device(size_t n):count(n){check(cudaMalloc((void**)&p,sizeof(T)*n));}
24
+ ~Device(){if(p)cudaFree(p);}
25
+ Device(const Device&)=delete;Device& operator=(const Device&)=delete;
26
+ void upload(const T* src){check(cudaMemcpy(p,src,count*sizeof(T),cudaMemcpyHostToDevice));}
27
+ void download(T* dst){check(cudaMemcpy(dst,p,count*sizeof(T),cudaMemcpyDeviceToHost));}
28
+ };
29
+ __global__ void assemble(int n,int count,int nv,int np,const Node* nodes,const double* params,const double* meta,double* G,double* B){
30
+ int total=count*n*(n+4);
31
+ for(int index=blockIdx.x*blockDim.x+threadIdx.x;index<total;index+=blockDim.x*gridDim.x){
32
+ int t=index/(n*(n+4)),entry=index%(n*(n+4));
33
+ KernelSpec v{params+t*(nv+np)},p{params+t*(nv+np)+nv};const Node* pts=nodes+t*n;
34
+ if(entry<n*n){int row=entry%n,col=entry/n;if(row<col)continue;
35
+ double value=custom_eval(pts[col].op,pts[row].op,pts[row].x-pts[col].x,pts[row].y-pts[col].y,meta[3*t],v,p);
36
+ G[t*n*n+col*n+row]=value;G[t*n*n+row*n+col]=value;
37
+ }else{int j=entry-n*n,row=j%n,target=j/n;
38
+ B[t*n*4+target*n+row]=custom_eval(pts[row].op,target+5,meta[3*t+1]-pts[row].x,meta[3*t+2]-pts[row].y,meta[3*t],v,p);
39
+ }
40
+ }
41
+ }
42
+ __global__ void scales(int n,int count,const double* G,double* scale){
43
+ for(int i=blockIdx.x*blockDim.x+threadIdx.x;i<count*n;i+=gridDim.x*blockDim.x){int t=i/n,row=i%n;double mx=0;for(int j=0;j<n;j++)mx=fmax(mx,fabs(G[t*n*n+j*n+row]));scale[i]=mx>0?1/sqrt(mx):nan("");}
44
+ }
45
+ __global__ void equilibrate(int n,int count,const double* G,const double* B,const double* scale,double* A,double* R){
46
+ for(int i=blockIdx.x*blockDim.x+threadIdx.x;i<count*n*(n+4);i+=gridDim.x*blockDim.x){int t=i/(n*(n+4)),j=i%(n*(n+4));if(j<n*n)A[t*n*n+j]=G[t*n*n+j]*scale[t*n+j%n]*scale[t*n+j/n];else{j-=n*n;R[t*n*4+j]=B[t*n*4+j]*scale[t*n+j%n];}}
47
+ }
48
+ __global__ void unscale(int n,int count,const double* scale,double* W){for(int i=blockIdx.x*blockDim.x+threadIdx.x;i<count*n*4;i+=gridDim.x*blockDim.x){int t=i/(n*4);W[i]*=scale[t*n+i%n];}}
49
+ __global__ void residuals(int n,int count,const double* G,const double* B,const double* W,double* error,double* denom){
50
+ for(int i=blockIdx.x*blockDim.x+threadIdx.x;i<count*n*4;i+=gridDim.x*blockDim.x){int t=i/(n*4),j=i%(n*4),row=j%n,k=j/n;double d=-B[i];for(int col=0;col<n;col++)d+=G[t*n*n+col*n+row]*W[t*n*4+k*n+col];double a=isfinite(d)?fabs(d):INFINITY;atomicMax((unsigned long long*)(error+t),(unsigned long long)__double_as_longlong(a));atomicMax((unsigned long long*)(denom+t),(unsigned long long)__double_as_longlong(fabs(B[i])));}
51
+ }
52
+ int main(int argc,char**argv){try{
53
+ if(argc!=3)throw std::runtime_error("Usage: cuda_lhi INPUT OUTPUT");
54
+ std::ifstream in(argv[1]);int count,chunk,nv,np;if(!(in>>count>>chunk>>nv>>np)||count<1||chunk<1||nv<0||np<0)throw std::runtime_error("Invalid header");
55
+ std::vector<Task> tasks(count);std::map<int,std::vector<int>> groups;
56
+ for(int i=0;i<count;i++){auto&t=tasks[i];int n;in>>n>>t.mu>>t.x>>t.y;if(n<1||n>4096)throw std::runtime_error("Invalid matrix size");t.v.resize(nv);t.p.resize(np);for(auto&v:t.v)in>>v;for(auto&v:t.p)in>>v;t.nodes.resize(n);for(auto&v:t.nodes)in>>v.op>>v.x>>v.y;if(!in)throw std::runtime_error("Invalid task input");groups[n].push_back(i);}
57
+ auto initial=Clock::now();check(cudaFree(0));cudaDeviceProp prop;check(cudaGetDeviceProperties(&prop,0));cublasHandle_t handle;blas(cublasCreate(&handle));double init=elapsed(initial);
58
+ std::vector<std::vector<double>> weights(count);std::vector<double> residual(count);double h2d=0,assembly=0,solve=0,d2h=0;int batches=0;
59
+ auto begin=Clock::now();
60
+ for(auto&group:groups){int n=group.first;auto&ids=group.second;
61
+ for(size_t offset=0;offset<ids.size();offset+=chunk){int b=std::min(size_t(chunk),ids.size()-offset);batches++;
62
+ std::vector<Node> nodes;std::vector<double> params,meta;
63
+ for(int j=0;j<b;j++){auto&t=tasks[ids[offset+j]];nodes.insert(nodes.end(),t.nodes.begin(),t.nodes.end());params.insert(params.end(),t.v.begin(),t.v.end());params.insert(params.end(),t.p.begin(),t.p.end());meta.insert(meta.end(),{t.mu,t.x,t.y});}
64
+ Device<Node> dn(b*n);Device<double> dp(std::max(1,b*(nv+np))),dm(b*3),G(size_t(b)*n*n),A(size_t(b)*n*n),B(b*n*4),W(b*n*4),scale(b*n),error(b),denom(b);Device<int> piv(b*n),info(b);Device<double*> aa(b),ww(b);
65
+ std::vector<double*> ap(b),wp(b);for(int j=0;j<b;j++){ap[j]=A.p+size_t(j)*n*n;wp[j]=W.p+j*n*4;}
66
+ auto t=Clock::now();dn.upload(nodes.data());if(!params.empty())check(cudaMemcpy(dp.p,params.data(),params.size()*sizeof(double),cudaMemcpyHostToDevice));dm.upload(meta.data());aa.upload(ap.data());ww.upload(wp.data());check(cudaMemset(error.p,0,b*sizeof(double)));check(cudaMemset(denom.p,0,b*sizeof(double)));h2d+=elapsed(t);
67
+ int blocks=std::min(4096,(b*n*(n+4)+127)/128);t=Clock::now();
68
+ assemble<<<blocks,128>>>(n,b,nv,np,dn.p,dp.p,dm.p,G.p,B.p);scales<<<blocks,128>>>(n,b,G.p,scale.p);equilibrate<<<blocks,128>>>(n,b,G.p,B.p,scale.p,A.p,W.p);check(cudaGetLastError());check(cudaDeviceSynchronize());assembly+=elapsed(t);
69
+ t=Clock::now();blas(cublasDgetrfBatched(handle,n,aa.p,n,piv.p,info.p,b));std::vector<int> status(b);info.download(status.data());for(int j=0;j<b;j++)if(status[j])throw std::runtime_error("LU failure at stencil "+std::to_string(ids[offset+j])+", info="+std::to_string(status[j]));int result=0;blas(cublasDgetrsBatched(handle,CUBLAS_OP_N,n,4,(const double**)aa.p,n,piv.p,ww.p,n,&result,b));if(result)throw std::runtime_error("Batched solve failed");unscale<<<blocks,128>>>(n,b,scale.p,W.p);residuals<<<blocks,128>>>(n,b,G.p,B.p,W.p,error.p,denom.p);check(cudaGetLastError());check(cudaDeviceSynchronize());solve+=elapsed(t);
70
+ t=Clock::now();std::vector<double> host(b*n*4),err(b),norm(b);W.download(host.data());error.download(err.data());denom.download(norm.data());d2h+=elapsed(t);
71
+ for(int j=0;j<b;j++){int id=ids[offset+j];weights[id].assign(host.begin()+j*n*4,host.begin()+(j+1)*n*4);residual[id]=err[j]/(norm[j]?norm[j]:1);if(!std::isfinite(residual[id]))throw std::runtime_error("Nonfinite residual");for(double v:weights[id])if(!std::isfinite(v))throw std::runtime_error("Nonfinite weight");}
72
+ }
73
+ }
74
+ double compute=elapsed(begin);blas(cublasDestroy(handle));
75
+ std::ofstream out(argv[2]);out<<std::setprecision(17);for(int i=0;i<count;i++){out<<i<<" "<<tasks[i].nodes.size()<<" none "<<residual[i];for(double v:weights[i])out<<" "<<v;out<<"\n";}if(!out)throw std::runtime_error("Output write failed");
76
+ std::cout<<std::setprecision(9)<<"{\"device\":\""<<prop.name<<"\",\"context_seconds\":"<<init<<",\"compute_seconds\":"<<compute<<",\"h2d_seconds\":"<<h2d<<",\"assembly_seconds\":"<<assembly<<",\"solve_seconds\":"<<solve<<",\"d2h_seconds\":"<<d2h<<",\"groups\":"<<groups.size()<<",\"batches\":"<<batches<<"}\n";
77
+ return 0;
78
+ }catch(const std::exception&e){std::cerr<<e.what()<<"\n";return 1;}}
@@ -0,0 +1,92 @@
1
+ #pragma once
2
+ // Keep parallelism across stencils; Eigen must not create nested workers.
3
+ #define EIGEN_DONT_PARALLELIZE
4
+ #include "real.hpp"
5
+ #ifndef RBFLAB_DOUBLE
6
+ inline bool operator!=(const Real&a,const Real&b){return !(a==b);}
7
+ inline bool operator<=(const Real&a,const Real&b){return !(a>b);}
8
+ inline bool operator>=(const Real&a,const Real&b){return !(a<b);}
9
+ inline Real abs2(const Real&a){return a*a;}
10
+ inline Real conj(const Real&a){return a;}
11
+ inline Real real(const Real&a){return a;}
12
+ inline Real imag(const Real&){return Real(0);}
13
+ #endif
14
+ #include <Eigen/Core>
15
+ #ifndef RBFLAB_DOUBLE
16
+ namespace Eigen {
17
+ template<> struct NumTraits<::Real>:GenericNumTraits<::Real> {
18
+ using Real=::Real; using NonInteger=::Real; using Nested=::Real; using Literal=::Real;
19
+ enum {IsComplex=0,IsInteger=0,IsSigned=1,RequireInitialization=1,ReadCost=10,AddCost=20,MulCost=40};
20
+ static Real epsilon(){Real x(1);mpfr_div_2si(x.v,x.v,::Real::bits-1,MPFR_RNDN);return x;}
21
+ static Real dummy_precision(){return sqrt(epsilon());}
22
+ static int digits10(){return int((::Real::bits-1)*0.3010299956639812);}
23
+ };
24
+ }
25
+ #endif
26
+ #include <Eigen/LU>
27
+ #include <vector>
28
+ #ifdef RBFLAB_DOUBLE
29
+ using EigenScalar=double;
30
+ inline double eigen_value(const Real&x){return x.v;}
31
+ #else
32
+ using EigenScalar=Real;
33
+ inline const Real& eigen_value(const Real&x){return x;}
34
+ #endif
35
+ // Own the factorization once and reuse it for all reconstruction/diagnostic RHS.
36
+ struct EigenLocalLU {
37
+ using Dense=Eigen::Matrix<EigenScalar,Eigen::Dynamic,Eigen::Dynamic>;
38
+ using Rows=std::vector<std::vector<Real>>;
39
+ Eigen::PartialPivLU<Dense> factor;
40
+ static Dense dense(const Rows& rows){
41
+ Dense a(rows.size(),rows.at(0).size());
42
+ for(int i=0;i<a.rows();++i)for(int j=0;j<a.cols();++j)a(i,j)=eigen_value(rows[i][j]);
43
+ return a;
44
+ }
45
+ explicit EigenLocalLU(const Rows& a):factor(dense(a)){
46
+ for(int i=0;i<factor.matrixLU().rows();++i)
47
+ if(factor.matrixLU()(i,i)==EigenScalar(0))throw std::runtime_error("Singular local matrix at selected precision");
48
+ }
49
+ Rows solve(const Rows& b)const{
50
+ Dense x=factor.solve(dense(b));
51
+ Rows result(x.rows(),std::vector<Real>(x.cols()));
52
+ for(int i=0;i<x.rows();++i)for(int j=0;j<x.cols();++j)result[i][j]=Real(x(i,j));
53
+ return result;
54
+ }
55
+ };
56
+
57
+ #include <memory>
58
+ #ifdef RBFLAB_DOUBLE
59
+ #include <Eigen/SVD>
60
+ #endif
61
+ struct EigenLocalSolver {
62
+ using Rows=EigenLocalLU::Rows;
63
+ std::unique_ptr<EigenLocalLU> lu;
64
+ #ifdef RBFLAB_DOUBLE
65
+ Eigen::JacobiSVD<EigenLocalLU::Dense> svd;
66
+ #endif
67
+ int size,rank; double threshold=0,condition2=0;
68
+ EigenLocalSolver(const Rows& a,const std::string& solver,double cutoff):size(a.size()),rank(size){
69
+ if(solver=="lu"){lu=std::make_unique<EigenLocalLU>(a);return;}
70
+ #ifdef RBFLAB_DOUBLE
71
+ svd.compute(EigenLocalLU::dense(a),Eigen::ComputeThinU|Eigen::ComputeThinV);
72
+ if(svd.info()!=Eigen::Success)throw std::runtime_error("Local SVD failed");
73
+ if(cutoff>=0)svd.setThreshold(cutoff);
74
+ threshold=svd.threshold();rank=svd.rank();
75
+ const auto& values=svd.singularValues();
76
+ condition2=values(size-1)>0 ? values(0)/values(size-1) : std::numeric_limits<double>::infinity();
77
+ #else
78
+ throw std::runtime_error("SVD local solver currently requires Float64");
79
+ #endif
80
+ }
81
+ Rows solve(const Rows& b)const{
82
+ if(lu)return lu->solve(b);
83
+ #ifdef RBFLAB_DOUBLE
84
+ EigenLocalLU::Dense x=svd.solve(EigenLocalLU::dense(b));
85
+ Rows result(x.rows(),std::vector<Real>(x.cols()));
86
+ for(int i=0;i<x.rows();++i)for(int j=0;j<x.cols();++j)result[i][j]=Real(x(i,j));
87
+ return result;
88
+ #else
89
+ throw std::runtime_error("SVD local solver currently requires Float64");
90
+ #endif
91
+ }
92
+ };