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.
@@ -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,3 @@
1
+ # Single source of truth for the sTiles Python package version.
2
+ # Date-based (CalVer): YYYY.M.D. Bump with ../release.sh (stamps today's date).
3
+ __version__ = "2026.7.19"
@@ -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,2 @@
1
+ numpy>=1.20
2
+ scipy>=1.5
@@ -0,0 +1 @@
1
+ sTiles
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+