from phasic import (
configure, get_config, get_available_options, reset_config,
PhasicConfig, PTDConfigError,
)
import os
import sys
from vscodenb import set_vscode_theme
set_vscode_theme()Configuration
Phasic exposes a single configure() entry point for tuning the library’s behaviour: which compute backend to use, how many CPU threads OpenMP should spawn, when to engage high-precision arithmetic, whether to parallelise Gaussian elimination across strongly-connected components, where to put the on-disk cache, and so on. Every public field describes a user-visible outcome — never which third-party library or internal mechanism realises it.
configure() can be called persistently (the settings stay applied until further changes) or used as a context manager (with configure(...): ...) for a single block of code. Each field is also reachable via a PHASIC_* / OMP_NUM_THREADS environment variable, so SLURM job scripts, CI pipelines, and shell aliases can set the same knobs without touching Python. The two interfaces are kept consistent: configure(...) writes to the env var, and get_config() seeds itself from the env var on first call. If a shell-set env var and an explicit configure() argument disagree, the library raises PTDConfigError rather than silently choosing one — every conflict surfaces loudly.
The defaults
configure() with no arguments is a no-op; the defaults are already in effect. Inspect them via get_config():
cfg = get_config()
print(cfg)PhasicConfig:
{
"fields": {
"compute": "auto",
"cpu_threads": 8,
"parallel_elimination": false,
"parallel_elimination_min_subgraph": null,
"parallel_elimination_max_concurrent": null,
"svgd_strategy": "auto",
"high_precision_mode": "auto",
"high_precision_bits": null,
"ill_condition_threshold": 1000000000000.0,
"warn_on_ill_conditioning": true,
"reward_compute_cache": false,
"graph_cache": true,
"cache_dir": null,
"strict": true,
"verbose": false
},
"environment": {
"OMP_NUM_THREADS": "8",
"PHASIC_HIERAR_ELIMINATION": null,
"PHASIC_MIN_SCC_SIZE_TO_CACHE": null,
"PHASIC_MAX_PARALLEL_SCCS": null,
"PHASIC_FORCE_MPFR": null,
"PHASIC_MPFR_BITS": null,
"PHASIC_CONDITION_THRESHOLD": null,
"PHASIC_DISABLE_CONDITION_WARNINGS": null,
"PHASIC_REWARD_COMPUTE_CACHE": null,
"PHASIC_DISABLE_GRAPH_CACHE": null,
"PHASIC_CACHE_DIR": null
},
"derived": {
"jax_active": false,
"mpfr_active": false,
"cpu_threads_source": "configure()",
"cache_dir_resolved": "/Users/kmt/.phasic_cache",
"compute_resolved": "jax-cpu"
}
}
Every default is chosen so the library “just works” out of the box on a single laptop:
compute='auto'— JAX path if installable, else pure C++.cpu_threads=None— phasic auto-detects on import fromSLURM_CPUS_PER_TASK,SLURM_CPUS_ON_NODE,os.sched_getaffinity(0), thenos.cpu_count().parallel_elimination=False— the monolithic Gaussian elimination is fine for small to medium graphs.high_precision_mode='auto'— MPFR engages automatically when the condition number exceedsill_condition_threshold(default1e12).cache_enabled=True,cache_dir=None— caches live under~/.phasic_cache/.strict=True— incompatible requests raise instead of warning.verbose=False— only warnings and errors are logged.
Available options
The 13 public fields are organised by user intent:
Compute backend
| Field | Type | Default | Effect |
|---|---|---|---|
compute |
'auto', 'cpu', 'jax-cpu', 'jax-gpu' |
'auto' |
Single knob for the compute backend. 'auto' picks JAX if importable, else falls back to pure C++. 'cpu' disables JAX/JIT/FFI entirely. 'jax-gpu' raises if no GPU device is visible. |
Parallel execution
| Field | Type | Default | Effect | Env var |
|---|---|---|---|---|
cpu_threads |
int or None |
None (auto) |
OpenMP thread count. Backs OMP_NUM_THREADS. |
OMP_NUM_THREADS |
parallel_elimination |
bool |
False |
Opt into the hierarchical SCC composer: graph is decomposed into strongly-connected components, sibling SCCs at each level eliminated concurrently under OpenMP. | PHASIC_HIERAR_ELIMINATION |
parallel_elimination_min_subgraph |
int or None |
None (4) |
SCCs whose synthetic graph has fewer vertices skip the cache; below this size the elimination is too cheap to be worth a disk write. | PHASIC_MIN_SCC_SIZE_TO_CACHE |
parallel_elimination_max_concurrent |
int or None |
None (no cap) |
Cap on simultaneous per-SCC computes within one level. Lets you keep OpenMP threads for nested work while limiting SCC-level fan-out. | PHASIC_MAX_PARALLEL_SCCS |
Numerical precision
| Field | Type | Default | Effect | Env var |
|---|---|---|---|---|
high_precision_mode |
'auto', 'always', 'never' |
'auto' |
When to engage MPFR arithmetic. 'auto': when the condition number exceeds ill_condition_threshold. 'always': every moment computation. 'never': never. |
PHASIC_FORCE_MPFR |
high_precision_bits |
int or None |
None (auto from condition) |
MPFR precision in bits. Must be ≥ 53 if set. | PHASIC_MPFR_BITS |
ill_condition_threshold |
float |
1e12 |
Condition number above which 'auto' mode engages MPFR. |
PHASIC_CONDITION_THRESHOLD |
warn_on_ill_conditioning |
bool |
True |
Emit log warnings when condition number is high. | PHASIC_DISABLE_CONDITION_WARNINGS (inverted) |
Caching
| Field | Type | Default | Effect | Env var |
|---|---|---|---|---|
cache_enabled |
bool |
True |
Use the on-disk caches under cache_dir. |
PHASIC_DISABLE_CACHE (inverted) |
cache_dir |
str or None |
None ($HOME/.phasic_cache) |
Cache root. On SLURM clusters, point this at a shared filesystem so workers share cache entries. | PHASIC_CACHE_DIR |
Runtime behaviour
| Field | Type | Default | Effect |
|---|---|---|---|
strict |
bool |
True |
Raise on missing-feature warnings (e.g. JAX requested but not installed). Does NOT affect conflict checking — conflicts always raise. |
verbose |
bool |
False |
Print configuration details on validate(). |
Setting options
Call configure() with any subset of the fields above. All updates are validated together and only take effect if no conflicts are detected. Unrecognised kwargs raise PTDConfigError with the full list of valid names.
# Force MPFR for every moment computation, at 256-bit precision.
configure(high_precision_mode='always', high_precision_bits=256)
# Reset to defaults.
reset_config()# Enable parallel SCC elimination on a multi-core machine
# and put the cache on a shared filesystem.
import tempfile
shared_cache = tempfile.mkdtemp(prefix='phasic_cache_')
configure(
parallel_elimination=True,
parallel_elimination_max_concurrent=4,
cache_dir=shared_cache,
)
# Re-read to confirm the env vars were written.
import os
print('PHASIC_HIERAR_ELIMINATION =', os.environ.get('PHASIC_HIERAR_ELIMINATION'))
print('PHASIC_MAX_PARALLEL_SCCS =', os.environ.get('PHASIC_MAX_PARALLEL_SCCS'))
print('PHASIC_CACHE_DIR =', os.environ.get('PHASIC_CACHE_DIR'))
reset_config()
import shutil; shutil.rmtree(shared_cache)
# Restore env vars to a clean state for the next cells.
for v in ('PHASIC_HIERAR_ELIMINATION', 'PHASIC_MAX_PARALLEL_SCCS', 'PHASIC_CACHE_DIR'):
os.environ.pop(v, None)PHASIC_HIERAR_ELIMINATION = 1
PHASIC_MAX_PARALLEL_SCCS = 4
PHASIC_CACHE_DIR = /var/folders/s6/srs8qkh52w1_h32d65z95tth0000gn/T/phasic_cache_z4oeior5
Temporary settings with context manager
configure(...) returns a context-manager-shaped object. Using it with a with block applies the settings on entry and rolls them back at the end of the block — useful when you want to switch behavior for a single computation without leaking the change to the rest of the script.
The rollback is comprehensive: every field touched by the configure() call, and every phasic-tracked environment variable, is restored to whatever value it held just before the with statement. If the block raises, the rollback still happens. Conflict checking fires at the with statement, not inside the body. If the settings disagree with the shell environment, configure() raises before the block runs and no rollback is needed.
# Default: parallel_elimination is False.
print('outside:', get_config().parallel_elimination)
with configure(parallel_elimination=True, parallel_elimination_max_concurrent=4):
# Inside the block, the field is set and the env var is written.
print('inside :', get_config().parallel_elimination,
'env =', os.environ.get('PHASIC_HIERAR_ELIMINATION'))
# Outside the block, everything is rolled back.
print('outside:', get_config().parallel_elimination,
'env =', os.environ.get('PHASIC_HIERAR_ELIMINATION'))outside: False
inside : True env = 1
outside: False env = None
You can also bind the live PhasicConfig to inspect or read from inside the block:
with configure(high_precision_mode='always', high_precision_bits=256) as cfg:
print(f"inside the block: mode={cfg.high_precision_mode!r}, "
f"bits={cfg.high_precision_bits}")inside the block: mode='always', bits=256
with configure(parallel_elimination=True):
with configure(cpu_threads=4):
cfg = get_config()
print('inner:', cfg.parallel_elimination, cfg.cpu_threads)
cfg = get_config()
print('outer:', cfg.parallel_elimination, cfg.cpu_threads)
cfg = get_config()
print('after:', cfg.parallel_elimination, cfg.cpu_threads)inner: True 4
outer: True None
after: False None
Inspecting the effective configuration
get_config().effective() returns a dict with three sections: the current public field values, the corresponding environment variables, and a derived section that answers practical questions (“is JAX active right now?”, “where did cpu_threads come from?”, “what does compute='auto' resolve to on this system?”). repr(cfg) pretty-prints the same information.
configure(parallel_elimination=True, cpu_threads=4)
eff = get_config().effective()
print('Fields:')
for k, v in eff['fields'].items():
print(f' {k:38s} = {v}')
print()
print('Environment:')
for k, v in eff['environment'].items():
print(f' {k:38s} = {v}')
print()
print('Derived:')
for k, v in eff['derived'].items():
print(f' {k:38s} = {v}')
reset_config()Fields:
compute = auto
cpu_threads = 4
parallel_elimination = True
parallel_elimination_min_subgraph = None
parallel_elimination_max_concurrent = None
svgd_strategy = auto
high_precision_mode = auto
high_precision_bits = None
ill_condition_threshold = 1000000000000.0
warn_on_ill_conditioning = True
reward_compute_cache = False
graph_cache = True
cache_dir = None
strict = True
verbose = False
Environment:
OMP_NUM_THREADS = 4
PHASIC_HIERAR_ELIMINATION = 1
PHASIC_MIN_SCC_SIZE_TO_CACHE = None
PHASIC_MAX_PARALLEL_SCCS = None
PHASIC_FORCE_MPFR = None
PHASIC_MPFR_BITS = None
PHASIC_CONDITION_THRESHOLD = None
PHASIC_DISABLE_CONDITION_WARNINGS = None
PHASIC_REWARD_COMPUTE_CACHE = None
PHASIC_DISABLE_GRAPH_CACHE = None
PHASIC_CACHE_DIR = None
Derived:
jax_active = False
mpfr_active = False
cpu_threads_source = configure()
cache_dir_resolved = /Users/kmt/.phasic_cache
compute_resolved = jax-cpu
Profiling SCC decomposability
Before turning parallel_elimination=True on, it’s useful to know whether a graph actually decomposes into many small SCCs (where parallel elimination helps) or into a few large ones (where it doesn’t). Graph.plot_scc_decomp() draws a level-wise treemap that answers this at a glance:
- Each row is a level of the condensation. The source side of the chain (the start vertex’s SCC) is drawn at the top; the sink side (absorbing state) is at the bottom. Time flows downward.
- Each tile within a row is one SCC; its width is proportional to the SCC’s vertex count, on an absolute scale shared across all rows. A narrow row genuinely has fewer total vertices than a wide one.
- The label on the left of each row, e.g.
L5 (23), tells you how many parallel-eliminable SCCs that level contains.
Wide rows mean good parallelism; rows with a single small tile are elimination bottlenecks that the SCC composer can’t speed up.
from phasic import Graph, with_ipv
# A coalescent with 6 lineages — enough to show several levels.
@with_ipv([6] + [0] * 5)
def coalescent(state):
transitions = []
for i in range(state.size):
for j in range(i, state.size):
same = int(i == j)
if same and state[i] < 2: continue
if not same and (state[i] < 1 or state[j] < 1): continue
new = state.copy()
new[i] -= 1
new[j] -= 1
new[i + j + 1] += 1
transitions.append((new, state[i] * (state[j] - same) / (1 + same)))
return transitions
g = Graph(coalescent)
g.plot_scc_decomp(figsize=(10, 4));The plot is structural: it depends only on the graph topology, not on any runtime telemetry. For aggregate cache hit/miss counters after a hierarchical compose, use phasic.cache.scc_compose_stats() (see the Multi-level caching tutorial).
The SCC decomposition tutorial walks through four worked examples — a hand-built cycle, a coalescent, a two-locus ARG, and a migration chain — explaining what shapes of treemap indicate good or bad parallelism potential.
Conflict policy: nothing silently overrides anything
If an environment variable was set in the shell (i.e. before phasic was imported) and a later configure() call requests a different value for the matching field, the library raises rather than silently overwriting. This rule applies symmetrically: configure() cannot silently win over the shell, and the shell cannot silently win over configure().
# Pretend OMP_NUM_THREADS was set by the shell (i.e. before
# phasic was imported). The bookkeeping helper below clears
# the "phasic-assigned" marker so the conflict checker treats
# the env var as user-set; in a real shell session this is
# automatic.
from phasic.config import _phasic_assigned_env
_phasic_assigned_env.discard('OMP_NUM_THREADS')
os.environ['OMP_NUM_THREADS'] = '2'
reset_config()
try:
configure(cpu_threads=8)
except PTDConfigError as e:
print('Caught conflict:')
print(' ', str(e)[:200])
# Resolve by aligning the values, or by unsetting the env var.
configure(cpu_threads=2) # agrees with shell — no conflict
print()
print('After agreement: OMP_NUM_THREADS =', os.environ['OMP_NUM_THREADS'])
# Clean up.
del os.environ['OMP_NUM_THREADS']
reset_config()Caught conflict:
Conflicting configuration for cpu_threads: shell sets OMP_NUM_THREADS='2' but configure(cpu_threads=8) would write OMP_NUM_THREADS='8'. Unset OMP_NUM_THREADS before launch, or align the two values.
After agreement: OMP_NUM_THREADS = 2
The same policy applies to validation: configure(high_precision_mode='always') on a build without MPFR raises, configure(compute='jax-gpu') on a CPU-only machine raises, configure(parallel_elimination=True, cpu_threads=1) raises (parallelism with one thread is meaningless), and so on. strict=False only downgrades availability warnings (e.g. “no GPU detected”); conflicts and contradictions always raise.
Probing what’s available
Use get_available_options() to branch on build-time features before configuring:
opts = get_available_options()
for k, v in opts.items():
print(f' {k:14s} = {v}') jax = True
cpp = True
mpfr = True
platforms = ['cpu']
# Example: enable MPFR-always only when MPFR is built.
if opts['mpfr']:
configure(high_precision_mode='always')
else:
configure(high_precision_mode='auto')
reset_config()Environment variables
All public fields except compute, strict, and verbose are backed by a PHASIC_* or OMP_NUM_THREADS environment variable. Setting them in the shell has the same effect as calling configure():
# Equivalent to phasic.configure(parallel_elimination=True,
# parallel_elimination_max_concurrent=8,
# cache_dir='/scratch/phasic_cache')
export PHASIC_HIERAR_ELIMINATION=1
export PHASIC_MAX_PARALLEL_SCCS=8
export PHASIC_CACHE_DIR=/scratch/phasic_cache
python my_script.pyWhen phasic is imported, get_config() reads each of these env vars and seeds the corresponding field, so the dataclass and the C runtime always agree on what’s enabled. On SLURM clusters this is the recommended path: the job script sets the env vars and the Python code never has to touch configure(...).
Resetting
reset_config() discards the cached global config object so the next get_config() rebuilds it fresh from the current environment. Useful in tests, less so in production code. It does not roll back env vars that previous configure(...) calls wrote — if you need a fully clean environment, unset the relevant PHASIC_* / OMP_NUM_THREADS variables manually in os.environ first.
# Inspect what's currently in the environment, reset, and confirm.
print('Before reset:', list(k for k in os.environ if k.startswith('PHASIC_')))
reset_config()
print('After reset: ', list(k for k in os.environ if k.startswith('PHASIC_')))Before reset: ['PHASIC_FORCE_MPFR']
After reset: ['PHASIC_FORCE_MPFR']