The phasic model hub

munch-group/phasic-traces is the public registry where pre-computed elimination artifacts are published.

Every parameterised phasic graph has a deterministic content hash (ptd_graph_content_hash). The first time you call g.expectation() (or g.pdf() / g.moments()), the C side runs the symbolic elimination and writes the result to ~/.phasic_cache/parameterized_reward_compute/<hash>.bin. The next call on a structurally identical graph reuses that file — even in a different process.

The sharing system lets you exchange those .bin files with other users via a public GitHub registry at munch-group/phasic-traces. Two methods on Graph cover the whole story:

If someone published a .bin for this graph, you download it into the local cache using g.pull_cache(). To list models available at phasic-traces use the top-level function list_computes for browsing without a graph. E.g., phasic.list_computes(domain='population-genetics')

To make an elimination available to others, use g.push_cache(id=..., description=...) to compute it (unless already cached locally) and submit it as a pull request for the phasic-traces repository. This feature needs git and the GitHub CLI gh, authenticated once with gh auth login in a terminal.Both are easily installed using either Pixi, Conda or Pip:

pixi global install git gh
conda install git gh
pip install git gh

Setup

import numpy as np
import phasic
from phasic import Graph, list_computes
from phasic.compute_repository import _param_compute_cache_dir

What’s published right now?

list_computes() downloads registry.json from the hub and returns one dict per published artifact. Use the domain, model_type, and tags keyword arguments to filter.

for entry in list_computes():
    print(
        f"{entry['compute_id']:24s}  "
        f"vertices={entry.get('vertices', '?'):>4}  "
        f"params={entry.get('param_length', '?')}  "
        f"hash={entry['graph_hash'][:16]}..."
    )
coal_n5_theta1            vertices=   6  params=1  hash=4d652ed73c66ed4a...
coal_n10_theta1           vertices=  11  params=1  hash=2c6488c6486ae2d2...
coal_n20_theta1           vertices=  21  params=1  hash=6f7690305bd5c9ec...
# Drill in: just the coalescent models, with their full metadata.
for entry in list_computes(model_type='coalescent'):
    print(entry['compute_id'])
    print(f"  description:    {entry['description']}")
    print(f"  tags:           {entry.get('tags', [])}")
    print(f"  format_revision: {entry['format_revision']}")
    print()
coal_n5_theta1
  description:    Kingman coalescent for n=5 haploid samples (1 parameter)
  tags:           ['coalescent', 'kingman', 'population-genetics']
  format_revision: 2

coal_n10_theta1
  description:    Kingman coalescent for n=10 haploid samples (1 parameter)
  tags:           ['coalescent', 'kingman', 'population-genetics']
  format_revision: 2

coal_n20_theta1
  description:    Kingman coalescent for n=20 haploid samples (1 parameter)
  tags:           ['coalescent', 'kingman', 'population-genetics']
  format_revision: 2

Reusing a published model

If you build a graph that’s structurally identical to one in the hub, pull_cache() downloads the pre-computed elimination and drops it into the local phasic cache. The next call to expectation(), pdf(), or moments() reads the file via the normal C-side cache lookup — no extra plumbing needed.

The hub has a coalescent for n=5 published as coal_n5_theta1. We’ll recreate it locally and reuse the published elimination.

def coalescent_callback(state):
    """Standard Kingman coalescent: rate n(n-1)/2."""
    n = state[0]
    if n <= 1:
        return []
    return [(np.array([n - 1]), [n * (n - 1) / 2])]

g = Graph(coalescent_callback, ipv=[5])
print(f'vertices:      {g.vertices_length()}')
print(f'param_length:  {g.param_length()}')
print(f'graph hash:    {phasic.hash.compute_graph_hash(g).hash_hex[:32]}...')
vertices:      6
param_length:  1
graph hash:    4d652ed73c66ed4ada3dd13e0dde2051...

Wipe the local cache to simulate a fresh machine, then pull from the hub:

cache_dir = _param_compute_cache_dir()
hash_hex = phasic.hash.compute_graph_hash(g).hash_hex
bin_path = cache_dir / f'{hash_hex}.bin'

if bin_path.exists():
    bin_path.unlink()
print(f'before pull: file present = {bin_path.exists()}')

hit = g.pull_cache()
print(f'pull_cache():           {hit}')
print(f'after pull: file present = {bin_path.exists()}')
print(f'size: {bin_path.stat().st_size:,} bytes')
before pull: file present = False
pull_cache():           True
after pull: file present = True
size: 5,064 bytes

Now expectation() (and pdf, moments, etc.) reuses the downloaded elimination. The result must match what the publisher computed (1.6 = expected time to coalescence for n=5 under the standard rate):

exp = g.expectation()
print(f'expectation: {exp}')
assert abs(exp - 1.6) < 1e-10, 'unexpected expectation value'
expectation: 1.6

Calling pull_cache() on an unregistered graph

pull_cache() returns False if there’s no entry for the graph’s content hash. It doesn’t raise — you can check the return value to decide whether to run elimination locally or publish your own artifact afterwards.

# Build a coalescent with a non-standard sample size (n=4 is not
# published as of this writing).
g_unpublished = Graph(coalescent_callback, ipv=[4])
hit = g_unpublished.pull_cache()
print(f'pull_cache for n=4: {hit}')
pull_cache for n=4: False

Verifying a published model end-to-end

One use of the hub: verify that someone else’s published expectation matches what you get when running the elimination locally from the same callback. This is roughly the workflow for reviewing a paper’s numerical claims.

Clear the cache, recompute locally, and compare against the published artifact’s result.

# Step 1: clear local cache, compute fresh.
if bin_path.exists():
    bin_path.unlink()
g_fresh = Graph(coalescent_callback, ipv=[5])
exp_local = g_fresh.expectation()
print(f'local computation:  {exp_local}')

# Step 2: clear cache again, pull from hub.
if bin_path.exists():
    bin_path.unlink()
g_hub = Graph(coalescent_callback, ipv=[5])
assert g_hub.pull_cache() is True
exp_hub = g_hub.expectation()
print(f'hub artifact:       {exp_hub}')

print(f'agreement: {abs(exp_local - exp_hub) < 1e-10}')
local computation:  1.6
hub artifact:       1.6
agreement: True

Both come out to 1.6 to machine precision. The hub artifact is a faithful capture of the local elimination.

Previewing what a publish would look like

g.push_cache(..., dry_run=True) builds the artifact, computes its SHA-256, and returns a JSON string of the entry that would be added to registry.json. It does not touch the network. Use this to check your metadata before opening an actual PR.

We’ll preview what publishing a small custom model would generate:

# A two-step parameterised chain (not in the hub).
g_demo = Graph(1)
v0 = g_demo.starting_vertex()
v1 = g_demo.find_or_create_vertex([1])
v2 = g_demo.find_or_create_vertex([2])
v0.add_edge(v1, [1.0])
v1.add_edge(v2, [1.0])
g_demo.update_weights([2.0])

entry_json = g_demo.push_cache(
    id='demo_two_step_chain',
    description='Two-step parameterised chain (demo, not for publishing)',
    domain='examples',
    model_type='chain',
    tags=['demo'],
    dry_run=True,
)
print(entry_json)
{
  "demo_two_step_chain": {
    "graph_hash": "29dc136099b6cf8de1d9180a234a2358b63c5ddca6649889f9ed6a25d914418c",
    "format_revision": 3,
    "artifacts": {
      "parent": {
        "cid_or_path": "artifacts/29dc136099b6cf8de1d9180a234a2358b63c5ddca6649889f9ed6a25d914418c.bin",
        "sha256": "c2ebeaf5cbb025a3bc67a66bc955d5cb0d489de9aadb950d59fb672081bc1209",
        "size_bytes": 2304
      },
      "scc": []
    },
    "metadata": {
      "description": "Two-step parameterised chain (demo, not for publishing)",
      "author": "Kasper Munch <kaspermunch@birc.au.dk>",
      "license": "MIT",
      "vertices": 3,
      "param_length": 1,
      "domain": "examples",
      "model_type": "chain",
      "tags": [
        "demo"
      ]
    }
  }
}

Inspect the preview before deciding whether to publish for real. A few things worth checking:

  • graph_hash matches what compute_graph_hash(g) returned. Anyone re-building the same graph will get the same hash and reach this artifact via pull_cache().
  • format_revision is 2 (the current C-side format). Older builds with format_revision < 2 would reject this entry; newer builds will load it.
  • parent.sha256 is the cryptographic checksum of the .bin. Consumers verify it before installing.
  • metadata.vertices and param_length are filled in for you.
  • metadata.author comes from your local git config user.name/email. Override with the author= keyword if you want to attribute differently.

Once the preview looks right, drop dry_run=True to actually open the PR (requires gh auth login set up first).