sTiles 2026.7.19__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.
- stiles-2026.7.19/PKG-INFO +16 -0
- stiles-2026.7.19/README.md +39 -0
- stiles-2026.7.19/pyproject.toml +35 -0
- stiles-2026.7.19/sTiles/__init__.py +40 -0
- stiles-2026.7.19/sTiles/_ffi.py +331 -0
- stiles-2026.7.19/sTiles/_version.py +3 -0
- stiles-2026.7.19/sTiles/core.py +473 -0
- stiles-2026.7.19/sTiles.egg-info/PKG-INFO +16 -0
- stiles-2026.7.19/sTiles.egg-info/SOURCES.txt +11 -0
- stiles-2026.7.19/sTiles.egg-info/dependency_links.txt +1 -0
- stiles-2026.7.19/sTiles.egg-info/requires.txt +2 -0
- stiles-2026.7.19/sTiles.egg-info/top_level.txt +1 -0
- stiles-2026.7.19/setup.cfg +4 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sTiles
|
|
3
|
+
Version: 2026.7.19
|
|
4
|
+
Summary: Python bindings for the sTiles sparse Cholesky / selected-inverse framework
|
|
5
|
+
Author-email: Esmail Abdul Fattah <esmail.abdulfattah@kaust.edu.sa>
|
|
6
|
+
Project-URL: Homepage, https://esmail-abdulfattah.github.io/sTiles/
|
|
7
|
+
Project-URL: Repository, https://github.com/esmail-abdulfattah/sTiles
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Requires-Dist: numpy>=1.20
|
|
16
|
+
Requires-Dist: scipy>=1.5
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# pysTiles
|
|
2
|
+
|
|
3
|
+
Python bindings for the **sTiles** sparse Cholesky / selected-inverse
|
|
4
|
+
framework. Pure `ctypes` over the prebuilt `libstiles` shared object — no
|
|
5
|
+
compiler or build step on the user's machine.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
import scipy.sparse as sp
|
|
9
|
+
from pysTiles import sTiles
|
|
10
|
+
|
|
11
|
+
with sTiles(Q, cores=4, mode="auto", inverse=True) as s:
|
|
12
|
+
s.logdet # log|Q|
|
|
13
|
+
s.selinv_diag() # diag(Q^-1) -> marginal variances
|
|
14
|
+
s.solve(b) # Q x = b
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Reuse the preprocessing (one symbolic analysis, many numeric factorizations):
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
s = sTiles(Q0, cores=4, inverse=True)
|
|
21
|
+
for theta in grid:
|
|
22
|
+
s.update_values(build_Q(theta)) # reuses ordering + layout
|
|
23
|
+
ll = ... s.logdet ...
|
|
24
|
+
s.close()
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install .
|
|
31
|
+
# if the library isn't bundled or in a dev checkout:
|
|
32
|
+
export STILES_LIB=/path/to/libstiles.so
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The library is located via `$STILES_LIB`, `$STILES_LIB_DIR`, a bundled
|
|
36
|
+
`pysTiles/_libs/<os>-<arch>/` binary, or a repo `lib/libstiles.so` fallback.
|
|
37
|
+
|
|
38
|
+
See [`../README.md`](../README.md) for the full API table, the R binding, and
|
|
39
|
+
CI/packaging notes. Run the self-check with `python examples/quickstart.py`.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "sTiles"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python bindings for the sTiles sparse Cholesky / selected-inverse framework"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
authors = [{ name = "Esmail Abdul Fattah", email = "esmail.abdulfattah@kaust.edu.sa" }]
|
|
11
|
+
dependencies = ["numpy>=1.20", "scipy>=1.5"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"License :: Other/Proprietary License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Intended Audience :: Science/Research",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Mathematics",
|
|
17
|
+
"Operating System :: POSIX :: Linux",
|
|
18
|
+
"Operating System :: MacOS",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://esmail-abdulfattah.github.io/sTiles/"
|
|
23
|
+
Repository = "https://github.com/esmail-abdulfattah/sTiles"
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
packages = ["sTiles"]
|
|
27
|
+
|
|
28
|
+
[tool.setuptools.dynamic]
|
|
29
|
+
version = { attr = "sTiles._version.__version__" }
|
|
30
|
+
|
|
31
|
+
[tool.setuptools.package-data]
|
|
32
|
+
# Prebuilt libstiles binaries dropped in by CI (per platform sub-directory).
|
|
33
|
+
# These are large and are NOT committed to git; a release wheel bundles the
|
|
34
|
+
# matching one. Layout: _libs/<os>-<arch>/libstiles.{so,dylib} (see sync_binaries.sh).
|
|
35
|
+
sTiles = ["_libs/**/*.so", "_libs/**/*.dylib", "_libs/*.so", "_libs/*.dylib"]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
sTiles -- Python bindings for the sTiles sparse Cholesky / selected-inverse
|
|
3
|
+
framework (KAUST).
|
|
4
|
+
|
|
5
|
+
Loads the prebuilt ``libstiles`` shared object via ctypes; no build step and no
|
|
6
|
+
compiler required on the user's machine.
|
|
7
|
+
|
|
8
|
+
Quick start
|
|
9
|
+
-----------
|
|
10
|
+
import scipy.sparse as sp
|
|
11
|
+
from sTiles import sTiles
|
|
12
|
+
|
|
13
|
+
with sTiles(Q, cores=4, inverse=True) as s:
|
|
14
|
+
s.logdet
|
|
15
|
+
s.selinv_diag() # diag(Q^-1)
|
|
16
|
+
s.solve(b) # Q x = b
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .core import sTiles, factorize, version, library_path
|
|
20
|
+
from .core import (
|
|
21
|
+
MODE_AUTO,
|
|
22
|
+
MODE_DENSE,
|
|
23
|
+
MODE_SEMISPARSE,
|
|
24
|
+
MODE_SPARSE,
|
|
25
|
+
sTilesError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"sTiles",
|
|
30
|
+
"factorize",
|
|
31
|
+
"version",
|
|
32
|
+
"library_path",
|
|
33
|
+
"sTilesError",
|
|
34
|
+
"MODE_AUTO",
|
|
35
|
+
"MODE_DENSE",
|
|
36
|
+
"MODE_SEMISPARSE",
|
|
37
|
+
"MODE_SPARSE",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
from ._version import __version__ # date-based CalVer, YYYY.M.D
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low-level ctypes binding to libstiles.
|
|
3
|
+
|
|
4
|
+
This module does exactly three things:
|
|
5
|
+
|
|
6
|
+
1. Locate the shared library (``libstiles.so`` on Linux, ``libstiles.dylib``
|
|
7
|
+
on macOS) for the current platform.
|
|
8
|
+
2. ``ctypes.CDLL`` it.
|
|
9
|
+
3. Declare the ``argtypes`` / ``restype`` for every ``sTiles_*`` entry point
|
|
10
|
+
the high-level wrapper uses.
|
|
11
|
+
|
|
12
|
+
Everything user-facing lives in :mod:`sTiles.core`; this file is the raw
|
|
13
|
+
FFI surface and has no NumPy/SciPy dependency.
|
|
14
|
+
|
|
15
|
+
Library search order (first hit wins)
|
|
16
|
+
-------------------------------------
|
|
17
|
+
1. ``$STILES_LIB`` -- full path to the shared object.
|
|
18
|
+
2. ``$STILES_LIB_DIR`` -- directory containing it.
|
|
19
|
+
3. ``$STILES_BINARIES_DIR`` -- a CI-artifact tree with
|
|
20
|
+
``libstiles-<plat>/lib/libstiles.{so,dylib}``.
|
|
21
|
+
4. a ``binaries/`` (or ``bindings/binaries/``) directory in any ancestor,
|
|
22
|
+
using the same CI-artifact layout -- so unzipped GitHub Actions artifacts
|
|
23
|
+
dropped in ``bindings/binaries/`` are found with zero configuration.
|
|
24
|
+
5. bundled ``_libs/<plat>/`` -- shipped inside the wheel / R package by CI.
|
|
25
|
+
6. repo dev fallback -- ``lib/libstiles.{so,dylib}`` in an ancestor.
|
|
26
|
+
7. GitHub Release download -- the matching platform library is fetched from
|
|
27
|
+
the project's Release assets and cached (this
|
|
28
|
+
is what makes ``pip install`` work). Disable
|
|
29
|
+
with ``$STILES_NO_DOWNLOAD=1``.
|
|
30
|
+
|
|
31
|
+
The library built for Linux embeds MKL/SCOTCH/METIS statically and localizes
|
|
32
|
+
their symbols, so a plain ``CDLL`` is safe even inside a process that already
|
|
33
|
+
loaded its own MKL (e.g. R, or MKL-backed NumPy/SciPy). The macOS build links Homebrew OpenBLAS +
|
|
34
|
+
LAPACK, so those must be discoverable at load time on a Mac.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import ctypes
|
|
40
|
+
import os
|
|
41
|
+
import platform
|
|
42
|
+
import shutil
|
|
43
|
+
import sys
|
|
44
|
+
import tempfile
|
|
45
|
+
import urllib.request
|
|
46
|
+
import zipfile
|
|
47
|
+
from ctypes import (
|
|
48
|
+
POINTER,
|
|
49
|
+
c_bool,
|
|
50
|
+
c_char_p,
|
|
51
|
+
c_double,
|
|
52
|
+
c_int,
|
|
53
|
+
c_longlong,
|
|
54
|
+
c_void_p,
|
|
55
|
+
)
|
|
56
|
+
from pathlib import Path
|
|
57
|
+
|
|
58
|
+
__all__ = ["lib", "library_path", "c_int_p", "c_double_p", "c_bool_p"]
|
|
59
|
+
|
|
60
|
+
c_int_p = POINTER(c_int)
|
|
61
|
+
c_double_p = POINTER(c_double)
|
|
62
|
+
c_bool_p = POINTER(c_bool)
|
|
63
|
+
|
|
64
|
+
# Suppress the one-time ASCII banner libstiles prints on first use. Set before
|
|
65
|
+
# the library is loaded so its getenv() sees it; overridable by exporting
|
|
66
|
+
# STILES_NO_BANNER=0 for users who want the banner.
|
|
67
|
+
os.environ.setdefault("STILES_NO_BANNER", "1")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _platform_tag() -> str:
|
|
71
|
+
"""Return the ``<os>-<arch>`` sub-directory name used for bundled libs."""
|
|
72
|
+
machine = platform.machine().lower()
|
|
73
|
+
arch = {
|
|
74
|
+
"x86_64": "x86_64",
|
|
75
|
+
"amd64": "x86_64",
|
|
76
|
+
"arm64": "arm64",
|
|
77
|
+
"aarch64": "arm64",
|
|
78
|
+
}.get(machine, machine)
|
|
79
|
+
if sys.platform == "darwin":
|
|
80
|
+
return f"macos-{arch}"
|
|
81
|
+
if sys.platform.startswith("linux"):
|
|
82
|
+
return f"linux-{arch}"
|
|
83
|
+
return f"{sys.platform}-{arch}"
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _ci_folder() -> str:
|
|
87
|
+
"""Name of the CI build-artifact directory for this platform.
|
|
88
|
+
|
|
89
|
+
The GitHub Actions ``build`` workflow uploads one directory per target as
|
|
90
|
+
``libstiles-<name>/lib/libstiles.{so,dylib}`` -- these names differ from the
|
|
91
|
+
bundle ``<os>-<arch>`` tag (e.g. ``libstiles-macos-apple-arm64``).
|
|
92
|
+
"""
|
|
93
|
+
machine = platform.machine().lower()
|
|
94
|
+
arch = {"x86_64": "x86_64", "amd64": "x86_64",
|
|
95
|
+
"arm64": "arm64", "aarch64": "arm64"}.get(machine, machine)
|
|
96
|
+
if sys.platform == "darwin":
|
|
97
|
+
return "libstiles-macos-apple-arm64" if arch == "arm64" \
|
|
98
|
+
else "libstiles-macos-intel-x86_64"
|
|
99
|
+
if sys.platform.startswith("linux"):
|
|
100
|
+
return f"libstiles-linux-{arch}"
|
|
101
|
+
if sys.platform.startswith("win"):
|
|
102
|
+
return f"libstiles-windows-{arch}"
|
|
103
|
+
return f"libstiles-{sys.platform}-{arch}"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _lib_filename() -> str:
|
|
107
|
+
if sys.platform == "darwin":
|
|
108
|
+
return "libstiles.dylib"
|
|
109
|
+
if sys.platform.startswith("win"):
|
|
110
|
+
return "libstiles.dll"
|
|
111
|
+
return "libstiles.so"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _candidate_paths() -> list[Path]:
|
|
115
|
+
fname = _lib_filename()
|
|
116
|
+
ci = _ci_folder()
|
|
117
|
+
cands: list[Path] = []
|
|
118
|
+
|
|
119
|
+
env_lib = os.environ.get("STILES_LIB")
|
|
120
|
+
if env_lib:
|
|
121
|
+
cands.append(Path(env_lib))
|
|
122
|
+
|
|
123
|
+
env_dir = os.environ.get("STILES_LIB_DIR")
|
|
124
|
+
if env_dir:
|
|
125
|
+
cands.append(Path(env_dir) / fname)
|
|
126
|
+
|
|
127
|
+
here = Path(__file__).resolve().parent
|
|
128
|
+
|
|
129
|
+
# CI binaries tree: <root>/libstiles-<ci>/lib/libstiles.{so,dylib}.
|
|
130
|
+
# Explicit root, then any `binaries/` or `bindings/binaries/` above us.
|
|
131
|
+
env_bin = os.environ.get("STILES_BINARIES_DIR")
|
|
132
|
+
if env_bin:
|
|
133
|
+
cands.append(Path(env_bin) / ci / "lib" / fname)
|
|
134
|
+
for parent in [here, *here.parents]:
|
|
135
|
+
cands.append(parent / "binaries" / ci / "lib" / fname)
|
|
136
|
+
cands.append(parent / "bindings" / "binaries" / ci / "lib" / fname)
|
|
137
|
+
|
|
138
|
+
# Bundled: sTiles/_libs/<plat>/libstiles.{so,dylib} and a flat fallback.
|
|
139
|
+
cands.append(here / "_libs" / _platform_tag() / fname)
|
|
140
|
+
cands.append(here / "_libs" / fname)
|
|
141
|
+
|
|
142
|
+
# Cache dir from a previous release download (see _download_from_release).
|
|
143
|
+
cands.append(_cache_dir() / ci / fname)
|
|
144
|
+
|
|
145
|
+
# Repo dev fallback: search ancestors for lib/libstiles.{so,dylib}.
|
|
146
|
+
for parent in [here, *here.parents]:
|
|
147
|
+
cands.append(parent / "lib" / fname)
|
|
148
|
+
|
|
149
|
+
return cands
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ---------------------------------------------------------------------------
|
|
153
|
+
# Fetch the matching prebuilt libstiles from the GitHub Release.
|
|
154
|
+
#
|
|
155
|
+
# When pip-installed there is no binary in the tree, so on first use we download
|
|
156
|
+
# the platform library from the project's Release assets and cache it under the
|
|
157
|
+
# user cache dir. The Linux/macOS builds are self-contained (BLAS embedded), so
|
|
158
|
+
# the cached file loads with no extra system packages.
|
|
159
|
+
#
|
|
160
|
+
# Overrides:
|
|
161
|
+
# $STILES_NO_DOWNLOAD=1 -- never hit the network (raise instead).
|
|
162
|
+
# $STILES_RELEASE_REPO -- "owner/repo" hosting the Release (default below).
|
|
163
|
+
# $STILES_RELEASE_BASE_URL -- full base URL for the assets (bypasses the repo).
|
|
164
|
+
# $STILES_CACHE_DIR -- where to cache the downloaded library.
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
_RELEASE_REPO = os.environ.get("STILES_RELEASE_REPO", "esmail-abdulfattah/sTiles")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _cache_dir() -> Path:
|
|
170
|
+
env = os.environ.get("STILES_CACHE_DIR")
|
|
171
|
+
if env:
|
|
172
|
+
return Path(env)
|
|
173
|
+
if sys.platform.startswith("win"):
|
|
174
|
+
base = os.environ.get("LOCALAPPDATA", str(Path.home() / "AppData" / "Local"))
|
|
175
|
+
return Path(base) / "sTiles"
|
|
176
|
+
base = os.environ.get("XDG_CACHE_HOME", str(Path.home() / ".cache"))
|
|
177
|
+
return Path(base) / "sTiles"
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _download_from_release() -> Path | None:
|
|
181
|
+
"""Download the matching libstiles into the cache; return its path or None."""
|
|
182
|
+
if os.environ.get("STILES_NO_DOWNLOAD"):
|
|
183
|
+
return None
|
|
184
|
+
ci = _ci_folder()
|
|
185
|
+
fname = _lib_filename()
|
|
186
|
+
dest = _cache_dir() / ci
|
|
187
|
+
lib_path = dest / fname
|
|
188
|
+
if lib_path.is_file():
|
|
189
|
+
return lib_path # already downloaded on a previous run
|
|
190
|
+
|
|
191
|
+
base = os.environ.get(
|
|
192
|
+
"STILES_RELEASE_BASE_URL",
|
|
193
|
+
f"https://github.com/{_RELEASE_REPO}/releases/latest/download",
|
|
194
|
+
)
|
|
195
|
+
url = f"{base}/{ci}.zip"
|
|
196
|
+
try:
|
|
197
|
+
dest.mkdir(parents=True, exist_ok=True)
|
|
198
|
+
sys.stderr.write(f"sTiles: fetching libstiles for {ci} from {url}\n")
|
|
199
|
+
with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
|
|
200
|
+
with urllib.request.urlopen(url) as resp: # noqa: S310
|
|
201
|
+
shutil.copyfileobj(resp, tmp)
|
|
202
|
+
tmp_zip = tmp.name
|
|
203
|
+
try:
|
|
204
|
+
with zipfile.ZipFile(tmp_zip) as zf:
|
|
205
|
+
for member in zf.namelist():
|
|
206
|
+
bn = os.path.basename(member)
|
|
207
|
+
# The library, plus (Windows) the sibling runtime DLLs.
|
|
208
|
+
want = bn == fname or (
|
|
209
|
+
sys.platform.startswith("win") and bn.lower().endswith(".dll")
|
|
210
|
+
)
|
|
211
|
+
if want:
|
|
212
|
+
with zf.open(member) as src, open(dest / bn, "wb") as out:
|
|
213
|
+
shutil.copyfileobj(src, out)
|
|
214
|
+
finally:
|
|
215
|
+
os.unlink(tmp_zip)
|
|
216
|
+
except Exception as exc: # noqa: BLE001 - any failure -> fall through to error
|
|
217
|
+
sys.stderr.write(f"sTiles: release download failed ({exc})\n")
|
|
218
|
+
return None
|
|
219
|
+
return lib_path if lib_path.is_file() else None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _load() -> tuple[ctypes.CDLL, str]:
|
|
223
|
+
tried: list[str] = []
|
|
224
|
+
for path in _candidate_paths():
|
|
225
|
+
tried.append(str(path))
|
|
226
|
+
if path.is_file():
|
|
227
|
+
try:
|
|
228
|
+
return ctypes.CDLL(str(path)), str(path)
|
|
229
|
+
except OSError as exc: # pragma: no cover - surfaced below
|
|
230
|
+
tried[-1] += f" (load failed: {exc})"
|
|
231
|
+
|
|
232
|
+
# Nothing local: fetch the prebuilt library from the GitHub Release.
|
|
233
|
+
downloaded = _download_from_release()
|
|
234
|
+
if downloaded is not None:
|
|
235
|
+
tried.append(str(downloaded))
|
|
236
|
+
try:
|
|
237
|
+
return ctypes.CDLL(str(downloaded)), str(downloaded)
|
|
238
|
+
except OSError as exc:
|
|
239
|
+
tried[-1] += f" (load failed: {exc})"
|
|
240
|
+
|
|
241
|
+
# Last resort: let the loader resolve a bare SONAME via LD_LIBRARY_PATH.
|
|
242
|
+
try:
|
|
243
|
+
return ctypes.CDLL(_lib_filename()), _lib_filename()
|
|
244
|
+
except OSError:
|
|
245
|
+
pass
|
|
246
|
+
|
|
247
|
+
raise OSError(
|
|
248
|
+
"Could not locate libstiles for this platform.\n"
|
|
249
|
+
"The automatic download from the GitHub Release failed or was disabled.\n"
|
|
250
|
+
"Set $STILES_LIB to the shared object, point $STILES_BINARIES_DIR at a\n"
|
|
251
|
+
f"CI-artifact tree ({_ci_folder()}/lib/{_lib_filename()}), or drop it in\n"
|
|
252
|
+
f"sTiles/_libs/{_platform_tag()}/{_lib_filename()}.\nSearched:\n "
|
|
253
|
+
+ "\n ".join(tried)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
lib, library_path = _load()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# ---------------------------------------------------------------------------
|
|
261
|
+
# Prototype declarations.
|
|
262
|
+
#
|
|
263
|
+
# The handle is an opaque ``void*``; every lifecycle call takes ``void**`` which
|
|
264
|
+
# on the Python side is ``byref(c_void_p)``. We declare those params as
|
|
265
|
+
# ``c_void_p`` (a pointer-to-pointer is still just an address) and always pass
|
|
266
|
+
# ``ctypes.byref(handle)`` at the call site.
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
def _decl(name, restype, argtypes):
|
|
269
|
+
fn = getattr(lib, name)
|
|
270
|
+
fn.restype = restype
|
|
271
|
+
fn.argtypes = argtypes
|
|
272
|
+
return fn
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Version / logging ---------------------------------------------------------
|
|
276
|
+
_decl("sTiles_get_version", c_char_p, [])
|
|
277
|
+
_decl("sTiles_set_log_level", None, [c_int])
|
|
278
|
+
_decl("sTiles_expert_user", None, [])
|
|
279
|
+
|
|
280
|
+
# Global configuration ------------------------------------------------------
|
|
281
|
+
_decl("sTiles_set_tile_size", None, [c_int])
|
|
282
|
+
_decl("sTiles_return_tile_size", c_int, [])
|
|
283
|
+
_decl("sTiles_get_auto_tile_size", c_int, [])
|
|
284
|
+
_decl("sTiles_set_tile_type_mode", None, [c_int])
|
|
285
|
+
_decl("sTiles_set_ordering_mode", None, [c_int])
|
|
286
|
+
_decl("sTiles_force_ND", None, [c_int])
|
|
287
|
+
|
|
288
|
+
# Lifecycle -----------------------------------------------------------------
|
|
289
|
+
# int sTiles_create(void**, int num_groups, const int* calls_per_group,
|
|
290
|
+
# const int* cores_per_group, const int* chol_type,
|
|
291
|
+
# const bool* get_inverse)
|
|
292
|
+
_decl("sTiles_create", c_int,
|
|
293
|
+
[c_void_p, c_int, c_int_p, c_int_p, c_int_p, c_bool_p])
|
|
294
|
+
# int sTiles_assign_graph_one_call(int g, int c, void**, int n, int nnz,
|
|
295
|
+
# int* row, int* col)
|
|
296
|
+
_decl("sTiles_assign_graph_one_call", c_int,
|
|
297
|
+
[c_int, c_int, c_void_p, c_int, c_int, c_int_p, c_int_p])
|
|
298
|
+
_decl("sTiles_init_group", c_int, [c_int, c_void_p])
|
|
299
|
+
_decl("sTiles_assign_values", c_int, [c_int, c_int, c_void_p, c_double_p])
|
|
300
|
+
_decl("sTiles_bind", c_int, [c_int, c_int, c_void_p])
|
|
301
|
+
_decl("sTiles_unbind", c_int, [c_int, c_int, c_void_p])
|
|
302
|
+
_decl("sTiles_chol", c_int, [c_int, c_int, c_void_p])
|
|
303
|
+
_decl("sTiles_selinv", c_int, [c_int, c_int, c_void_p])
|
|
304
|
+
_decl("sTiles_freeGroup", None, [c_int])
|
|
305
|
+
_decl("sTiles_quit", None, [])
|
|
306
|
+
|
|
307
|
+
# Result accessors ----------------------------------------------------------
|
|
308
|
+
_decl("sTiles_get_logdet", c_double, [c_int, c_int, c_void_p])
|
|
309
|
+
_decl("sTiles_get_nnz_factor", c_longlong, [c_int, c_int, c_void_p])
|
|
310
|
+
_decl("sTiles_get_selinv_elm", c_double, [c_int, c_int, c_int, c_int, c_void_p])
|
|
311
|
+
_decl("sTiles_get_chol_elm", c_double, [c_int, c_int, c_int, c_int, c_void_p])
|
|
312
|
+
# double* sTiles_get_selinv_row(int g, int c, int node, int* neighbors,
|
|
313
|
+
# int size, void**)
|
|
314
|
+
_decl("sTiles_get_selinv_row", c_double_p,
|
|
315
|
+
[c_int, c_int, c_int, c_int_p, c_int, c_void_p])
|
|
316
|
+
_decl("sTiles_clear_selinv", c_int, [c_int, c_int, c_void_p])
|
|
317
|
+
_decl("sTiles_get_chol_timing", c_double, [c_int, c_int, c_void_p])
|
|
318
|
+
_decl("sTiles_get_selinv_timing", c_double, [c_int, c_int, c_void_p])
|
|
319
|
+
|
|
320
|
+
# Permutation ---------------------------------------------------------------
|
|
321
|
+
# int sTiles_get_logical_element_perm(int g, int c, void**, int* out_perm)
|
|
322
|
+
_decl("sTiles_get_logical_element_perm", c_int, [c_int, c_int, c_void_p, c_int_p])
|
|
323
|
+
|
|
324
|
+
# Solvers (B is column-major, original order, overwritten in place) ----------
|
|
325
|
+
_decl("sTiles_solve_LLT", c_int, [c_int, c_int, c_void_p, c_double_p, c_int])
|
|
326
|
+
_decl("sTiles_solve_L", c_int, [c_int, c_int, c_void_p, c_double_p, c_int])
|
|
327
|
+
_decl("sTiles_solve_LT", c_int, [c_int, c_int, c_void_p, c_double_p, c_int])
|
|
328
|
+
|
|
329
|
+
# Memory estimate (static, no handle) --------------------------------------
|
|
330
|
+
_decl("sTiles_estimate_memory", c_double,
|
|
331
|
+
[c_int, c_int, c_int, c_int, c_int, c_int])
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""
|
|
2
|
+
High-level Python interface to sTiles.
|
|
3
|
+
|
|
4
|
+
Typical use (compute a Cholesky, log-determinant, marginal variances, solve)::
|
|
5
|
+
|
|
6
|
+
import numpy as np, scipy.sparse as sp
|
|
7
|
+
from sTiles import sTiles
|
|
8
|
+
|
|
9
|
+
Q = ... # symmetric positive-definite sparse matrix
|
|
10
|
+
with sTiles(Q, cores=4, inverse=True) as s:
|
|
11
|
+
print(s.logdet) # log|Q|
|
|
12
|
+
var = s.selinv_diag() # diag(Q^-1) -> marginal variances
|
|
13
|
+
x = s.solve(b) # Q x = b
|
|
14
|
+
|
|
15
|
+
Reuse the preprocessing -- factor many matrices that share one sparsity pattern
|
|
16
|
+
without repeating the (expensive) symbolic analysis::
|
|
17
|
+
|
|
18
|
+
s = sTiles(Q0, cores=4, inverse=True)
|
|
19
|
+
for theta in grid:
|
|
20
|
+
s.factorize(build_Q(theta)) # reuses ordering + layout
|
|
21
|
+
loglik = ... s.logdet ...
|
|
22
|
+
s.close()
|
|
23
|
+
|
|
24
|
+
Notes
|
|
25
|
+
-----
|
|
26
|
+
* The matrix must be symmetric positive-definite. Only the lower triangle is
|
|
27
|
+
read; the upper triangle (if present) is ignored.
|
|
28
|
+
* All indices exposed by this API are 0-based in the *original* ordering of the
|
|
29
|
+
input matrix -- sTiles undoes its internal fill-reducing permutation for you.
|
|
30
|
+
* State inside libstiles is keyed by an integer ``group`` index. Distinct live
|
|
31
|
+
:class:`sTiles` objects must use distinct ``group`` values (default 0).
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import ctypes
|
|
37
|
+
from ctypes import byref, c_bool, c_int, c_void_p
|
|
38
|
+
|
|
39
|
+
import numpy as np
|
|
40
|
+
|
|
41
|
+
from . import _ffi
|
|
42
|
+
from ._ffi import lib
|
|
43
|
+
|
|
44
|
+
__all__ = ["sTiles", "factorize", "version", "library_path"]
|
|
45
|
+
|
|
46
|
+
# tile_type_mode values (mirror stiles.h)
|
|
47
|
+
MODE_DENSE = 0
|
|
48
|
+
MODE_SEMISPARSE = 1
|
|
49
|
+
MODE_SPARSE = 2
|
|
50
|
+
MODE_AUTO = 3
|
|
51
|
+
_MODE_ALIASES = {
|
|
52
|
+
"dense": MODE_DENSE,
|
|
53
|
+
"semisparse": MODE_SEMISPARSE,
|
|
54
|
+
"semi": MODE_SEMISPARSE,
|
|
55
|
+
"sparse": MODE_SPARSE,
|
|
56
|
+
"auto": MODE_AUTO,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def version() -> str:
|
|
61
|
+
raw = lib.sTiles_get_version()
|
|
62
|
+
return raw.decode() if raw else "unknown"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def library_path() -> str:
|
|
66
|
+
"""Absolute path of the loaded libstiles shared object."""
|
|
67
|
+
return _ffi.library_path
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_mode(mode) -> int:
|
|
71
|
+
if isinstance(mode, str):
|
|
72
|
+
try:
|
|
73
|
+
return _MODE_ALIASES[mode.lower()]
|
|
74
|
+
except KeyError:
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"unknown mode {mode!r}; use one of {sorted(_MODE_ALIASES)}"
|
|
77
|
+
) from None
|
|
78
|
+
return int(mode)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _lower_coo(Q):
|
|
82
|
+
"""
|
|
83
|
+
Reduce a square symmetric matrix to its lower triangle in a canonical COO
|
|
84
|
+
order (sorted by (row, col)), returned as int32 row/col and float64 values.
|
|
85
|
+
|
|
86
|
+
Accepts anything SciPy can turn into a COO matrix, or a dense ndarray.
|
|
87
|
+
"""
|
|
88
|
+
import scipy.sparse as sp
|
|
89
|
+
|
|
90
|
+
if sp.issparse(Q):
|
|
91
|
+
coo = sp.tril(Q).tocoo()
|
|
92
|
+
row = np.ascontiguousarray(coo.row, dtype=np.int32)
|
|
93
|
+
col = np.ascontiguousarray(coo.col, dtype=np.int32)
|
|
94
|
+
val = np.ascontiguousarray(coo.data, dtype=np.float64)
|
|
95
|
+
n = Q.shape[0]
|
|
96
|
+
else:
|
|
97
|
+
A = np.asarray(Q, dtype=np.float64)
|
|
98
|
+
if A.ndim != 2 or A.shape[0] != A.shape[1]:
|
|
99
|
+
raise ValueError("matrix must be square 2-D")
|
|
100
|
+
n = A.shape[0]
|
|
101
|
+
ii, jj = np.nonzero(np.tril(A))
|
|
102
|
+
row = np.ascontiguousarray(ii, dtype=np.int32)
|
|
103
|
+
col = np.ascontiguousarray(jj, dtype=np.int32)
|
|
104
|
+
val = np.ascontiguousarray(A[ii, jj], dtype=np.float64)
|
|
105
|
+
|
|
106
|
+
if Q.shape[0] != Q.shape[1]:
|
|
107
|
+
raise ValueError("matrix must be square")
|
|
108
|
+
|
|
109
|
+
# Canonical (row, col) order so repeated factorizations of the same pattern
|
|
110
|
+
# line up value-for-value.
|
|
111
|
+
order = np.lexsort((col, row))
|
|
112
|
+
return int(n), row[order].copy(), col[order].copy(), val[order].copy()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class sTilesError(RuntimeError):
|
|
116
|
+
pass
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class sTiles:
|
|
120
|
+
"""
|
|
121
|
+
A live sTiles factorization of one symmetric positive-definite matrix.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
Q : scipy.sparse matrix or 2-D ndarray
|
|
126
|
+
Symmetric positive-definite matrix. Only the lower triangle is used.
|
|
127
|
+
cores : int
|
|
128
|
+
Worker threads for the factorization (default 1).
|
|
129
|
+
mode : {'auto','dense','semisparse','sparse'} or int
|
|
130
|
+
Tile factorization regime (default 'auto').
|
|
131
|
+
tile_size : int
|
|
132
|
+
Tile size; use -1 for sTiles auto-detection (default 40).
|
|
133
|
+
inverse : bool
|
|
134
|
+
Reserve storage for the selected inverse. Must be True to call
|
|
135
|
+
:meth:`selinv_diag`, :meth:`selinv_elm`, or :meth:`selinv_row`
|
|
136
|
+
(default False).
|
|
137
|
+
group : int
|
|
138
|
+
libstiles group index; use distinct values for concurrent instances.
|
|
139
|
+
log_level : int
|
|
140
|
+
Verbosity of libstiles' own logging (default -1 = silent). Errors are
|
|
141
|
+
always shown. Use 0 for [TIME] markers, 1 = info, 2 = debug, 3 = trace.
|
|
142
|
+
factorize : bool
|
|
143
|
+
If True (default) run the numeric Cholesky as part of construction. If
|
|
144
|
+
False, only the preprocessing (symbolic analysis) runs, and you call
|
|
145
|
+
:meth:`factorize` yourself -- letting you time the two phases apart.
|
|
146
|
+
|
|
147
|
+
Notes
|
|
148
|
+
-----
|
|
149
|
+
Construction has two phases: **preprocessing** (ordering bake-off + tile
|
|
150
|
+
layout, depends only on the sparsity pattern) and the **numeric** Cholesky.
|
|
151
|
+
With ``factorize=False`` the constructor stops after preprocessing::
|
|
152
|
+
|
|
153
|
+
s = sTiles(Q, inverse=True, factorize=False) # time preprocessing
|
|
154
|
+
s.factorize() # time the numeric part
|
|
155
|
+
s.chol_time # library-measured numeric time
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
def __init__(self, Q, cores=1, mode="auto", tile_size=40, inverse=False,
|
|
159
|
+
group=0, log_level=-1, factorize=True):
|
|
160
|
+
self._closed = False
|
|
161
|
+
self._handle = c_void_p(None)
|
|
162
|
+
self.group = int(group)
|
|
163
|
+
self.want_inverse = bool(inverse)
|
|
164
|
+
self._factored = False
|
|
165
|
+
self._selinv_done = False
|
|
166
|
+
self.cores = int(cores)
|
|
167
|
+
self.mode = _resolve_mode(mode)
|
|
168
|
+
|
|
169
|
+
n, row, col, val = _lower_coo(Q)
|
|
170
|
+
self.n = n
|
|
171
|
+
self.nnz = row.size
|
|
172
|
+
self._row = row # keep refs alive for the lib's lifetime
|
|
173
|
+
self._col = col
|
|
174
|
+
self._values = val
|
|
175
|
+
|
|
176
|
+
lib.sTiles_set_log_level(int(log_level))
|
|
177
|
+
|
|
178
|
+
# --- global configuration (expert mode gates the setters) ----------
|
|
179
|
+
lib.sTiles_expert_user()
|
|
180
|
+
lib.sTiles_set_tile_size(int(tile_size))
|
|
181
|
+
lib.sTiles_set_tile_type_mode(self.mode)
|
|
182
|
+
|
|
183
|
+
# --- create handle -------------------------------------------------
|
|
184
|
+
# State inside libstiles is indexed by group; a handle must be created
|
|
185
|
+
# with at least (group+1) groups for `group` to be a valid slot. Only
|
|
186
|
+
# our group gets a graph; the rest stay empty.
|
|
187
|
+
ng = self.group + 1
|
|
188
|
+
calls = (c_int * ng)(*([1] * ng))
|
|
189
|
+
cores_arr = (c_int * ng)(*([self.cores] * ng))
|
|
190
|
+
chol_type = (c_int * ng)(*([0] * ng)) # 0 = sparse factorization variant
|
|
191
|
+
get_inv = (c_bool * ng)(*([self.want_inverse] * ng))
|
|
192
|
+
rc = lib.sTiles_create(byref(self._handle), ng, calls, cores_arr,
|
|
193
|
+
chol_type, get_inv)
|
|
194
|
+
self._check(rc, "sTiles_create")
|
|
195
|
+
|
|
196
|
+
# --- PHASE 1: preprocessing (graph + ordering + tile layout) -------
|
|
197
|
+
# Symbolic only -- no numeric values, no Cholesky.
|
|
198
|
+
rc = lib.sTiles_assign_graph_one_call(
|
|
199
|
+
self.group, 0, byref(self._handle), self.n, self.nnz,
|
|
200
|
+
row.ctypes.data_as(_ffi.c_int_p), col.ctypes.data_as(_ffi.c_int_p))
|
|
201
|
+
self._check(rc, "sTiles_assign_graph_one_call")
|
|
202
|
+
|
|
203
|
+
rc = lib.sTiles_init_group(self.group, byref(self._handle))
|
|
204
|
+
self._check(rc, "sTiles_init_group")
|
|
205
|
+
|
|
206
|
+
# --- PHASE 2: numeric factorization (optional) ---------------------
|
|
207
|
+
if factorize:
|
|
208
|
+
self.factorize()
|
|
209
|
+
|
|
210
|
+
# -- internals ----------------------------------------------------------
|
|
211
|
+
def _check(self, rc, what):
|
|
212
|
+
if rc is not None and rc != 0:
|
|
213
|
+
raise sTilesError(f"{what} failed (status {rc})")
|
|
214
|
+
|
|
215
|
+
def _require_open(self):
|
|
216
|
+
if self._closed:
|
|
217
|
+
raise sTilesError("this sTiles handle has been closed")
|
|
218
|
+
|
|
219
|
+
def _require_factored(self):
|
|
220
|
+
self._require_open()
|
|
221
|
+
if not self._factored:
|
|
222
|
+
raise sTilesError(
|
|
223
|
+
"not factorized yet; call .factorize() (preprocessing is done, "
|
|
224
|
+
"the numeric Cholesky is not)")
|
|
225
|
+
|
|
226
|
+
def factorize(self, Q=None):
|
|
227
|
+
"""
|
|
228
|
+
Run the numeric Cholesky (assign_values + chol), reusing the
|
|
229
|
+
preprocessing. This is PHASE 2 and is what you time separately from
|
|
230
|
+
construction. Pass ``Q`` to factor a matrix with the SAME sparsity
|
|
231
|
+
pattern (reusing the preprocessing); otherwise the values captured at
|
|
232
|
+
construction are used. Returns ``self``.
|
|
233
|
+
"""
|
|
234
|
+
self._require_open()
|
|
235
|
+
if Q is not None:
|
|
236
|
+
_, row, col, val = _lower_coo(Q)
|
|
237
|
+
if row.size != self.nnz or not (
|
|
238
|
+
np.array_equal(row, self._row) and np.array_equal(col, self._col)
|
|
239
|
+
):
|
|
240
|
+
raise sTilesError(
|
|
241
|
+
"factorize(Q=) requires an identical sparsity pattern; "
|
|
242
|
+
"build a new sTiles for a different pattern")
|
|
243
|
+
self._values = val
|
|
244
|
+
val = self._values
|
|
245
|
+
rc = lib.sTiles_assign_values(
|
|
246
|
+
self.group, 0, byref(self._handle),
|
|
247
|
+
val.ctypes.data_as(_ffi.c_double_p))
|
|
248
|
+
self._check(rc, "sTiles_assign_values")
|
|
249
|
+
lib.sTiles_bind(self.group, 0, byref(self._handle))
|
|
250
|
+
rc = lib.sTiles_chol(self.group, 0, byref(self._handle))
|
|
251
|
+
lib.sTiles_unbind(self.group, 0, byref(self._handle))
|
|
252
|
+
self._check(rc, "sTiles_chol")
|
|
253
|
+
self._factored = True
|
|
254
|
+
self._selinv_done = False
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
def _ensure_selinv(self):
|
|
258
|
+
if not self.want_inverse:
|
|
259
|
+
raise sTilesError(
|
|
260
|
+
"construct sTiles(..., inverse=True) to use the selected inverse")
|
|
261
|
+
self._require_factored()
|
|
262
|
+
if not self._selinv_done:
|
|
263
|
+
lib.sTiles_bind(self.group, 0, byref(self._handle))
|
|
264
|
+
rc = lib.sTiles_selinv(self.group, 0, byref(self._handle))
|
|
265
|
+
lib.sTiles_unbind(self.group, 0, byref(self._handle))
|
|
266
|
+
self._check(rc, "sTiles_selinv")
|
|
267
|
+
self._selinv_done = True
|
|
268
|
+
|
|
269
|
+
# -- results ------------------------------------------------------------
|
|
270
|
+
@property
|
|
271
|
+
def is_factored(self) -> bool:
|
|
272
|
+
"""Whether the numeric Cholesky has run (PHASE 2)."""
|
|
273
|
+
return self._factored
|
|
274
|
+
|
|
275
|
+
@property
|
|
276
|
+
def logdet(self) -> float:
|
|
277
|
+
"""log-determinant of Q (== 2 * sum(log diag(L)))."""
|
|
278
|
+
self._require_factored()
|
|
279
|
+
return float(lib.sTiles_get_logdet(self.group, 0, byref(self._handle)))
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def nnz_factor(self) -> int:
|
|
283
|
+
"""Number of stored non-zeros in the Cholesky factor L."""
|
|
284
|
+
self._require_open()
|
|
285
|
+
return int(lib.sTiles_get_nnz_factor(self.group, 0, byref(self._handle)))
|
|
286
|
+
|
|
287
|
+
@property
|
|
288
|
+
def chol_time(self) -> float:
|
|
289
|
+
"""Library-measured numeric Cholesky time in seconds (no Python overhead)."""
|
|
290
|
+
self._require_factored()
|
|
291
|
+
return float(lib.sTiles_get_chol_timing(self.group, 0, byref(self._handle)))
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def selinv_time(self) -> float:
|
|
295
|
+
"""Library-measured selected-inverse time in seconds (no Python overhead)."""
|
|
296
|
+
self._require_factored()
|
|
297
|
+
return float(lib.sTiles_get_selinv_timing(self.group, 0, byref(self._handle)))
|
|
298
|
+
|
|
299
|
+
def summary(self) -> dict:
|
|
300
|
+
"""
|
|
301
|
+
Structured snapshot: dimensions, fill, mode, phase state and
|
|
302
|
+
library-measured timings. Mirrors R's ``sTiles_summary(s)`` (returns a
|
|
303
|
+
dict, e.g. ``s.summary()["chol_time"]``). Timings are ``None`` until the
|
|
304
|
+
corresponding phase has run.
|
|
305
|
+
"""
|
|
306
|
+
self._require_open()
|
|
307
|
+
modes = {0: "dense", 1: "semisparse", 2: "sparse", 3: "auto"}
|
|
308
|
+
return {
|
|
309
|
+
"n": self.n,
|
|
310
|
+
"nnz": self.nnz,
|
|
311
|
+
"nnz_factor": self.nnz_factor,
|
|
312
|
+
"mode": modes.get(self.mode, self.mode),
|
|
313
|
+
"cores": self.cores,
|
|
314
|
+
"inverse": self.want_inverse,
|
|
315
|
+
"factored": self._factored,
|
|
316
|
+
"chol_time": self.chol_time if self._factored else None,
|
|
317
|
+
"selinv_time": self.selinv_time if self._selinv_done else None,
|
|
318
|
+
"version": version(),
|
|
319
|
+
"library": library_path(),
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
def selinv(self):
|
|
323
|
+
"""
|
|
324
|
+
Compute the selected inverse explicitly, as its own (timeable) phase --
|
|
325
|
+
``Z = Q^-1`` restricted to the pattern of the Cholesky factor. Requires
|
|
326
|
+
``inverse=True``. Idempotent; otherwise it is computed lazily on the
|
|
327
|
+
first ``selinv_*`` query. Returns ``self``.
|
|
328
|
+
"""
|
|
329
|
+
self._ensure_selinv()
|
|
330
|
+
return self
|
|
331
|
+
|
|
332
|
+
def chol_elm(self, i: int, j: int) -> float:
|
|
333
|
+
"""Entry L[i, j] of the Cholesky factor (0-based, original order)."""
|
|
334
|
+
self._require_factored()
|
|
335
|
+
return float(lib.sTiles_get_chol_elm(
|
|
336
|
+
self.group, 0, int(i), int(j), byref(self._handle)))
|
|
337
|
+
|
|
338
|
+
def selinv_elm(self, i: int, j: int) -> float:
|
|
339
|
+
"""
|
|
340
|
+
Selected-inverse entry ``(Q^-1)[i, j]`` at ANY position (0-based,
|
|
341
|
+
original order). Returns the value when ``(i, j)`` lies in the factor
|
|
342
|
+
pattern (pattern of ``L+L^T``) and exactly ``0.0`` outside it. Both
|
|
343
|
+
triangles work (Z is symmetric). Triggers the selected-inverse
|
|
344
|
+
computation on first use.
|
|
345
|
+
"""
|
|
346
|
+
self._ensure_selinv()
|
|
347
|
+
return float(lib.sTiles_get_selinv_elm(
|
|
348
|
+
self.group, 0, int(i), int(j), byref(self._handle)))
|
|
349
|
+
|
|
350
|
+
def selinv_diag(self) -> np.ndarray:
|
|
351
|
+
"""
|
|
352
|
+
Diagonal of the selected inverse, ``diag(Q^-1)`` -- the marginal
|
|
353
|
+
variances. Length-``n`` float64 array in original order.
|
|
354
|
+
"""
|
|
355
|
+
self._ensure_selinv()
|
|
356
|
+
out = np.empty(self.n, dtype=np.float64)
|
|
357
|
+
get = lib.sTiles_get_selinv_elm
|
|
358
|
+
h = byref(self._handle)
|
|
359
|
+
g = self.group
|
|
360
|
+
for i in range(self.n):
|
|
361
|
+
out[i] = get(g, 0, i, i, h)
|
|
362
|
+
return out
|
|
363
|
+
|
|
364
|
+
def selinv_row(self, node: int, neighbors) -> np.ndarray:
|
|
365
|
+
"""
|
|
366
|
+
Values ``(Q^-1)[node, k]`` for each ``k`` in ``neighbors`` (entries that
|
|
367
|
+
fall outside the factor pattern come back as 0).
|
|
368
|
+
"""
|
|
369
|
+
self._ensure_selinv()
|
|
370
|
+
nb = np.ascontiguousarray(neighbors, dtype=np.int32)
|
|
371
|
+
ptr = lib.sTiles_get_selinv_row(
|
|
372
|
+
self.group, 0, int(node), nb.ctypes.data_as(_ffi.c_int_p),
|
|
373
|
+
nb.size, byref(self._handle))
|
|
374
|
+
if not ptr:
|
|
375
|
+
raise sTilesError("sTiles_get_selinv_row returned null")
|
|
376
|
+
vals = np.ctypeslib.as_array(ptr, shape=(nb.size,)).copy()
|
|
377
|
+
return vals
|
|
378
|
+
|
|
379
|
+
def permutation(self) -> np.ndarray:
|
|
380
|
+
"""
|
|
381
|
+
Fill-reducing permutation over the original nodes, as a length-``n``
|
|
382
|
+
int32 array (logical, ND-padding removed).
|
|
383
|
+
"""
|
|
384
|
+
self._require_open()
|
|
385
|
+
out = np.empty(self.n, dtype=np.int32)
|
|
386
|
+
m = lib.sTiles_get_logical_element_perm(
|
|
387
|
+
self.group, 0, byref(self._handle), out.ctypes.data_as(_ffi.c_int_p))
|
|
388
|
+
if m < 0:
|
|
389
|
+
raise sTilesError("sTiles_get_logical_element_perm failed")
|
|
390
|
+
return out[:m]
|
|
391
|
+
|
|
392
|
+
# -- solves -------------------------------------------------------------
|
|
393
|
+
def _solve(self, fn, b):
|
|
394
|
+
self._require_factored()
|
|
395
|
+
B = np.asarray(b, dtype=np.float64)
|
|
396
|
+
one_d = B.ndim == 1
|
|
397
|
+
if one_d:
|
|
398
|
+
B = B.reshape(self.n, 1)
|
|
399
|
+
if B.shape[0] != self.n:
|
|
400
|
+
raise ValueError(f"rhs has {B.shape[0]} rows, expected {self.n}")
|
|
401
|
+
nrhs = B.shape[1]
|
|
402
|
+
# libstiles wants column-major (each rhs contiguous); overwrite in place.
|
|
403
|
+
work = np.asfortranarray(B, dtype=np.float64).copy(order="F")
|
|
404
|
+
lib.sTiles_bind(self.group, 0, byref(self._handle))
|
|
405
|
+
rc = fn(self.group, 0, byref(self._handle),
|
|
406
|
+
work.ctypes.data_as(_ffi.c_double_p), nrhs)
|
|
407
|
+
lib.sTiles_unbind(self.group, 0, byref(self._handle))
|
|
408
|
+
self._check(rc, fn.__name__)
|
|
409
|
+
return work[:, 0].copy() if one_d else np.ascontiguousarray(work)
|
|
410
|
+
|
|
411
|
+
def solve(self, b, system="A") -> np.ndarray:
|
|
412
|
+
"""
|
|
413
|
+
Solve with the factorization. ``b`` is a length-n vector or n x nrhs
|
|
414
|
+
matrix. ``system``: ``"A"`` solves ``Q x = b`` (default), ``"L"`` solves
|
|
415
|
+
``L y = b`` (forward), ``"Lt"`` solves ``L^T x = b`` (backward).
|
|
416
|
+
"""
|
|
417
|
+
fn = {"A": lib.sTiles_solve_LLT, "L": lib.sTiles_solve_L,
|
|
418
|
+
"Lt": lib.sTiles_solve_LT}.get(system)
|
|
419
|
+
if fn is None:
|
|
420
|
+
raise ValueError(f"system must be 'A', 'L' or 'Lt', got {system!r}")
|
|
421
|
+
return self._solve(fn, b)
|
|
422
|
+
|
|
423
|
+
def solve_L(self, b) -> np.ndarray:
|
|
424
|
+
"""Forward solve ``L y = b`` (alias for ``solve(b, 'L')``)."""
|
|
425
|
+
return self.solve(b, "L")
|
|
426
|
+
|
|
427
|
+
def solve_LT(self, b) -> np.ndarray:
|
|
428
|
+
"""Backward solve ``L^T x = b`` (alias for ``solve(b, 'Lt')``)."""
|
|
429
|
+
return self.solve(b, "Lt")
|
|
430
|
+
|
|
431
|
+
# -- value reuse (same pattern, new values) ----------------------------
|
|
432
|
+
def update_values(self, Q):
|
|
433
|
+
"""
|
|
434
|
+
Re-factor a matrix that shares this object's sparsity pattern, reusing
|
|
435
|
+
the symbolic analysis (ordering + layout). Much cheaper than building
|
|
436
|
+
a fresh :class:`sTiles`. Alias for ``factorize(Q)``; raises if the
|
|
437
|
+
pattern differs.
|
|
438
|
+
"""
|
|
439
|
+
return self.factorize(Q)
|
|
440
|
+
|
|
441
|
+
# -- lifecycle ----------------------------------------------------------
|
|
442
|
+
def close(self):
|
|
443
|
+
if not self._closed:
|
|
444
|
+
try:
|
|
445
|
+
lib.sTiles_freeGroup(self.group)
|
|
446
|
+
finally:
|
|
447
|
+
self._closed = True
|
|
448
|
+
self._handle = c_void_p(None)
|
|
449
|
+
|
|
450
|
+
def __enter__(self):
|
|
451
|
+
return self
|
|
452
|
+
|
|
453
|
+
def __exit__(self, *exc):
|
|
454
|
+
self.close()
|
|
455
|
+
return False
|
|
456
|
+
|
|
457
|
+
def __del__(self):
|
|
458
|
+
try:
|
|
459
|
+
self.close()
|
|
460
|
+
except Exception:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
def __repr__(self):
|
|
464
|
+
if self._closed:
|
|
465
|
+
return "<sTiles closed>"
|
|
466
|
+
phase = "factorized" if self._factored else "analyzed (not factorized)"
|
|
467
|
+
return (f"<sTiles n={self.n}, nnz={self.nnz}, mode={self.mode}, "
|
|
468
|
+
f"cores={self.cores}, {phase}>")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def factorize(Q, **kw) -> sTiles:
|
|
472
|
+
"""Convenience constructor -- identical to :class:`sTiles`."""
|
|
473
|
+
return sTiles(Q, **kw)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sTiles
|
|
3
|
+
Version: 2026.7.19
|
|
4
|
+
Summary: Python bindings for the sTiles sparse Cholesky / selected-inverse framework
|
|
5
|
+
Author-email: Esmail Abdul Fattah <esmail.abdulfattah@kaust.edu.sa>
|
|
6
|
+
Project-URL: Homepage, https://esmail-abdulfattah.github.io/sTiles/
|
|
7
|
+
Project-URL: Repository, https://github.com/esmail-abdulfattah/sTiles
|
|
8
|
+
Classifier: License :: Other/Proprietary License
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
12
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Requires-Dist: numpy>=1.20
|
|
16
|
+
Requires-Dist: scipy>=1.5
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
sTiles/__init__.py
|
|
4
|
+
sTiles/_ffi.py
|
|
5
|
+
sTiles/_version.py
|
|
6
|
+
sTiles/core.py
|
|
7
|
+
sTiles.egg-info/PKG-INFO
|
|
8
|
+
sTiles.egg-info/SOURCES.txt
|
|
9
|
+
sTiles.egg-info/dependency_links.txt
|
|
10
|
+
sTiles.egg-info/requires.txt
|
|
11
|
+
sTiles.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
sTiles
|