Architecture and typed algorithm API¶
Core owns profile interfaces, typed requests, artifact references, serialization and execution. Algorithm repositories own concrete algorithms, configurations, payloads and scientific compatibility. The contract remains normative. These diagrams describe implemented classes. This source is also packaged in the wheel and rendered by the template documentation.
Profile hierarchy¶
Every profile requires preprocessing; only pretrained profiles require pretraining. A family can inherit several profile ABCs when shared methods have the same semantics, or register separate implementation classes that share internal components.
classDiagram
class Algorithm {
<<abstract>>
name
config_type
config
preprocess_data(request)*
validate_inputs(request)
}
class SupervisedAlgorithm {
<<abstract>>
train_supervised(request)*
predict_supervised(request)*
}
class PretrainedAlgorithm {
<<abstract>>
pretrain(request)*
}
class FinetuneAlgorithm {
<<abstract>>
train_finetune(request)*
predict_finetune(request)*
}
class ProbeAlgorithm {
<<abstract>>
infer_embeddings(request)*
train_probe(request)*
predict_probe(request)*
}
class ZeroShotDirectAlgorithm {
<<abstract>>
predict_zero_shot_direct(request)*
}
class ZeroShotMaterializedAlgorithm {
<<abstract>>
infer_trajectories(request)*
predict_zero_shot_materialized(request)*
}
class PredictOnlyAlgorithm {
<<abstract>>
predict_only(request)*
}
Algorithm <|-- SupervisedAlgorithm
Algorithm <|-- PretrainedAlgorithm
Algorithm <|-- PredictOnlyAlgorithm
PretrainedAlgorithm <|-- FinetuneAlgorithm
PretrainedAlgorithm <|-- ProbeAlgorithm
PretrainedAlgorithm <|-- ZeroShotDirectAlgorithm
PretrainedAlgorithm <|-- ZeroShotMaterializedAlgorithm
Each profile class declares its profile and ordered steps; method annotations identify request
types. Request fields define CLI arguments, while each request declares its output argument and
result artifact class. There is no independent step-definition registry. ALGORITHM_PROFILES and
STEP_SPECS are read-only views derived from these classes. StepSpec.from_request_type constructs
a specification for manifest validation and command inspection without maintaining a second definition.
Algorithm configuration¶
Configuration answers how this algorithm behaves. A request answers what one invocation reads and writes. An artifact reference identifies persisted data or fitted state. These are separate objects; sharing serialization does not make them one hierarchy.
The algorithm instance owns its configuration. Hyperparameters belong here, not in step requests.
ExecutionOptions belongs to each request and holds the invocation's seed and overwrite policy.
classDiagram
class Algorithm {
<<abstract>>
config: AlgorithmConfig
}
class AlgorithmConfig {
<<frozen dataclass>>
additional_predicates_file: Path optional
validate()
from_dict(values)
from_file(path)
save(path)
load(path)
}
class AlgorithmOwnedConfig {
<<frozen dataclass>>
algorithm parameters and defaults
}
class NNConfig {
<<frozen dataclass>>
capacity: float
learning_rate: float
batch_size: int
weight_decay: float
dropout: float
}
class AlgorithmOwnedNNConfig {
<<frozen dataclass>>
architecture-specific defaults and settings
}
Algorithm --> AlgorithmConfig : config
AlgorithmConfig <|-- AlgorithmOwnedConfig
AlgorithmConfig <|-- NNConfig
NNConfig <|-- AlgorithmOwnedNNConfig
Define configuration in the algorithm repository:
from dataclasses import dataclass
from meds_algorithm import AlgorithmConfig
@dataclass(frozen=True)
class Config(AlgorithmConfig):
C: float = 1.0
max_iterations: int = 1000
def validate(self):
if self.C <= 0 or self.max_iterations < 1:
raise ValueError("C and max_iterations must be positive")
Core validates field types, rejects unknown keys and nonfinite floats, and resolves defaults. Supported
values are strings, booleans, integers, finite floats, paths, enums, tuples, optional values and nested
frozen dataclasses. Mutable configuration fields and arbitrary Python objects are unsupported. JSON
arrays can initialize typed tuples. Override validate, not __post_init__, for algorithm constraints.
Use config_file=/path/config.yaml on the CLI for a JSON/YAML values mapping. Relative paths in that
file resolve against its parent. Config.save writes a versioned JSON envelope with an explicit class
identifier and all resolved values. Config.load checks the requested class and schema version; it
never imports code named by a file. Increment the algorithm's schema_version when serialized meaning
changes; unsupported versions fail without guessing migrations. Requests use the same serialization.
Constructed references use absolute paths; moving artifacts requires explicitly updating locations.
additional_predicates_file is an optional keyword-only configuration field inherited by concrete
configs. Preprocessing alone composes it with dataset predicates, rejects duplicate names/keys, and
supplies a resolved request.predicates_file to the hook and method. The runner preserves that file
inside prepared data with source/resolved digests; its contents are reserved. Later stages do not read
the additions, and TaskSpec continues to use the original task binding. See contract section 2.3.
Neural-network configuration and scaling experiments¶
AlgorithmConfig is the common parent. NNConfig is an optional, framework-independent child for
neural algorithms whose settings have the semantics below. It provides validation and serialization,
not a neural network, optimizer or training loop. Classical algorithms continue to extend
AlgorithmConfig directly.
| Setting | Meaning and validation |
|---|---|
capacity |
Positive finite multiplier relative to the algorithm's reference architecture; defaults to 1.0 |
learning_rate |
Positive finite base optimizer learning rate, before scheduling |
batch_size |
Positive integer examples per device per microbatch, before gradient accumulation |
weight_decay |
Nonnegative finite weight-decay coefficient; defaults to zero |
dropout |
Finite probability of dropping a unit, in [0, 1); defaults to zero |
Core deliberately supplies no default learning rate or batch size. A concrete algorithm defines its
reference architecture and multiplier-to-dimensions mapping, supplies source-defined defaults, chooses
its optimizer and scheduler, and specifies which parameter
groups receive weight decay and where dropout is applied. Do not treat weight decay as an interchangeable
L2 loss penalty or dropout as a keep probability. If an architecture has several widths or dropout rates,
declare their meaning and additional typed fields locally; do not silently ignore inherited settings.
For a neural algorithm without meaningful capacity scaling or microbatch semantics, use AlgorithmConfig
with explicit local fields instead of inventing a mapping just to satisfy this optional interface.
For example, a transformer can scale its hidden dimension while keeping depth fixed. This illustrative
policy rounds down to a multiple of attention heads, with at least one dimension per head. The reference
width must already be divisible by the head count, so capacity=1.0 preserves the reference architecture.
Other algorithms may scale depth, several dimensions, or target parameter counts; document and test the
exact mapping and rounding. capacity=2.0 does not universally mean twice the parameters or FLOPs.
from dataclasses import dataclass, replace
from meds_algorithm import NNConfig
@dataclass(frozen=True)
class TransformerConfig(NNConfig):
learning_rate: float = 0.001
batch_size: int = 32
weight_decay: float = 0.01
dropout: float = 0.1
reference_width: int = 128
depth: int = 4
heads: int = 4
@property
def hidden_size(self) -> int:
return max(1, int(self.reference_width * self.capacity / self.heads)) * self.heads
def validate(self):
super().validate()
if self.heads < 1 or self.reference_width < 1 or self.reference_width % self.heads:
raise ValueError("reference_width must be positive and divisible by positive heads")
if self.depth < 1:
raise ValueError("depth must be positive")
config = TransformerConfig()
larger = replace(config, capacity=2.0)
assert larger.hidden_size == 256
larger.save("config.json")
assert TransformerConfig.load("config.json") == larger
Override validate() with super().validate() when adding constraints to NNConfig.
Set the implementation's config_type = TransformerConfig and export the named config from its package.
The existing runner records every resolved field in config.json and the manifest. YAML uses the same
names; lr and bs aliases are intentionally not accepted:
capacity: 2.0
reference_width: 128
depth: 4
heads: 4
learning_rate: 0.001
batch_size: 32
weight_decay: 0.01
dropout: 0.1
The capacity multiplier is an experimental input, not a comparable measured parameter count. For scaling-law
analysis, an implementation should separately report total and trainable parameters of the instantiated
network, resolved architecture dimensions, actual optimizer steps, consumed examples/events/tokens (with
units), and effective batch size. Record resolved dimensions in an algorithm-owned payload/report as well
as retaining the multiplier, reference settings and implementation revision in existing core provenance.
For fixed-size full microbatches, effective batch size is batch_size × gradient_accumulation_steps ×
world_size; uneven batches or dynamic token batching require actual measurements. Any accumulation,
distributed or token-budget settings belong in the concrete config. Core does not infer these quantities,
instantiate optimizers or enforce that an implementation uses a field; algorithm tests must verify the
mapping. There is no automatic scaling-law experiment runner or standardized scaling-results artifact yet.
Data scaling belongs to experiment orchestration¶
Recommended ownership is upstream: MEDS-DEV or another experiment runner selects the training cohort
and materializes or references an explicit subset before calling algorithms. This is an integration
recommendation, not a claim that MEDS-DEV already implements that sampler. No data_fraction field or
hidden sampler is added to NNConfig.
- Fix the split assignment first. Select training subjects, retaining their valid history, rather than sampling arbitrary event rows. Keep tuning and evaluation keys fixed across a data-scaling sweep.
- Use deterministic nested subsets with a recorded selection seed and explicit membership digest/list. Reuse the same subject membership across algorithms; a fraction and seed alone are not an identity.
- Record dataset revision, eligible and selected subjects/events, selection rule and temporal cutoffs upstream. Scope learned preprocessing and label bundles consistently to the selected training cohort. For pretraining versus fine-tuning, declare separately which population is being scaled.
- Let the algorithm report what was actually consumed after filtering/tokenization, plus repetitions and training budget. Dataset size, unique token count and total tokens processed are different quantities; choose whether a sweep fixes steps, epochs or compute and record that decision.
These cohort-selection and consumption-reporting integrations remain future work. The implemented shared interface covers configuration and its existing persisted provenance; it does not perform data selection or count network parameters. Avoid adding another dataset-selection configuration until its upstream owner and identity format are agreed.
Step requests: typed method parameters¶
A request is an immutable parameter object for one method call. Its concrete type defines the
required inputs, optional inputs and output destination. The children represent different operations;
a preprocessing request does not turn into a training request. Every method has a concrete annotation,
for example predict_supervised(request: PredictSupervisedRequest) -> PayloadMetadata.
StepRequest supplies these common fields and methods to all 13 concrete requests:
classDiagram
class StepRequest {
<<frozen dataclass>>
output: ArtifactDestination
execution: ExecutionOptions
predicates_file: Path optional
from_arguments(arguments)
artifact_inputs()
save(path)
load(path)
}
class ArtifactDestination {
directory: Path
}
class ExecutionOptions {
seed: int = 0
overwrite: bool = false
}
StepRequest --> ArtifactDestination : output
StepRequest --> ExecutionOptions : execution
Callers instantiate concrete requests. output_argument and result_type are class metadata used to
map CLI arguments and construct the resulting artifact reference; they are not caller-supplied fields.
The following diagrams show every concrete request, split for readability. Each inherits the common
fields above. Relationships are inheritance (hollow arrow) or references to other objects (plain arrow).
Preprocessing and training requests¶
classDiagram
StepRequest <|-- PreprocessDataRequest
StepRequest <|-- PretrainRequest
StepRequest <|-- TrainSupervisedRequest
StepRequest <|-- TrainFinetuneRequest
StepRequest <|-- TrainProbeRequest
class PreprocessDataRequest {
meds_dir: Path
result_type: PreparedDataArtifact
}
class PretrainRequest {
data: PreparedDataArtifact
result_type: PretrainedAlgorithmArtifact
}
class TrainSupervisedRequest {
data: PreparedDataArtifact
labels: TrainingLabels
result_type: SupervisedAlgorithmArtifact
}
class TrainFinetuneRequest {
data: PreparedDataArtifact
pretrained: PretrainedAlgorithmArtifact
labels: TrainingLabels
result_type: FinetunedAlgorithmArtifact
}
class TrainProbeRequest {
embeddings: EmbeddingsArtifact
labels: TrainingLabels
result_type: ProbeAlgorithmArtifact
}
Embedding inference and fitted prediction requests¶
classDiagram
StepRequest <|-- InferEmbeddingsRequest
StepRequest <|-- PredictSupervisedRequest
StepRequest <|-- PredictFinetuneRequest
StepRequest <|-- PredictProbeRequest
class InferEmbeddingsRequest {
data: PreparedDataArtifact
pretrained: PretrainedAlgorithmArtifact
index: PredictionIndex
result_type: EmbeddingsArtifact
}
class PredictSupervisedRequest {
data: PreparedDataArtifact
fitted_algorithm: SupervisedAlgorithmArtifact
index: PredictionIndex
result_type: PredictionsArtifact
}
class PredictFinetuneRequest {
data: PreparedDataArtifact
fitted_algorithm: FinetunedAlgorithmArtifact
index: PredictionIndex
result_type: PredictionsArtifact
}
class PredictProbeRequest {
embeddings: EmbeddingsArtifact
fitted_algorithm: ProbeAlgorithmArtifact
index: PredictionIndex
result_type: PredictionsArtifact
}
fitted_algorithm references saved fitted state; it is not the Python algorithm instance. The CLI
names remain input_supervised_algorithm_dir, input_finetuned_algorithm_dir and
input_probe_algorithm_dir. These three requests use serialization schema version 2; version 1
request envelopes with the old algorithm field are rejected rather than silently reinterpreted.
Trajectory inference, zero-shot and predict-only requests¶
classDiagram
StepRequest <|-- InferTrajectoriesRequest
StepRequest <|-- PredictZeroShotDirectRequest
StepRequest <|-- PredictZeroShotMaterializedRequest
StepRequest <|-- PredictOnlyRequest
class InferTrajectoriesRequest {
data: PreparedDataArtifact
pretrained: PretrainedAlgorithmArtifact
index: PredictionIndex
result_type: TrajectoriesArtifact
}
class PredictZeroShotDirectRequest {
data: PreparedDataArtifact
pretrained: PretrainedAlgorithmArtifact
index: PredictionIndex
task: TaskSpec
result_type: PredictionsArtifact
}
class PredictZeroShotMaterializedRequest {
data: PreparedDataArtifact
trajectories: TrajectoriesArtifact
meds_dir: Path
index: PredictionIndex
task: TaskSpec
result_type: PredictionsArtifact
}
class PredictOnlyRequest {
data: PreparedDataArtifact
index: PredictionIndex
context: PredictionContext optional
task: TaskSpec optional
result_type: PredictionsArtifact
}
TrainingLabels.read accepts only train/tuning. PredictionIndex.read projects keys at the I/O boundary.
PredictionContext explicitly selects train/tuning and checks overlap with evaluated keys. Zero-shot
requests carry TaskSpec. These interfaces make permitted inputs clear; they are not a filesystem sandbox.
All requests inherit optional predicates_file; preprocessing uses feature predicates and zero-shot
prediction uses task predicates. Inference has no training-label field. Evaluation truth is a runner-only
argument and never enters a prediction request.
Constructing and executing a request¶
Given an implemented MyAlgorithm(SupervisedAlgorithm) and its MyAlgorithmConfig, a caller uses:
from pathlib import Path
from meds_algorithm import (
ArtifactDestination,
PreparedDataArtifact,
StepRunner,
SupervisedAlgorithmArtifact,
)
from meds_algorithm.requests import PredictionIndex, PredictSupervisedRequest
algorithm = MyAlgorithm(MyAlgorithmConfig()) # implementation and its hyperparameters
request = PredictSupervisedRequest(
data=PreparedDataArtifact(Path("prepared")),
fitted_algorithm=SupervisedAlgorithmArtifact(Path("trained")),
index=PredictionIndex(Path("index"), ("held_out",)),
output=ArtifactDestination(Path("predictions")),
)
predictions = StepRunner().run(
algorithm,
"predict_supervised",
request,
profile="supervised",
)
Required keyword fields, type validation and semantic checks make the method boundary explicit. Frozen
requests cannot be modified in place. The runner substitutes a private staging destination in a copy;
the algorithm writes to request.output.directory. Configuration remains on algorithm.config.
Both objects are persisted separately as config.json and request.json for reproducibility.
Artifact references and outputs¶
Artifacts are persisted results shared between independent invocations. References identify their directory and expected kind; they do not hold a loaded neural network or preprocessing payload.
classDiagram
class ArtifactReference {
<<frozen dataclass>>
directory: Path
kind: ArtifactType
open(directory)
read_manifest()
}
ArtifactReference <|-- PreparedDataArtifact
ArtifactReference <|-- PretrainedAlgorithmArtifact
ArtifactReference <|-- SupervisedAlgorithmArtifact
ArtifactReference <|-- FinetunedAlgorithmArtifact
ArtifactReference <|-- ProbeAlgorithmArtifact
ArtifactReference <|-- EmbeddingsArtifact
ArtifactReference <|-- TrajectoriesArtifact
ArtifactReference <|-- PredictionsArtifact
class PayloadMetadata {
format: str
version: int = 1
}
PredictSupervisedRequest --> PreparedDataArtifact : data
PredictSupervisedRequest --> SupervisedAlgorithmArtifact : fitted_algorithm
PredictSupervisedRequest --> PredictionIndex : index
Constructors normalize paths without loading payloads. open/read_manifest validate the common
envelope and artifact type. The runner validates references before algorithm code. Payload readers
remain algorithm-owned; core imposes no tensor, tokenizer, batching or dataframe representation.
Methods return PayloadMetadata describing what they wrote. The runner returns the published typed
artifact reference; metadata and the reference are different objects.
Shared execution¶
AlgorithmRepository associates profile names with implementation classes, creates a fresh instance
for every dispatch, converts CLI arguments into a request, and delegates to StepRunner.run. Python
callers can invoke the runner directly.
sequenceDiagram
participant Caller as CLI or Python caller
participant Runner as StepRunner
participant Algorithm as Algorithm implementation
participant Publish as measured_artifact
Caller->>Runner: run(algorithm, step, request, profile)
Runner->>Runner: Validate types, paths, manifests, index and context
Runner->>Algorithm: validate_inputs(request) when no feature composition is needed
Runner->>Publish: Lock destination, create staging, start measurements
Publish-->>Runner: Staging directory
opt Preprocessing with additional predicates
Runner->>Runner: Save resolved feature predicates from validated source snapshots
Runner->>Algorithm: validate_inputs(request with resolved predicates)
end
Runner->>Algorithm: step(request with staging destination)
Algorithm-->>Runner: PayloadMetadata(format, version)
Runner->>Runner: Validate prediction keys/probabilities and zero-shot ledger
Runner->>Runner: Attach optional evaluation truth after scoring
Runner->>Runner: Save config.json and original request.json
Runner->>Publish: Successful context exit
Publish->>Publish: Write manifest and publish with rollback
Runner-->>Caller: Typed published artifact reference
Algorithm methods write payloads only into request.output.directory and return PayloadMetadata. They
must not write the reserved manifest.yaml, config.json or request.json, or publish final directories.
Directly calling algorithm methods bypasses runner validation/publication; production and test adapters
must use the runner. The runner uses low-level publication helpers internally.
Prediction payloads are Parquet shards with keys and probabilities, without ground truth. The runner
validates their combined key set and probabilities, then optionally attaches evaluation labels. That
label path is a runner-only argument, never part of the algorithm's prediction request. Install the data
extra for typed prediction execution. Zero-shot outputs include relaxations.yaml at their root.
Outputs must not overlap inputs. Failures before publication preserve the previous published artifact;
filesystem/crash limitations remain as described in the README. The measured interval includes algorithm
execution and postprocessing/publication preparation, not initial input validation. Algorithms apply
request.execution.seed to relevant RNGs and subprocesses; core does not seed frameworks implicitly.
The default Algorithm.validate_inputs requires fitted-algorithm inputs to record the same algorithm name
and resolved configuration. Extend it for vocabulary, shape, payload-format and scientific checks.
Override that default deliberately for a documented policy, such as compatible external pretrained
algorithms. Reopen persisted artifacts; prediction must not depend on training on the same object.
Each successful artifact includes resolved configuration and original request, implementation identity, explicit algorithm-checkout provenance, upstream manifest digests and a key-only index digest. Manifest digests identify envelopes, not every payload byte. Dataset revisions and algorithm-specific content identities remain algorithm/run responsibilities; serialization does not make external data immutable. Replaying evaluation additionally requires the runner-only label path recorded in the manifest.
Ownership and compatibility¶
| Location | Owns |
|---|---|
| Core | Profile ABCs, request/artifact types, serialization, runner, shared validation and conformance |
| Template | Algorithm/config stubs, CLI, profile YAML, test adapters and operational files |
| Algorithm repository | Concrete typed config, algorithms/components, payload formats, compatibility and scientific tests |
AlgorithmRepository is the only repository dispatcher. It constructs typed requests and delegates
to StepRunner; the former mapping-based command dispatcher has been removed. Fitted inputs must
record the complete resolved configuration; the default validator does not fill missing historical fields.
Contract version 4 uses algorithm-named package imports, CLI entry points, artifact types and arguments.
Existing repositories and incompatible serialized requests require an explicit migration.
Classes and functional helpers¶
Objects own meaningful responsibilities: algorithms own configuration and implementation methods,
requests own typed invocation parameters, references own artifact identity, and the runner owns execution.
StepSpec.from_request_type is a class factory because its purpose is to construct a specification.
Small stateless operations such as file hashing, encoding/decoding and conformance assertions remain
functions. staged_directory and measured_artifact are context managers: their local state exists only
between entry and exit, with cleanup and rollback on failure. A class wrapping these functions would
not improve their current lifecycle. _ProcessTreeMemorySampler is a class because it manages a
background sampler with start/stop state. The design does not require every function to become a method.