Simulation Database API Reference
Simulation archive database — schema and queue interfaces.
This module defines the v2 archive layout described in DESIGN.md: manifest.json + index.jsonl + per-sim status.json/summary.json under sims/<name>/, with frozen code under code/, and an explicit split between RequestQueue (decides what to submit) and RunQueue (actually runs the generator).
The existing classes in BaseManager.py / CondorManager.py / SlurmManager.py keep working unchanged; this module is additive. Real condor/slurm queue implementations should land here as small subclasses of RequestQueue / RunQueue.
- Status:
Manifest, Index, StatusRecord, code-freeze: implemented.
LocalRequestQueue + LocalRunQueue: implemented (no schedd; runs the frozen generator inline). Used by examples and tests.
DualCondorRequestQueue + DualCondorRunQueue: implemented. The run queue writes per-(sim, level) submit descriptions, assembles a chained DAG, and dispatches via condor_submit_dag (with -name <run_pool> for cross-pool). poll() queries the run pool’s schedd via the cached _htcondor_module from CondorManager (htcondor / htcondor2). Output-on-disk drives final ‘complete’ transitions via Archive.refresh_status_from_disk.
SlurmRunQueue: not yet provided (stubbed out at the design level only). Build on simple_slurm or pyslurmutils; mirror the condor implementation.
make_queues_from_manifest(archive): instantiates queues from the manifest’s request_queue / run_queue config. Used by cli/request_sim.py –ensure to attach queues automatically.
- class RIFT.simulation_manager.database.Archive(base_location: str | Path, manifest: Manifest | None = None, request_queue: RequestQueue | None = None, run_queue: RunQueue | None = None, generator_spec: Callable[[...], Any] | str | PathLike | Dict[str, Any] | None = None, summarizer_spec: Callable[[...], Any] | str | PathLike | Dict[str, Any] | None = None, same_q_spec: Callable[[...], Any] | str | PathLike | Dict[str, Any] | None = None, lookup_key_spec: Callable[[...], Any] | str | PathLike | Dict[str, Any] | None = None)
Bases:
objectThin facade over the on-disk layout. Owns the FSM; queues report state, this class applies it. Construction either creates a new archive (when manifest is given) or rehydrates an existing one (when only base_location is given).
- expected_output(sim_name: str, level: int) Tuple[str, str]
Return (output_basename, absolute_remap_target) for a (sim, level) worker. Use the basename in transfer_output_files and the pair in transfer_output_remaps so condor places the worker’s output at the canonical archive location on the submit node.
- find_existing(params: Any) str | None
Return the name of an existing sim whose params satisfy same_q(params, existing.params), else None. O(1) average via the lookup_key bucket; O(K) worst case where K is the bucket size (typically 1).
- get_resources(name: str) Dict[str, Any]
- get_status(name: str) str | None
- load_generator() Callable[[...], Any]
- load_summarizer() Callable[[...], Any] | None
- rebuild_index() int
Reconstruct index.jsonl from per-sim status.json files. Useful if the index is corrupted, lost, or out of sync (e.g. after manual file ops). Returns the count of rows written. Re-runs the summarizer would be a separate pass (resummarize_all).
- refine(name: str, target_level: int) bool
Explicit refinement request. Bumps the target_level if needed and (when the sim already had output) transitions it to ‘refine_ready’. Returns True iff a bump occurred.
- refresh_status_from_disk() Dict[str, str]
Sweep every sim and reconcile its levels[] history with the files actually present under sims/<name>/. New level_<N>.json files (e.g. dropped in by a condor worker via transfer_output_remaps) are absorbed; sims promote to ‘complete’ when current_level reaches target_level, or to ‘refine_ready’ if they’re not done.
Returns a dict mapping sim_name -> new_status for every sim whose status changed. Safe to call repeatedly (idempotent when no new outputs have arrived).
- register(params: Any, target_level: int = 1, name: str | None = None) str
Idempotent under same_q + level. Behavior:
sim does not exist: register fresh with target_level=N
sim exists, current_level >= N: return name, no work
sim exists, current_level < N: bump target_level to max(existing, N), transition to ‘refine_ready’, return name
Returns the (existing or newly allocated) sim_name in all cases. Multi-writer-safe: holds the archive lock for the duration.
- resummarize_all(*, only_complete: bool = False) Dict[str, str]
Re-load the manifest’s summarizer and re-run it for every sim with at least one completed level. Used after the summarizer’s source has been updated (a new freeze + manifest rewrite, or an in-place edit of code/summarizer.py for archives whose summarizer is allowed to evolve).
Returns {sim_name: ‘ok’ | ‘no-summarizer’ | ‘no-levels’ | ‘error: <msg>’}. With only_complete=True skips sims whose status isn’t ‘complete’.
- set_resources(name: str, request_memory: int | None = None, request_disk: str | None = None, extra_condor_cmds: Dict[str, str] | None = None) None
Per-sim overrides on top of the queue’s defaults. Only the keys you pass are updated; pass None to leave a field unchanged. The queue’s build_worker reads these and merges them on top of its own defaults at submit time.
- sim_dir(name: str) Path
- simulations_iter_names() Iterable[str]
- transfer_input_files_for(sim_name: str, level: int) List[str]
Files a worker needs on the execute host to compute (sim, level).
Suitable as the value of condor’s transfer_input_files. Paths are returned as filesystem paths on the submit side; condor flattens basenames into the worker sandbox cwd.
- Includes:
<archive>/code — the whole frozen code directory
<archive>/sims/<name>/params.json
<archive>/sims/<name>/level_1.json … level_<level-1>.json (whichever are present on disk)
- transition(name: str, new_status: str, **fields: Any) None
- unstick(name: str, bump_memory: bool = False, bump_factor: float = 1.5) None
Clear stuck, transition to refine_ready (if has prior levels) or ready (if not). With bump_memory=True, also multiplies the per-sim request_memory by bump_factor so the next attempt asks for more headroom.
- unstick_all(bump_memory: bool = False, bump_factor: float = 1.5) List[str]
- update_summary(name: str, summary: Dict[str, Any]) None
- verify() Dict[str, Any]
Cross-check index.jsonl, per-sim status.json, and on-disk files. Returns a structured report — meant to be inspected by an operator before acting on it. Does NOT mutate; pair with rebuild_index if the report shows drift you want to repair.
- with_status(status: str) List[str]
- worker_bootstrap_script() str
Return a self-contained Python bootstrap script (as a string) that the run pool’s executable should be set to. The script expects argv:
–sim-name <n> –level <N> [–code-dir <path>] (default: ‘code’ relative to cwd;
slurm passes the archive’s absolute code/ path here)
[–prev-levels FILE […]]
Reads ./params.json from cwd, writes ./level_<N>.json to cwd. Used by both DualCondorRunQueue (sandbox cwd, files flattened) and SlurmRunQueue (cwd = sim_dir on shared FS).
- RIFT.simulation_manager.database.DEFAULT_OOM_HOLD_CODES = (34, 26)
Hold codes this class treats as “the job ran out of memory”, and the attribute that rations retries. Both are DEFAULTS, not facts: what a hold code means is a property of the site, not of HTCondor. 34 is the unambiguous memory code; 26 is SystemPolicy, which means whatever the site’s SYSTEM_PERIODIC_HOLD expressions say it means – on the LIGO clusters this policy was written for that is usually memory, and on an OSG access point it is as likely to be an anti-thrash limiter whose precondition is a high NumJobStarts. Sites that differ pass oom_hold_codes / oom_hold_subcode_exclusions / oom_retry_counter rather than editing this.
Deliberately NOT recorded here: which sites differ, and how. That belongs in whatever inventory the operator already keeps about their own infrastructure. A table of site facts in shared code is stale the day after it is written and wrong for everyone it does not name.
- class RIFT.simulation_manager.database.DualCondorRequestQueue(run_queue: DualCondorRunQueue | None = None, request_pool: str | None = None, run_pool: str | None = None, run_collector: str | None = None, **submit_kwargs: Any)
Bases:
RequestQueueRuns on the request condor pool. Its job is orchestration: pick the sims that need work, ask the run queue to actually dispatch them, and poll for completion.
In the simplest deployment the request queue is essentially a thin wrapper over the run queue — the planner that decides what to ask for lives upstream (e.g. the user’s hyperpipeline DAG calling Archive.register(params, target_level=N) per node).
- Configuration in the manifest’s request_queue.extra:
request_pool : str # informational; the schedd this DAG runs on run_pool : str # passthrough to the run queue run_collector : str # collector host for htcondor.Schedd lookups accounting_group / accounting_group_user
- kind: str = 'condor'
- class RIFT.simulation_manager.database.DualCondorRunQueue(run_pool: str | None = None, run_collector: str | None = None, request_memory: int = 4096, request_disk: str = '4G', accounting_group: str | None = None, accounting_group_user: str | None = None, getenv: str | None = None, use_singularity: bool = False, singularity_image: str | None = None, extra_condor_cmds: Dict[str, str] | None = None, extra_transfer_input_files: Sequence[str] | None = None, extra_transfer_output_files: Sequence[str] | None = None, auto_release_on_oom: bool = True, extra_periodic_release: str | None = None, oom_hold_codes: Sequence[int] | None = None, oom_hold_subcode_exclusions: Mapping[int, Sequence[int]] | None = None, oom_retry_counter: str | None = None, oom_max_retries: int = 5, oom_memory_factor: float = 1.5, subdag_factory: Callable[[Any, str, int], str] | None = None, submit_mode: str = 'submit', *, container_image: str | None = None, when_to_transfer_output: str | None = None, **submit_kwargs: Any)
Bases:
RunQueueRun-pool queue: ferries per-(sim, level) work to condor execute hosts and tracks completion via the schedd.
Each (sim, level) is one condor job. Levels for the same sim are chained as DAG parents/children so an accumulating generator sees its prior levels’ outputs already on disk before its job starts.
- Configuration:
- run_poolstr – target schedd (-name <run_pool> on
condor_submit_dag for cross-pool). None = local schedd.
- run_collectorstr – collector host for cross-pool
htcondor.Schedd(<collector>) queries in poll(). None = local.
accounting_group – defaults to env LIGO_ACCOUNTING accounting_group_user – defaults to env LIGO_USER_NAME
Both follow the standard LIGO/IGWN convention; the matching env-var fallback matches the legacy CondorManager behaviour so existing submit hosts work unchanged.
request_memory : int (MB), default 4096 request_disk : str (e.g. ‘4G’), default ‘4G’ getenv : str – value of condor ‘getenv’ command.
Default precedence: constructor kwarg > $RIFT_GETENV env > safe allowlist (‘LD_LIBRARY_PATH,PATH,PYTHONPATH,*RIFT*, LIBRARY_PATH’). NOTE: getenv = True is blocked by many sites (CIT among them); the allowlist is the OSG-blessed alternative. Pass getenv=’True’ explicitly only on sites that allow it.
- container_imagestr – KEYWORD-ONLY. Image reference for
HTCondor’s container universe (osdf://, docker://, a local .sif). Setting it switches the job from vanilla to universe = container. Mutually exclusive with use_singularity, which is the legacy +SingularityImage form.
- when_to_transfer_output: str – KEYWORD-ONLY. One of ON_EXIT
(default), ON_EXIT_OR_EVICT, ON_SUCCESS, NEVER. ON_EXIT discards the sandbox on eviction, so a preemptable pool loses whatever the job had already written. Like container_image and the transfer extras, this is emitted by build_worker and so is refused together with subdag_factory: a sub-DAG writes its own submit descriptions, and its nodes would transfer at the default instead.
use_singularity : bool singularity_image: str – required if use_singularity=True oom_hold_codes : seq – hold codes this site reports when a
job runs out of memory. Default DEFAULT_OOM_HOLD_CODES = (34, 26). 34 is unambiguous; 26 is SystemPolicy and means whatever the site’s SYSTEM_PERIODIC_HOLD expressions say, which elsewhere may be an anti-thrash limiter rather than memory.
- oom_hold_subcode_exclusions: {code: [subcode, …]} – sub-codes
to carve out of a code above. Needed because every SYSTEM_PERIODIC_HOLD at a site reports one hold code and only the sub-code separates “over memory” from “restarted too many times”. A sub-code keyed on a code not listed in oom_hold_codes is refused rather than ignored.
- oom_retry_counter: str – ClassAd expression rationing the
retries and scaling the bump. Default DEFAULT_OOM_RETRY_COUNTER = “NumJobStarts”. NumHolds is the other obvious choice and is not better everywhere: it counts holds of every kind, including transfer failures that increment it without the job ever running.
- extra_periodic_release: str – a ClassAd expression OR’d into
periodic_release alongside the OOM policy, for sites that hold jobs for reasons this class does not model. While auto_release_on_oom is on, the term is scoped away from whatever codes oom_hold_codes names, so oom_max_retries stays a real cap and the term governs every other code. With the OOM policy off it governs all of them. Setting periodic_release through extra_condor_cmds is refused – it replaced the whole expression and dropped the memory handling with it.
- extra_condor_cmds: dict – additional key = value lines
appended verbatim to the submit description (e.g. +DESIRED_SITES, +UNDESIRED_SITES for OSG site selection, requirements clauses).
- extra_transfer_input_files: list – extra entries APPENDED to
transfer_input_files for every job. Intended for bulk inputs addressed by URL (osdf://, http://) so they are fetched from a cache instead of staged through the submit host’s spool. Setting transfer_input_files via extra_condor_cmds would instead replace the archive’s own entries and strip the frozen code/ directory, leaving the worker nothing to run.
- extra_transfer_output_files: list – products to bring BACK
beyond the level_<N>.json marker, named relative to the job sandbox. {level} and {sim_name} are substituted, so e.g. “level_{level}” returns a per-level output directory. Each is remapped to the same relative path under sims/<name>/. transfer_output_files is explicit, so without this HTCondor returns only the marker and everything else the worker produced dies with the sandbox — the job completes having discarded its own results.
The defaults above also apply when DualCondorRunQueue is instantiated via make_queues_from_manifest() — keys absent from run_queue.extra in the manifest fall back to the constructor defaults, which in turn fall back to the env vars listed above.
- build_worker(archive: Archive, sim_name: str, level: int = 1) str
Write the per-(sim, level) condor submit description and return its absolute path. Idempotent: re-running it overwrites.
Per-sim resource overrides (set via Archive.set_resources) merge on top of the queue’s defaults: per-sim values for request_memory / request_disk / extra_condor_cmds win where set.
- property container_image: str
- property extra_periodic_release: str
- property extra_transfer_input_files: Tuple[str, ...]
- property extra_transfer_output_files: Tuple[str, ...]
- kind: str = 'condor'
- property oom_hold_codes: Tuple[int, ...]
- property oom_hold_subcode_exclusions: Mapping[int, Tuple[int, ...]]
- property oom_retry_counter: str
- poll(archive: Archive, sim_names: Iterable[str]) Dict[str, str]
Query the run pool’s schedd; sims with at least one job still in the queue go to ‘running’; sims that previously had jobs but now don’t, with their target levels’ output files present, are reconciled by Archive.refresh_status_from_disk (called by the request queue right after this returns).
Works with htcondor or htcondor2 via the cached _htcondor_module from CondorManager. If neither binding is available, returns the currently-recorded statuses without modification.
- submit(archive: Archive, sim_names: Iterable[str]) List[Tuple[str, str]]
For each sim, build its per-level work units (vanilla-universe .sub via build_worker, OR a sub-DAG via subdag_factory) and assemble a wrapper DAG with PARENT/CHILD edges between the levels of each sim. Behavior of the dispatch step depends on self.submit_mode:
“submit” (default): invoke condor_submit_dag and return [(sim, cluster_id), …].
“embed”: write the wrapper DAG and return [(sim, “”), …] without dispatching. The wrapper DAG path is recorded on self.last_wrapper_dag_path so a parent workflow can pick it up via SUBDAG EXTERNAL <id> <wrapper_path>.
When self.subdag_factory is set, each per-(sim, level) node in the wrapper DAG is SUBDAG EXTERNAL rather than JOB. This lets backends whose work unit is itself a DAG (e.g. GW PE via util_RIFT_pseudo_pipe) compose cleanly.
- property when_to_transfer_output: str
- class RIFT.simulation_manager.database.Index(base: str | Path)
Bases:
objectOne JSON object per line, one line per sim. The canonical, cheap view of the archive. Status updates and registrations rewrite the file under a simple file lock (not implemented yet — single-writer assumption is documented for now).
- FILENAME = 'index.jsonl'
- all() List[Dict[str, Any]]
- by_name(name: str) Dict[str, Any] | None
- property path: Path
- remove(name: str) None
- upsert(row: Dict[str, Any]) None
- with_status(status: str) List[Dict[str, Any]]
- class RIFT.simulation_manager.database.LocalRequestQueue(run_queue: LocalRunQueue)
Bases:
RequestQueueTrivial pass-through: every sim that needs work (‘ready’ or ‘refine_ready’) is immediately handed to the run queue.
- kind: str = 'local'
- class RIFT.simulation_manager.database.LocalRunQueue
Bases:
RunQueueRuns the frozen generator inline in the current process. Computes every missing level (current_level + 1 … target_level) for each submitted sim. Useful for end-to-end tests without a cluster.
- kind: str = 'local'
- class RIFT.simulation_manager.database.Manifest(data: Dict[str, Any])
Bases:
objectarchive-level metadata stored at <base>/manifest.json.
- FILENAME = 'manifest.json'
- classmethod new(name: str, request_queue_kind: str, run_queue_kind: str, generator_entrypoint: str = 'generator:run', summarizer_entrypoint: str | None = None, same_q_entrypoint: str | None = None, lookup_key_entrypoint: str | None = None, params_schema: Dict[str, Any] | None = None, summary_schema: Dict[str, Any] | None = None, rift_version: str | None = None, request_queue_extra: Dict[str, Any] | None = None, run_queue_extra: Dict[str, Any] | None = None) Manifest
- write(base: str | Path) None
- class RIFT.simulation_manager.database.RequestQueue
Bases:
objectDecides which sims should next be sent to the run queue, and tracks their state in the request system. Subclasses set kind.
- kind: str = 'abstract'
- class RIFT.simulation_manager.database.RunQueue
Bases:
objectActually runs sims (writes their output).
- kind: str = 'abstract'
- class RIFT.simulation_manager.database.SlurmRequestQueue(run_queue: SlurmRunQueue | None = None, **submit_kwargs: Any)
Bases:
RequestQueueSlurm-side orchestrator. Identifies pending sims and delegates submission to SlurmRunQueue.
- kind: str = 'slurm'
- class RIFT.simulation_manager.database.SlurmRunQueue(partition: str | None = None, time_limit: str = '01:00:00', nodes: int = 1, ntasks: int = 1, cpus_per_task: int = 1, request_memory: int = 4096, request_disk: str | None = None, account: str | None = None, qos: str | None = None, partition_extra: Dict[str, str] | None = None, sbatch_path: str = 'sbatch', squeue_path: str = 'squeue', sacct_path: str = 'sacct', python_executable: str = 'python3', prelude: str = '', **submit_kwargs: Any)
Bases:
RunQueuePer-(sim, level) worker is one sbatch script. Levels for the same sim are chained via –dependency=afterok:<previous-jobid>.
- Configuration:
partition : str – required: SLURM partition / queue time_limit : str – e.g. ‘02:00:00’ (default ‘01:00:00’) nodes : int – default 1 ntasks : int – default 1 cpus_per_task : int – default 1 request_memory : int (MB), default 4096 request_disk : str – ignored by slurm directly; informational account : str – defaults to env SLURM_ACCOUNT qos : str – defaults to env SLURM_QOS partition_extra : dict – additional #SBATCH –key=value lines
emitted verbatim
sbatch_path : str – default ‘sbatch’ squeue_path : str – default ‘squeue’ sacct_path : str – default ‘sacct’ python_executable : str – default ‘python3’; what to invoke the
bootstrap with
- preludestr – shell snippet inserted before the
bootstrap (module load python, source /path/to/env/bin/activate, singularity exec … python3 …)
Compared to DualCondorRunQueue, this queue does NOT bake in an auto-OOM-release mechanism. Slurm’s standard tool for that is the job’s –requeue policy combined with operator-driven scontrol update jobid=… ReqMem=…; users who want automated retry should use Archive.unstick(name, bump_memory=True) and re-submit, or wrap the sbatch in a re-submit shell harness.
- build_worker(archive: Archive, sim_name: str, level: int = 1) str
Write the per-(sim, level) sbatch shell script and return its absolute path. Idempotent.
- kind: str = 'slurm'
- poll(archive: Archive, sim_names: Iterable[str]) Dict[str, str]
Use squeue to find jobs still in the queue, then sacct to confirm completion/failure for ones that have left. Updates archive statuses; output-on-disk is the authoritative completion signal (applied by the request queue’s poll via Archive.refresh_status_from_disk).
- class RIFT.simulation_manager.database.StatusRecord(data: Dict[str, Any])
Bases:
objectsims/<name>/status.json. The full per-sim story; the index is a projection of this. Mutations go through transition() so the history log and the FSM stay consistent.
- FILENAME = 'status.json'
- append_level(level: int, output_path: str) None
Record a successful level computation. Updates current_level and the levels[] list. Caller should subsequently transition to ‘complete’ or ‘refine_ready’ as appropriate.
- bump_target(target_level: int) bool
Raise target_level if target_level is higher than the current target. Returns True iff a bump occurred.
- needs_more_work() bool
- classmethod new(name: str, params: Any, target_level: int = 1) StatusRecord
- classmethod read(sim_dir: str | Path) StatusRecord
- transition(new_status: str, **fields: Any) None
- write(sim_dir: str | Path) None
- RIFT.simulation_manager.database.WHEN_TO_TRANSFER_OUTPUT = ('ON_EXIT', 'ON_EXIT_OR_EVICT', 'ON_SUCCESS')
HTCondor’s when_to_transfer_output vocabulary, as the JDL defines it. Three values, not four: NEVER is NOT one of them, and condor does not say so – measured against condor 25.13.1, NEVER submits with rc=0 and materialises as WhenToTransferOutput=”ON_EXIT”, silently. A caller setting it to suppress transfer gets transfer. BANANA is rejected loudly, so listing NEVER blessed the one value condor discards while catching nothing condor would not have caught.
- RIFT.simulation_manager.database.freeze_code(spec: Callable[[...], Any] | str | PathLike | Dict[str, Any], code_dir: str | Path, target_filename: str = 'generator.py') str
Snapshot the generator (or summarizer) source into <code_dir>/.
Returns the entrypoint string (“module:callable”) that should be stored in the manifest. Three input shapes are accepted:
callable: inspect.getsource() is captured into a single-file module named after target_filename. The function must be self-contained (no closure captures, no module-relative imports beyond the standard library and explicitly-listed deps).
path: a .py file is copied verbatim.
- dict: {“module_path”: …, “entrypoint”: “mod:fn”,
“extra_files”: […]}. All listed files are copied; the
named module becomes the canonical generator.
- RIFT.simulation_manager.database.load_entrypoint(code_dir: str | Path, entrypoint: str) Callable[[...], Any]
Resolve a ‘module:callable’ entrypoint against <code_dir>/. Used by workers (and by LocalRunQueue for the no-schedd path).
- RIFT.simulation_manager.database.make_queues_from_manifest(archive: Archive) Tuple[RequestQueue, RunQueue]
Instantiate (request_queue, run_queue) from the archive’s manifest. The run queue’s extra dict is passed as keyword arguments; the request queue gets the run_queue plumbed in.
Used by the request_sim CLI’s –ensure mode so the user doesn’t have to wire queues by hand.