pymc-cache 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 (63) hide show
  1. pymc_cache-0.1.0/.gitignore +19 -0
  2. pymc_cache-0.1.0/AGENTS.md +3 -0
  3. pymc_cache-0.1.0/CHANGELOG.md +11 -0
  4. pymc_cache-0.1.0/CONTRIBUTING.md +3 -0
  5. pymc_cache-0.1.0/LICENSE +21 -0
  6. pymc_cache-0.1.0/PKG-INFO +70 -0
  7. pymc_cache-0.1.0/README.md +41 -0
  8. pymc_cache-0.1.0/SECURITY.md +3 -0
  9. pymc_cache-0.1.0/_quarto.yml +51 -0
  10. pymc_cache-0.1.0/api/CacheBackend.qmd +7 -0
  11. pymc_cache-0.1.0/api/CacheEntry.qmd +16 -0
  12. pymc_cache-0.1.0/api/CacheLookup.qmd +21 -0
  13. pymc_cache-0.1.0/api/CachePolicy.qmd +7 -0
  14. pymc_cache-0.1.0/api/CacheSession.qmd +51 -0
  15. pymc_cache-0.1.0/api/CacheWarning.qmd +7 -0
  16. pymc_cache-0.1.0/api/DiskCache.qmd +52 -0
  17. pymc_cache-0.1.0/api/MemoryCache.qmd +21 -0
  18. pymc_cache-0.1.0/api/UncacheableError.qmd +7 -0
  19. pymc_cache-0.1.0/api/cache_key.qmd +15 -0
  20. pymc_cache-0.1.0/api/cleanup_cache.qmd +7 -0
  21. pymc_cache-0.1.0/api/index.qmd +20 -0
  22. pymc_cache-0.1.0/api/model_digest.qmd +10 -0
  23. pymc_cache-0.1.0/api/sample.qmd +21 -0
  24. pymc_cache-0.1.0/api/sample_smc.qmd +16 -0
  25. pymc_cache-0.1.0/contributing.qmd +5 -0
  26. pymc_cache-0.1.0/custom.scss +2 -0
  27. pymc_cache-0.1.0/examples/basic.py +26 -0
  28. pymc_cache-0.1.0/examples/custom_backend.py +36 -0
  29. pymc_cache-0.1.0/examples/monitor_composition.py +48 -0
  30. pymc_cache-0.1.0/examples/preflight.py +26 -0
  31. pymc_cache-0.1.0/getting-started.qmd +38 -0
  32. pymc_cache-0.1.0/guides/identity.qmd +28 -0
  33. pymc_cache-0.1.0/guides/migration.qmd +7 -0
  34. pymc_cache-0.1.0/guides/monitor.qmd +7 -0
  35. pymc_cache-0.1.0/guides/storage.qmd +16 -0
  36. pymc_cache-0.1.0/guides/troubleshooting.qmd +11 -0
  37. pymc_cache-0.1.0/guides/walkthrough.qmd +11 -0
  38. pymc_cache-0.1.0/index.qmd +12 -0
  39. pymc_cache-0.1.0/pyproject.toml +63 -0
  40. pymc_cache-0.1.0/reference.qmd +18 -0
  41. pymc_cache-0.1.0/releasing.qmd +11 -0
  42. pymc_cache-0.1.0/scripts/build-docs.sh +8 -0
  43. pymc_cache-0.1.0/scripts/check_artifact.py +67 -0
  44. pymc_cache-0.1.0/scripts/check_docs.py +51 -0
  45. pymc_cache-0.1.0/scripts/check_examples.py +26 -0
  46. pymc_cache-0.1.0/scripts/lock.sh +5 -0
  47. pymc_cache-0.1.0/scripts/smoke_test.py +41 -0
  48. pymc_cache-0.1.0/src/pymc_cache/__init__.py +13 -0
  49. pymc_cache-0.1.0/src/pymc_cache/_version.py +1 -0
  50. pymc_cache-0.1.0/src/pymc_cache/api.py +127 -0
  51. pymc_cache-0.1.0/src/pymc_cache/backends.py +262 -0
  52. pymc_cache-0.1.0/src/pymc_cache/cli.py +50 -0
  53. pymc_cache-0.1.0/src/pymc_cache/identity.py +246 -0
  54. pymc_cache-0.1.0/src/pymc_cache/io.py +269 -0
  55. pymc_cache-0.1.0/src/pymc_cache/naming.py +28 -0
  56. pymc_cache-0.1.0/src/pymc_cache/policy.py +65 -0
  57. pymc_cache-0.1.0/src/pymc_cache/py.typed +0 -0
  58. pymc_cache-0.1.0/src/pymc_cache/session.py +146 -0
  59. pymc_cache-0.1.0/tests/test_cache.py +365 -0
  60. pymc_cache-0.1.0/tests/test_identity.py +59 -0
  61. pymc_cache-0.1.0/tests/test_public_contract.py +94 -0
  62. pymc_cache-0.1.0/tests/test_safety.py +185 -0
  63. pymc_cache-0.1.0/tests/test_text_codec.py +242 -0
@@ -0,0 +1,19 @@
1
+ .extraction-source/
2
+ .venv/
3
+ __pycache__/
4
+ *.py[cod]
5
+ *.egg-info/
6
+ .coverage
7
+ .pytest_cache/
8
+ dist/
9
+ build/
10
+ _site/
11
+ .quarto/
12
+ /reference/
13
+ /objects.json
14
+ .pymc-cache/
15
+ *.nc
16
+ .pi/
17
+
18
+ /.quarto/
19
+ **/*.quarto_ipynb
@@ -0,0 +1,3 @@
1
+ # Repository guidance
2
+
3
+ This repository owns cache identity, storage, and policy only. It must not depend on cloudposterior, pymc-monitor or Modal. Keep `.extraction-source/` temporary, ignored and out of all artifacts. Follow `CONTRIBUTING.md`; preserve old external cache data. Never silently reuse an entry when a model or sampler cannot be identified safely.
@@ -0,0 +1,11 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 (unreleased)
4
+
5
+ - Extract independent PyMC model-aware sampling cache from cloudposterior.
6
+ - Add versioned, fail-closed graph and call identity; bounded memory and atomic NetCDF disk backends.
7
+ - Add a preflight session API, safe disk-management CLI, and PyMC 5/6 and ArviZ 0/1 support.
8
+ - Encode nutpie divergence messages losslessly; support nullable diagnostic warning text and floating NaN without dropping data.
9
+ - Pre-release identity and disk schema 3 invalidate schema-1/2 entries; old files are misses, left untouched and never overwritten. Existing caches must be sampled again.
10
+
11
+ Old cloudposterior keys are deliberately not trusted or imported; old files are not deleted.
@@ -0,0 +1,3 @@
1
+ # Contributing
2
+
3
+ Install [uv](https://docs.astral.sh/uv/) and Quarto. Run `bash scripts/lock.sh`, `uv run --frozen pytest -q --cov=pymc_cache --cov-fail-under=85`, `uv run --frozen ruff check src tests scripts examples`, `bash scripts/build-docs.sh`, `uv build --no-sources`, and `uv run --frozen python scripts/check_artifact.py`. `scripts/lock.sh` checks the lock without user configuration before a frozen run; update the lock intentionally with `uv lock --no-config` when dependencies change. Test both supported PyMC/ArviZ majors before proposing a release. Make one conventional commit per logical change; discuss new cache formats and identity semantics before changing them. Do not check in posterior data.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Spencer Boucher
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,70 @@
1
+ Metadata-Version: 2.5
2
+ Name: pymc-cache
3
+ Version: 0.1.0
4
+ Summary: Safe, model-aware caching of PyMC sampling results
5
+ Project-URL: Homepage, https://github.com/justmytwospence/pymc-cache
6
+ Project-URL: Documentation, https://justmytwospence.github.io/pymc-cache/
7
+ Project-URL: Repository, https://github.com/justmytwospence/pymc-cache
8
+ Project-URL: Issues, https://github.com/justmytwospence/pymc-cache/issues
9
+ Author: Spencer Boucher
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: bayesian,cache,inferencedata,mcmc,pymc
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: arviz<2,>=0.17
22
+ Requires-Dist: h5netcdf>=1.3
23
+ Requires-Dist: h5py>=3.10
24
+ Requires-Dist: numpy>=1.25
25
+ Requires-Dist: pymc<7,>=5.28
26
+ Requires-Dist: pytensor>=2.38
27
+ Requires-Dist: xarray>=2023.1
28
+ Description-Content-Type: text/markdown
29
+
30
+ # pymc-cache
31
+
32
+ Safe, model-aware caching of native PyMC sampling results. Python 3.11+, PyMC 5.28–6 and ArviZ 0.x–1.x. No cloud account, monitoring service or Modal dependency.
33
+
34
+ ```bash
35
+ pip install 'pymc-cache @ git+https://github.com/justmytwospence/pymc-cache.git@<commit-sha>'
36
+ ```
37
+
38
+ ```python
39
+ import pymc as pm
40
+ import pymc_cache as pc
41
+
42
+ with pm.Model() as model:
43
+ mu = pm.Normal("mu", 0, 1)
44
+ options = dict(draws=10, tune=10, chains=1, cores=1, random_seed=42,
45
+ progressbar=False, model=model, cache="disk")
46
+ trace = pc.sample(**options)
47
+ # The next call loads the result from .pymc-cache/ without sampling:
48
+ again = pc.sample(**options)
49
+ ```
50
+
51
+ `pc.sample_smc(model=model, draws=100, cache="disk")` uses the native SMC sampler with an independent key. `cache=True` (default) uses an 8-entry in-memory LRU. `cache=False` runs the native sampler without hashing the model. Pass a directory/Path for a custom disk location; `overwrite=True` skips reads and atomically replaces an existing entry. Both backends copy results on read; memory copies on write too. Disk files are one NetCDF file per full SHA-256 key, with the key embedded in its metadata. Corruption is treated as a miss. `pc.cleanup_cache()` removes only entries owned by this package; `pymc-cache list|prune|clear --dir PATH` inspects/manages the same entries without following symlinks.
52
+
53
+ ## Preflight and integrations
54
+
55
+ ```python
56
+ session = pc.CacheSession(cache="disk", model=model)
57
+ lookup = session.lookup({"draws": 10, "tune": 10, "chains": 1,
58
+ "cores": 1, "random_seed": 42, "progressbar": False})
59
+ if lookup.hit:
60
+ trace = lookup.result
61
+ else:
62
+ trace = pm.sample(model=model, **lookup.normalized_kwargs)
63
+ session.store(lookup, trace, status="complete")
64
+ ```
65
+
66
+ A runner used with `pc.sample(..., sample_fn=runner)` must implement `runner.__pymc_cache_normalize__(model, call_kwargs)` and return **exactly** `call_kwargs`, `key_kwargs`, `sampler_identity`, and `versions`. The call arguments passed to the runner and the canonical key arguments must be explicit. The runner returns an inference result; only complete results, or adaptive results with an attested `met` convergence report, are stored. Stopped, futile, capped and incomplete results are returned but never stored. Cache identity includes the complete model graph, constants, mutable shared data, deterministics, coordinates, sampling kwargs, RNG states, operation, sampler and software versions. Unknown/custom Ops fail closed rather than risk a wrong hit. Unsafe steps/callbacks/callables require an explicit `sampler_key` identifying their implementation and configuration; the caller is responsible for changing this key when the callable changes. No code is executed from cache metadata.
67
+
68
+ Caching may hold sensitive posterior data. Treat `.pymc-cache/` as private, and use `cache=False` for confidential models when persistent data is not allowed. Cache files are not authenticated against a malicious writer; do not use a shared untrusted directory.
69
+
70
+ See [getting started](getting-started.qmd), [caching walkthrough](guides/walkthrough.qmd), [identity guide](guides/identity.qmd), [storage guide](guides/storage.qmd), [monitor composition](guides/monitor.qmd), [legacy migration](guides/migration.qmd), [troubleshooting](guides/troubleshooting.qmd), [runnable examples](examples/basic.py), and [API reference](reference.qmd). This package does not patch `pm.sample`, intercept model contexts, cache posterior predictive calls or provide cloud execution.
@@ -0,0 +1,41 @@
1
+ # pymc-cache
2
+
3
+ Safe, model-aware caching of native PyMC sampling results. Python 3.11+, PyMC 5.28–6 and ArviZ 0.x–1.x. No cloud account, monitoring service or Modal dependency.
4
+
5
+ ```bash
6
+ pip install 'pymc-cache @ git+https://github.com/justmytwospence/pymc-cache.git@<commit-sha>'
7
+ ```
8
+
9
+ ```python
10
+ import pymc as pm
11
+ import pymc_cache as pc
12
+
13
+ with pm.Model() as model:
14
+ mu = pm.Normal("mu", 0, 1)
15
+ options = dict(draws=10, tune=10, chains=1, cores=1, random_seed=42,
16
+ progressbar=False, model=model, cache="disk")
17
+ trace = pc.sample(**options)
18
+ # The next call loads the result from .pymc-cache/ without sampling:
19
+ again = pc.sample(**options)
20
+ ```
21
+
22
+ `pc.sample_smc(model=model, draws=100, cache="disk")` uses the native SMC sampler with an independent key. `cache=True` (default) uses an 8-entry in-memory LRU. `cache=False` runs the native sampler without hashing the model. Pass a directory/Path for a custom disk location; `overwrite=True` skips reads and atomically replaces an existing entry. Both backends copy results on read; memory copies on write too. Disk files are one NetCDF file per full SHA-256 key, with the key embedded in its metadata. Corruption is treated as a miss. `pc.cleanup_cache()` removes only entries owned by this package; `pymc-cache list|prune|clear --dir PATH` inspects/manages the same entries without following symlinks.
23
+
24
+ ## Preflight and integrations
25
+
26
+ ```python
27
+ session = pc.CacheSession(cache="disk", model=model)
28
+ lookup = session.lookup({"draws": 10, "tune": 10, "chains": 1,
29
+ "cores": 1, "random_seed": 42, "progressbar": False})
30
+ if lookup.hit:
31
+ trace = lookup.result
32
+ else:
33
+ trace = pm.sample(model=model, **lookup.normalized_kwargs)
34
+ session.store(lookup, trace, status="complete")
35
+ ```
36
+
37
+ A runner used with `pc.sample(..., sample_fn=runner)` must implement `runner.__pymc_cache_normalize__(model, call_kwargs)` and return **exactly** `call_kwargs`, `key_kwargs`, `sampler_identity`, and `versions`. The call arguments passed to the runner and the canonical key arguments must be explicit. The runner returns an inference result; only complete results, or adaptive results with an attested `met` convergence report, are stored. Stopped, futile, capped and incomplete results are returned but never stored. Cache identity includes the complete model graph, constants, mutable shared data, deterministics, coordinates, sampling kwargs, RNG states, operation, sampler and software versions. Unknown/custom Ops fail closed rather than risk a wrong hit. Unsafe steps/callbacks/callables require an explicit `sampler_key` identifying their implementation and configuration; the caller is responsible for changing this key when the callable changes. No code is executed from cache metadata.
38
+
39
+ Caching may hold sensitive posterior data. Treat `.pymc-cache/` as private, and use `cache=False` for confidential models when persistent data is not allowed. Cache files are not authenticated against a malicious writer; do not use a shared untrusted directory.
40
+
41
+ See [getting started](getting-started.qmd), [caching walkthrough](guides/walkthrough.qmd), [identity guide](guides/identity.qmd), [storage guide](guides/storage.qmd), [monitor composition](guides/monitor.qmd), [legacy migration](guides/migration.qmd), [troubleshooting](guides/troubleshooting.qmd), [runnable examples](examples/basic.py), and [API reference](reference.qmd). This package does not patch `pm.sample`, intercept model contexts, cache posterior predictive calls or provide cloud execution.
@@ -0,0 +1,3 @@
1
+ # Security
2
+
3
+ Cache files contain model outputs and potentially sensitive observed data. Store them in private directories, restrict filesystem permissions, and do not point this package at an untrusted shared cache. A full SHA-256 key is an accidental-collision/identity guard, **not** a cryptographic authenticity check against a malicious writer. Unsupported custom PyTensor Ops require a caller-maintained `model_key`; unsupported sampler steps and callables require a caller-maintained `sampler_key`. Changing their behavior without changing those keys is unsafe. Report vulnerabilities privately through GitHub's private vulnerability reporting. Do not attach real model data or credentials.
@@ -0,0 +1,51 @@
1
+ project:
2
+ type: website
3
+ output-dir: _site
4
+ render:
5
+ - index.qmd
6
+ - getting-started.qmd
7
+ - guides/*.qmd
8
+ - reference.qmd
9
+ - api/*.qmd
10
+ - contributing.qmd
11
+ - releasing.qmd
12
+ website:
13
+ title: pymc-cache
14
+ site-url: https://justmytwospence.github.io/pymc-cache/
15
+ repo-url: https://github.com/justmytwospence/pymc-cache
16
+ navbar:
17
+ left:
18
+ - href: getting-started.qmd
19
+ text: Get started
20
+ - href: guides/identity.qmd
21
+ text: Identity and safety
22
+ - href: guides/storage.qmd
23
+ text: Storage
24
+ - href: reference.qmd
25
+ text: API
26
+ quartodoc:
27
+ package: pymc_cache
28
+ dir: api
29
+ sections:
30
+ - title: Public API
31
+ contents:
32
+ - sample
33
+ - sample_smc
34
+ - CacheBackend
35
+ - MemoryCache
36
+ - DiskCache
37
+ - CacheSession
38
+ - CachePolicy
39
+ - CacheEntry
40
+ - CacheLookup
41
+ - UncacheableError
42
+ - CacheWarning
43
+ - cleanup_cache
44
+ - model_digest
45
+ - cache_key
46
+ format:
47
+ html:
48
+ theme: cosmo
49
+ toc: true
50
+ execute:
51
+ enabled: false
@@ -0,0 +1,7 @@
1
+ # CacheBackend { #pymc_cache.CacheBackend }
2
+
3
+ ```python
4
+ CacheBackend()
5
+ ```
6
+
7
+ Minimal backend interface; extra management operations are optional.
@@ -0,0 +1,16 @@
1
+ # CacheEntry { #pymc_cache.CacheEntry }
2
+
3
+ ```python
4
+ CacheEntry(
5
+ key,
6
+ model_slug,
7
+ operation,
8
+ created_at,
9
+ size_bytes,
10
+ status,
11
+ path,
12
+ software_versions,
13
+ )
14
+ ```
15
+
16
+ Metadata for an owned, valid disk entry (without loading posterior arrays).
@@ -0,0 +1,21 @@
1
+ # CacheLookup { #pymc_cache.CacheLookup }
2
+
3
+ ```python
4
+ CacheLookup(
5
+ backend,
6
+ key,
7
+ hit,
8
+ result,
9
+ normalized_kwargs,
10
+ disable_reason,
11
+ operation,
12
+ versions,
13
+ sampler_identity,
14
+ pre_run_model_digest,
15
+ )
16
+ ```
17
+
18
+ Immutable record of a pre-sampling cache lookup.
19
+
20
+ ``result`` is non-None only for a hit; ``key`` is usable by a later store.
21
+ ``normalized_kwargs`` is a snapshot, including pre-sampling RNG state.
@@ -0,0 +1,7 @@
1
+ # CachePolicy { #pymc_cache.CachePolicy }
2
+
3
+ ```python
4
+ CachePolicy(reuse_unseeded=True)
5
+ ```
6
+
7
+ Select whether unseeded sampling may reuse a previous run.
@@ -0,0 +1,51 @@
1
+ # CacheSession { #pymc_cache.CacheSession }
2
+
3
+ ```python
4
+ CacheSession(
5
+ cache=True,
6
+ *,
7
+ model=None,
8
+ policy=None,
9
+ namespace='',
10
+ model_key=None,
11
+ sampler_key=None,
12
+ )
13
+ ```
14
+
15
+ Compute identity and check a cache before beginning costly sampling.
16
+
17
+ Reuse the returned CacheLookup with :meth:`store` after sampling completes.
18
+ This API lets a remote caller avoid provisioning a container on a hit.
19
+
20
+ ## Methods
21
+
22
+ | Name | Description |
23
+ | --- | --- |
24
+ | [lookup](#pymc_cache.CacheSession.lookup) | Return a hit, a storable miss, or a safe uncacheable miss. |
25
+ | [store](#pymc_cache.CacheSession.store) | Publish only if graph and result still match the preflight identity. |
26
+
27
+ ### lookup { #pymc_cache.CacheSession.lookup }
28
+
29
+ ```python
30
+ CacheSession.lookup(
31
+ sample_kwargs,
32
+ *,
33
+ operation='sample',
34
+ overwrite=False,
35
+ versions=None,
36
+ sampler_identity=None,
37
+ )
38
+ ```
39
+
40
+ Return a hit, a storable miss, or a safe uncacheable miss.
41
+
42
+ ### store { #pymc_cache.CacheSession.store }
43
+
44
+ ```python
45
+ CacheSession.store(lookup, idata, *, status='complete', effective_sampler=None)
46
+ ```
47
+
48
+ Publish only if graph and result still match the preflight identity.
49
+
50
+ A reported sampler fallback is not stored under the preflight key; callers
51
+ must preflight a new identity if they want to cache that result.
@@ -0,0 +1,7 @@
1
+ # CacheWarning { #pymc_cache.CacheWarning }
2
+
3
+ ```python
4
+ CacheWarning()
5
+ ```
6
+
7
+ A cache was bypassed or an entry could not be persisted.
@@ -0,0 +1,52 @@
1
+ # DiskCache { #pymc_cache.DiskCache }
2
+
3
+ ```python
4
+ DiskCache(base_dir=None, model=None, *, max_entries_per_model=20)
5
+ ```
6
+
7
+ One-file, full-key-verified atomic NetCDF result cache.
8
+
9
+ Entries live at ``<root>/<model>/<parameters>-<full-sha256>.nc``. Contents
10
+ have an independently checked full key and schema; legacy files are never
11
+ reused. The directory is a trusted local cache, not an untrusted file reader.
12
+
13
+ ## Methods
14
+
15
+ | Name | Description |
16
+ | --- | --- |
17
+ | [clear](#pymc_cache.DiskCache.clear) | Delete only format-owned entries, preserving other files. |
18
+ | [delete](#pymc_cache.DiskCache.delete) | Remove matching owned entries, never arbitrary files or symlinks. |
19
+ | [entries](#pymc_cache.DiskCache.entries) | List this format's entries without materializing posterior data. |
20
+ | [prune](#pymc_cache.DiskCache.prune) | Keep the N newest entries per model; return count removed. |
21
+
22
+ ### clear { #pymc_cache.DiskCache.clear }
23
+
24
+ ```python
25
+ DiskCache.clear()
26
+ ```
27
+
28
+ Delete only format-owned entries, preserving other files.
29
+
30
+ ### delete { #pymc_cache.DiskCache.delete }
31
+
32
+ ```python
33
+ DiskCache.delete(key)
34
+ ```
35
+
36
+ Remove matching owned entries, never arbitrary files or symlinks.
37
+
38
+ ### entries { #pymc_cache.DiskCache.entries }
39
+
40
+ ```python
41
+ DiskCache.entries()
42
+ ```
43
+
44
+ List this format's entries without materializing posterior data.
45
+
46
+ ### prune { #pymc_cache.DiskCache.prune }
47
+
48
+ ```python
49
+ DiskCache.prune(keep=None)
50
+ ```
51
+
52
+ Keep the N newest entries per model; return count removed.
@@ -0,0 +1,21 @@
1
+ # MemoryCache { #pymc_cache.MemoryCache }
2
+
3
+ ```python
4
+ MemoryCache(max_entries=8)
5
+ ```
6
+
7
+ Thread-safe, copy-isolated LRU of posterior results.
8
+
9
+ ## Methods
10
+
11
+ | Name | Description |
12
+ | --- | --- |
13
+ | [clear](#pymc_cache.MemoryCache.clear) | Discard stored entries and return the number removed. |
14
+
15
+ ### clear { #pymc_cache.MemoryCache.clear }
16
+
17
+ ```python
18
+ MemoryCache.clear()
19
+ ```
20
+
21
+ Discard stored entries and return the number removed.
@@ -0,0 +1,7 @@
1
+ # UncacheableError { #pymc_cache.UncacheableError }
2
+
3
+ ```python
4
+ UncacheableError()
5
+ ```
6
+
7
+ Raised when the identity of a model or sampling option cannot be established.
@@ -0,0 +1,15 @@
1
+ # cache_key { #pymc_cache.cache_key }
2
+
3
+ ```python
4
+ cache_key(
5
+ model_identity,
6
+ sample_kwargs,
7
+ *,
8
+ operation='sample',
9
+ versions=None,
10
+ namespace='',
11
+ sampler_key=None,
12
+ )
13
+ ```
14
+
15
+ Full SHA-256 identity of a model, sampler settings and environment.
@@ -0,0 +1,7 @@
1
+ # cleanup_cache { #pymc_cache.cleanup_cache }
2
+
3
+ ```python
4
+ cleanup_cache(base_dir=None)
5
+ ```
6
+
7
+ Remove only new-format entries in a directory; do not delete the root.
@@ -0,0 +1,20 @@
1
+ # Function reference {.doc .doc-index}
2
+
3
+ ## Public API
4
+
5
+ | | |
6
+ | --- | --- |
7
+ | [sample](sample.qmd#pymc_cache.sample) | Run ``pm.sample`` (or a trusted runner), reusing a matching result. |
8
+ | [sample_smc](sample_smc.qmd#pymc_cache.sample_smc) | Run and cache ``pm.sample_smc`` without conflating it with NUTS. |
9
+ | [CacheBackend](CacheBackend.qmd#pymc_cache.CacheBackend) | Minimal backend interface; extra management operations are optional. |
10
+ | [MemoryCache](MemoryCache.qmd#pymc_cache.MemoryCache) | Thread-safe, copy-isolated LRU of posterior results. |
11
+ | [DiskCache](DiskCache.qmd#pymc_cache.DiskCache) | One-file, full-key-verified atomic NetCDF result cache. |
12
+ | [CacheSession](CacheSession.qmd#pymc_cache.CacheSession) | Compute identity and check a cache before beginning costly sampling. |
13
+ | [CachePolicy](CachePolicy.qmd#pymc_cache.CachePolicy) | Select whether unseeded sampling may reuse a previous run. |
14
+ | [CacheEntry](CacheEntry.qmd#pymc_cache.CacheEntry) | Metadata for an owned, valid disk entry (without loading posterior arrays). |
15
+ | [CacheLookup](CacheLookup.qmd#pymc_cache.CacheLookup) | Immutable record of a pre-sampling cache lookup. |
16
+ | [UncacheableError](UncacheableError.qmd#pymc_cache.UncacheableError) | Raised when the identity of a model or sampling option cannot be established. |
17
+ | [CacheWarning](CacheWarning.qmd#pymc_cache.CacheWarning) | A cache was bypassed or an entry could not be persisted. |
18
+ | [cleanup_cache](cleanup_cache.qmd#pymc_cache.cleanup_cache) | Remove only new-format entries in a directory; do not delete the root. |
19
+ | [model_digest](model_digest.qmd#pymc_cache.model_digest) | Content hash of model graph, data, outputs and coordinates. |
20
+ | [cache_key](cache_key.qmd#pymc_cache.cache_key) | Full SHA-256 identity of a model, sampler settings and environment. |
@@ -0,0 +1,10 @@
1
+ # model_digest { #pymc_cache.model_digest }
2
+
3
+ ```python
4
+ model_digest(model, *, model_key=None)
5
+ ```
6
+
7
+ Content hash of model graph, data, outputs and coordinates.
8
+
9
+ Custom Python operations require a user-maintained ``model_key``. Their
10
+ implementation cannot be inferred reliably from an Op's class name.
@@ -0,0 +1,21 @@
1
+ # sample { #pymc_cache.sample }
2
+
3
+ ```python
4
+ sample(
5
+ model=None,
6
+ cache=True,
7
+ overwrite=False,
8
+ policy=None,
9
+ namespace='',
10
+ model_key=None,
11
+ sampler_key=None,
12
+ sample_fn=None,
13
+ **sample_kwargs,
14
+ )
15
+ ```
16
+
17
+ Run ``pm.sample`` (or a trusted runner), reusing a matching result.
18
+
19
+ No global PyMC function is replaced. ``cache='disk'`` persists under
20
+ ``.pymc-cache``; ``cache=False`` always samples anew. A user-supplied
21
+ sample_fn must opt into safe identity or supply a trusted sampler_key.
@@ -0,0 +1,16 @@
1
+ # sample_smc { #pymc_cache.sample_smc }
2
+
3
+ ```python
4
+ sample_smc(
5
+ model=None,
6
+ cache=True,
7
+ overwrite=False,
8
+ policy=None,
9
+ namespace='',
10
+ model_key=None,
11
+ sampler_key=None,
12
+ **sample_kwargs,
13
+ )
14
+ ```
15
+
16
+ Run and cache ``pm.sample_smc`` without conflating it with NUTS.
@@ -0,0 +1,5 @@
1
+ ---
2
+ title: Contributing
3
+ ---
4
+
5
+ Follow the [repository contributing guide](https://github.com/justmytwospence/pymc-cache/blob/main/CONTRIBUTING.md) for installation and checks. Cache identity and persistence changes need explicit tests for false hits, incomplete results, older ArviZ and legacy data preservation. Do not commit posterior files, `.extraction-source/`, or generated site output.
@@ -0,0 +1,2 @@
1
+ /* Small readable adjustments to the default Quarto theme. */
2
+ main.content { max-width: 68rem; }
@@ -0,0 +1,26 @@
1
+ """Run a tiny repeatable PyMC sample twice with a private disk cache."""
2
+ from __future__ import annotations
3
+
4
+ import tempfile
5
+ from pathlib import Path
6
+
7
+ import pymc as pm
8
+
9
+ import pymc_cache as pc
10
+
11
+
12
+ def main() -> None:
13
+ with pm.Model() as model:
14
+ pm.Normal("mu", 0, 1)
15
+ with tempfile.TemporaryDirectory(prefix="pymc-cache-example-") as directory:
16
+ options = dict(model=model, draws=10, tune=10, chains=1, cores=1,
17
+ random_seed=42, progressbar=False, cache=Path(directory))
18
+ first = pc.sample(**options)
19
+ second = pc.sample(**options)
20
+ assert first is not second
21
+ assert len(pc.DiskCache(directory).entries()) == 1
22
+ print("Reused", len(second.posterior.draw), "draws from disk")
23
+
24
+
25
+ if __name__ == "__main__":
26
+ main()
@@ -0,0 +1,36 @@
1
+ """Implement a minimal copy-isolated backend without a storage dependency."""
2
+ from __future__ import annotations
3
+
4
+ import copy
5
+
6
+ import pymc as pm
7
+
8
+ import pymc_cache as pc
9
+
10
+
11
+ class PrivateBackend:
12
+ def __init__(self):
13
+ self.entries = {}
14
+
15
+ def load(self, key, **kwargs):
16
+ value = self.entries.get(key)
17
+ return copy.deepcopy(value) if value is not None else None
18
+
19
+ def save(self, key, idata, **kwargs):
20
+ self.entries[key] = copy.deepcopy(idata)
21
+
22
+
23
+ def main():
24
+ with pm.Model() as model:
25
+ pm.Normal("x")
26
+ backend = PrivateBackend()
27
+ options = dict(model=model, cache=backend, draws=2, tune=2, chains=1,
28
+ cores=1, random_seed=42, progressbar=False)
29
+ first = pc.sample(**options)
30
+ second = pc.sample(**options)
31
+ assert first is not second and len(backend.entries) == 1
32
+ print("Custom backend reused", len(second.posterior.draw), "draws")
33
+
34
+
35
+ if __name__ == "__main__":
36
+ main()
@@ -0,0 +1,48 @@
1
+ """Preflight an optional monitor runner before starting its work.
2
+
3
+ The callable is injected: pymc-cache does not import or install pymc-monitor.
4
+ Only pass a stable sampler_identity/versions for a runner whose semantics you own.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import pymc as pm
9
+
10
+ import pymc_cache as pc
11
+ from pymc_cache.policy import report_from_idata
12
+
13
+
14
+ def run_with_monitor(model, monitor_run, *, cache, sampler_identity, versions, **options):
15
+ """Reuse a result or call an external monitor; never store unverified adaptive runs."""
16
+ session = pc.CacheSession(cache=cache, model=model)
17
+ lookup = session.lookup(options, sampler_identity=sampler_identity, versions=versions)
18
+ if lookup.hit:
19
+ return lookup.result, True
20
+ result = monitor_run(model=model, **lookup.normalized_kwargs)
21
+ report = report_from_idata(result)
22
+ status = report.get("status", "unverified") if report else (
23
+ "unverified" if options.get("until") else "complete"
24
+ )
25
+ session.store(lookup, result, status=status, effective_sampler=sampler_identity)
26
+ return result, False
27
+
28
+
29
+ def main():
30
+ # Replace pm.sample with your pymc-monitor runner after verifying its call
31
+ # signature, version and convergence-report contract in your installation.
32
+ with pm.Model() as model:
33
+ pm.Normal("x")
34
+ cache = pc.MemoryCache()
35
+ opts = dict(draws=2, tune=2, chains=1, cores=1, random_seed=42, progressbar=False)
36
+ first, hit = run_with_monitor(model, pm.sample, cache=cache,
37
+ sampler_identity="native-pymc-example", versions={
38
+ "runner": pm.__version__}, **opts)
39
+ assert not hit
40
+ second, hit = run_with_monitor(model, pm.sample, cache=cache,
41
+ sampler_identity="native-pymc-example", versions={
42
+ "runner": pm.__version__}, **opts)
43
+ assert hit and first is not second
44
+ print("Preflight skipped the runner:", hit)
45
+
46
+
47
+ if __name__ == "__main__":
48
+ main()