meds-algorithm 0.4.0a4__py3-none-any.whl

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 (46) hide show
  1. meds_algorithm/ARCHITECTURE.md +376 -0
  2. meds_algorithm/CONTRACT.md +567 -0
  3. meds_algorithm/__init__.py +94 -0
  4. meds_algorithm/algorithms.py +157 -0
  5. meds_algorithm/artifacts/__init__.py +1 -0
  6. meds_algorithm/artifacts/inputs.py +38 -0
  7. meds_algorithm/artifacts/manifest.py +142 -0
  8. meds_algorithm/artifacts/publication.py +68 -0
  9. meds_algorithm/artifacts/references.py +79 -0
  10. meds_algorithm/artifacts/results.py +76 -0
  11. meds_algorithm/configuration.py +64 -0
  12. meds_algorithm/conformance/__init__.py +1 -0
  13. meds_algorithm/conformance/assertions.py +62 -0
  14. meds_algorithm/conformance/end_to_end.py +150 -0
  15. meds_algorithm/conformance/meds_dev.py +127 -0
  16. meds_algorithm/conformance/reports.py +49 -0
  17. meds_algorithm/conformance/signal.py +141 -0
  18. meds_algorithm/conformance/types.py +29 -0
  19. meds_algorithm/contracts/__init__.py +1 -0
  20. meds_algorithm/contracts/commands.py +182 -0
  21. meds_algorithm/contracts/enums.py +28 -0
  22. meds_algorithm/data/__init__.py +1 -0
  23. meds_algorithm/data/index.py +56 -0
  24. meds_algorithm/data/labels.py +55 -0
  25. meds_algorithm/data/predicates.py +60 -0
  26. meds_algorithm/data/validation.py +25 -0
  27. meds_algorithm/evaluation/__init__.py +1 -0
  28. meds_algorithm/evaluation/predictions.py +56 -0
  29. meds_algorithm/integrations/__init__.py +1 -0
  30. meds_algorithm/integrations/meds_dev.py +51 -0
  31. meds_algorithm/requests.py +262 -0
  32. meds_algorithm/runner.py +232 -0
  33. meds_algorithm/runtime/__init__.py +1 -0
  34. meds_algorithm/runtime/process.py +50 -0
  35. meds_algorithm/runtime/provenance.py +31 -0
  36. meds_algorithm/runtime/resources.py +97 -0
  37. meds_algorithm/serialization.py +122 -0
  38. meds_algorithm/spec.py +23 -0
  39. meds_algorithm/tasks/__init__.py +1 -0
  40. meds_algorithm/tasks/spec.py +136 -0
  41. meds_algorithm/version.py +3 -0
  42. meds_algorithm-0.4.0a4.dist-info/METADATA +212 -0
  43. meds_algorithm-0.4.0a4.dist-info/RECORD +46 -0
  44. meds_algorithm-0.4.0a4.dist-info/WHEEL +4 -0
  45. meds_algorithm-0.4.0a4.dist-info/entry_points.txt +4 -0
  46. meds_algorithm-0.4.0a4.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,262 @@
1
+ """One typed request per operation. Field metadata defines the existing CLI path names."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import MISSING, dataclass, field, fields
6
+ from pathlib import Path
7
+ from typing import ClassVar, get_args, get_type_hints
8
+
9
+ from .artifacts.references import (
10
+ ArtifactDestination,
11
+ ArtifactReference,
12
+ EmbeddingsArtifact,
13
+ FinetunedAlgorithmArtifact,
14
+ PredictionsArtifact,
15
+ PreparedDataArtifact,
16
+ PretrainedAlgorithmArtifact,
17
+ ProbeAlgorithmArtifact,
18
+ SupervisedAlgorithmArtifact,
19
+ TrajectoriesArtifact,
20
+ )
21
+ from .configuration import ExecutionOptions
22
+ from .serialization import Serializable
23
+ from .tasks.spec import TaskSpec
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class PredictionIndex(Serializable):
28
+ directory: Path
29
+ splits: tuple[str, ...]
30
+
31
+ def validate(self):
32
+ if (
33
+ not self.splits
34
+ or len(set(self.splits)) != len(self.splits)
35
+ or any(not s.strip() for s in self.splits)
36
+ ):
37
+ raise ValueError("splits must be a nonempty tuple of unique names")
38
+
39
+ def read(self):
40
+ from .data.index import read_index
41
+
42
+ return read_index(self.directory, self.splits)
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class TrainingLabels(Serializable):
47
+ directory: Path
48
+
49
+ def read(self, split: str = "train"):
50
+ from .data.labels import read_targets
51
+
52
+ if split not in ("train", "tuning"):
53
+ raise ValueError("Training labels permit only train and tuning")
54
+ return read_targets(self.directory, (split,))
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class PredictionContext(Serializable):
59
+ directory: Path
60
+ splits: tuple[str, ...]
61
+
62
+ def validate(self):
63
+ if (
64
+ not self.splits
65
+ or len(set(self.splits)) != len(self.splits)
66
+ or set(self.splits) - {"train", "tuning"}
67
+ ):
68
+ raise ValueError("Context splits must explicitly select train and/or tuning")
69
+
70
+ def read(self, index: PredictionIndex):
71
+ from .data.labels import read_context_labels
72
+
73
+ return read_context_labels(
74
+ self.directory, self.splits, index.splits, evaluation_index_dir=index.directory
75
+ )
76
+
77
+
78
+ @dataclass(frozen=True, kw_only=True)
79
+ class StepRequest(Serializable):
80
+ output: ArtifactDestination
81
+ execution: ExecutionOptions = field(default_factory=ExecutionOptions)
82
+ predicates_file: Path | None = field(default=None, metadata={"argument": "external_predicates_file"})
83
+ output_argument: ClassVar[str]
84
+ result_type: ClassVar[type[ArtifactReference]]
85
+
86
+ @classmethod
87
+ def argument_fields(cls):
88
+ hints = get_type_hints(cls)
89
+ return [(f, hints[f.name]) for f in fields(cls)]
90
+
91
+ @classmethod
92
+ def from_arguments(cls, arguments: dict):
93
+ values = dict(arguments)
94
+ output = values.pop(cls.output_argument, None)
95
+ if output is None:
96
+ raise ValueError(f"requires {cls.output_argument}=<path>")
97
+ result = {
98
+ "output": {"directory": output},
99
+ "execution": {"overwrite": values.pop("do_overwrite", False), "seed": values.pop("seed", 0)},
100
+ }
101
+ for f, annotation in cls.argument_fields():
102
+ if f.name in ("output", "execution"):
103
+ continue
104
+ kind = next((t for t in get_args(annotation) if t is not type(None)), annotation)
105
+ key = f.metadata.get("argument")
106
+ if key and key in values:
107
+ value = values.pop(key)
108
+ result[f.name] = (
109
+ {"directory": value}
110
+ if isinstance(kind, type) and issubclass(kind, (ArtifactReference, TrainingLabels))
111
+ else value
112
+ )
113
+ elif kind is PredictionIndex and "external_index_dir" in values:
114
+ result[f.name] = {
115
+ "directory": values.pop("external_index_dir"),
116
+ "splits": values.pop("splits", None),
117
+ }
118
+ elif kind is PredictionContext and "external_labels_dir" in values:
119
+ result[f.name] = {
120
+ "directory": values.pop("external_labels_dir"),
121
+ "splits": values.pop("context_splits", None),
122
+ }
123
+ elif kind is TaskSpec and "external_task_criteria_file" in values:
124
+ result[f.name] = {
125
+ "criteria_file": values.pop("external_task_criteria_file"),
126
+ "predicates_file": arguments.get("external_predicates_file"),
127
+ "name": values.pop("task_name", None),
128
+ }
129
+ if values:
130
+ raise ValueError(f"Unknown or forbidden arguments for {cls.__name__}: {sorted(values)}")
131
+ for f, _annotation in cls.argument_fields():
132
+ if f.default is MISSING and f.default_factory is MISSING and f.name not in result:
133
+ required = f.metadata.get(
134
+ "argument",
135
+ {
136
+ "index": "external_index_dir and splits",
137
+ "task": "external_task_criteria_file",
138
+ }.get(f.name, f.name),
139
+ )
140
+ raise ValueError(f"requires {required}")
141
+ return cls.from_dict(result)
142
+
143
+ def artifact_inputs(self):
144
+ for f in fields(self):
145
+ value = getattr(self, f.name)
146
+ if isinstance(value, ArtifactReference):
147
+ yield f.metadata["argument"], value
148
+
149
+
150
+ @dataclass(frozen=True, kw_only=True)
151
+ class PreprocessDataRequest(StepRequest):
152
+ meds_dir: Path = field(metadata={"argument": "external_meds_dir"})
153
+ output_argument = "output_data_dir"
154
+ result_type = PreparedDataArtifact
155
+
156
+
157
+ @dataclass(frozen=True, kw_only=True)
158
+ class PretrainRequest(StepRequest):
159
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
160
+ output_argument = "output_pretrained_algorithm_dir"
161
+ result_type = PretrainedAlgorithmArtifact
162
+
163
+
164
+ @dataclass(frozen=True, kw_only=True)
165
+ class TrainSupervisedRequest(StepRequest):
166
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
167
+ labels: TrainingLabels = field(metadata={"argument": "external_labels_dir"})
168
+ output_argument = "output_supervised_algorithm_dir"
169
+ result_type = SupervisedAlgorithmArtifact
170
+
171
+
172
+ @dataclass(frozen=True, kw_only=True)
173
+ class TrainFinetuneRequest(StepRequest):
174
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
175
+ pretrained: PretrainedAlgorithmArtifact = field(metadata={"argument": "input_pretrained_algorithm_dir"})
176
+ labels: TrainingLabels = field(metadata={"argument": "external_labels_dir"})
177
+ output_argument = "output_finetuned_algorithm_dir"
178
+ result_type = FinetunedAlgorithmArtifact
179
+
180
+
181
+ @dataclass(frozen=True, kw_only=True)
182
+ class InferEmbeddingsRequest(StepRequest):
183
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
184
+ pretrained: PretrainedAlgorithmArtifact = field(metadata={"argument": "input_pretrained_algorithm_dir"})
185
+ index: PredictionIndex
186
+ output_argument = "output_embeddings_dir"
187
+ result_type = EmbeddingsArtifact
188
+
189
+
190
+ @dataclass(frozen=True, kw_only=True)
191
+ class TrainProbeRequest(StepRequest):
192
+ embeddings: EmbeddingsArtifact = field(metadata={"argument": "input_embeddings_dir"})
193
+ labels: TrainingLabels = field(metadata={"argument": "external_labels_dir"})
194
+ output_argument = "output_probe_algorithm_dir"
195
+ result_type = ProbeAlgorithmArtifact
196
+
197
+
198
+ @dataclass(frozen=True, kw_only=True)
199
+ class PredictSupervisedRequest(StepRequest):
200
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
201
+ algorithm: SupervisedAlgorithmArtifact = field(metadata={"argument": "input_supervised_algorithm_dir"})
202
+ index: PredictionIndex
203
+ output_argument = "output_predictions_dir"
204
+ result_type = PredictionsArtifact
205
+
206
+
207
+ @dataclass(frozen=True, kw_only=True)
208
+ class PredictFinetuneRequest(StepRequest):
209
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
210
+ algorithm: FinetunedAlgorithmArtifact = field(metadata={"argument": "input_finetuned_algorithm_dir"})
211
+ index: PredictionIndex
212
+ output_argument = "output_predictions_dir"
213
+ result_type = PredictionsArtifact
214
+
215
+
216
+ @dataclass(frozen=True, kw_only=True)
217
+ class PredictProbeRequest(StepRequest):
218
+ embeddings: EmbeddingsArtifact = field(metadata={"argument": "input_embeddings_dir"})
219
+ algorithm: ProbeAlgorithmArtifact = field(metadata={"argument": "input_probe_algorithm_dir"})
220
+ index: PredictionIndex
221
+ output_argument = "output_predictions_dir"
222
+ result_type = PredictionsArtifact
223
+
224
+
225
+ @dataclass(frozen=True, kw_only=True)
226
+ class PredictZeroShotDirectRequest(StepRequest):
227
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
228
+ pretrained: PretrainedAlgorithmArtifact = field(metadata={"argument": "input_pretrained_algorithm_dir"})
229
+ index: PredictionIndex
230
+ task: TaskSpec
231
+ output_argument = "output_predictions_dir"
232
+ result_type = PredictionsArtifact
233
+
234
+
235
+ @dataclass(frozen=True, kw_only=True)
236
+ class InferTrajectoriesRequest(StepRequest):
237
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
238
+ pretrained: PretrainedAlgorithmArtifact = field(metadata={"argument": "input_pretrained_algorithm_dir"})
239
+ index: PredictionIndex
240
+ output_argument = "output_trajectories_dir"
241
+ result_type = TrajectoriesArtifact
242
+
243
+
244
+ @dataclass(frozen=True, kw_only=True)
245
+ class PredictZeroShotMaterializedRequest(StepRequest):
246
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
247
+ trajectories: TrajectoriesArtifact = field(metadata={"argument": "input_trajectories_dir"})
248
+ meds_dir: Path = field(metadata={"argument": "external_meds_dir"})
249
+ index: PredictionIndex
250
+ task: TaskSpec
251
+ output_argument = "output_predictions_dir"
252
+ result_type = PredictionsArtifact
253
+
254
+
255
+ @dataclass(frozen=True, kw_only=True)
256
+ class PredictOnlyRequest(StepRequest):
257
+ data: PreparedDataArtifact = field(metadata={"argument": "input_data_dir"})
258
+ index: PredictionIndex
259
+ context: PredictionContext | None = None
260
+ task: TaskSpec | None = None
261
+ output_argument = "output_predictions_dir"
262
+ result_type = PredictionsArtifact
@@ -0,0 +1,232 @@
1
+ """The single public execution path for typed algorithms and their CLI adapters."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import fields, is_dataclass, replace
6
+ from pathlib import Path
7
+
8
+ from .algorithms import Algorithm, profile_class, request_type
9
+ from .artifacts.manifest import digest_config, digest_file, measured_artifact
10
+ from .artifacts.references import ArtifactDestination, PayloadMetadata, PredictionsArtifact
11
+ from .data.predicates import RESOLVED_PREDICATES_FILENAME, compose_feature_predicates
12
+ from .requests import PredictionContext, PredictionIndex, PreprocessDataRequest, StepRequest
13
+ from .tasks.spec import RelaxationLedger
14
+
15
+
16
+ def _paths(value):
17
+ if isinstance(value, Path):
18
+ yield value
19
+ elif is_dataclass(value):
20
+ for field in fields(value):
21
+ yield from _paths(getattr(value, field.name))
22
+
23
+
24
+ class StepRunner:
25
+ """A fresh algorithm instance is supplied per call; persisted artifacts carry fitted state."""
26
+
27
+ def __init__(self, *, repo: Path | None = None):
28
+ self.repo = None if repo is None else Path(repo).resolve()
29
+
30
+ def run(
31
+ self,
32
+ algorithm: Algorithm,
33
+ step: str,
34
+ request: StepRequest,
35
+ *,
36
+ profile: str,
37
+ evaluation_labels_dir: Path | None = None,
38
+ ):
39
+ expected = request_type(profile, step)
40
+ if not isinstance(algorithm, profile_class(profile)):
41
+ raise TypeError(f"Algorithm does not implement {profile}")
42
+ if type(request) is not expected:
43
+ raise TypeError(f"{step} requires {expected.__name__}")
44
+ # Reconstruct to enforce types/value invariants even for hand-built Python requests.
45
+ request = expected.from_dict(request.to_dict())
46
+ algorithm.config_type.from_dict(algorithm.config.to_dict())
47
+ destination = request.output.directory
48
+ input_paths = [
49
+ path
50
+ for field in fields(request)
51
+ if field.name != "output"
52
+ for path in _paths(getattr(request, field.name))
53
+ ]
54
+ additional = (
55
+ algorithm.config.additional_predicates_file
56
+ if isinstance(request, PreprocessDataRequest)
57
+ else None
58
+ )
59
+ if additional is not None:
60
+ input_paths.append(additional)
61
+ if evaluation_labels_dir is not None:
62
+ if request.result_type is not PredictionsArtifact:
63
+ raise ValueError("Evaluation labels are allowed only after prediction")
64
+ evaluation_labels_dir = Path(evaluation_labels_dir).resolve()
65
+ input_paths.append(evaluation_labels_dir)
66
+ for path in input_paths:
67
+ if not path.exists():
68
+ raise FileNotFoundError(path)
69
+ if destination == path or destination in path.parents or path in destination.parents:
70
+ raise ValueError("Output must not overlap any input path")
71
+ inputs = {}
72
+ resolved_predicates = None
73
+ if additional is not None:
74
+ resolved_predicates, provenance = compose_feature_predicates(request.predicates_file, additional)
75
+ inputs["feature_predicates"] = provenance
76
+ if provenance["base"] is not None:
77
+ inputs["external_predicates_file"] = provenance["base"]
78
+ for argument, artifact in request.artifact_inputs():
79
+ artifact.read_manifest()
80
+ inputs[argument] = {
81
+ "path": str(artifact.directory),
82
+ "manifest_digest": digest_file(artifact.directory / "manifest.yaml"),
83
+ }
84
+ index = getattr(request, "index", None)
85
+ if isinstance(index, PredictionIndex):
86
+ keys = index.read()
87
+ inputs["external_index_dir"] = {
88
+ "path": str(index.directory),
89
+ "splits": list(index.splits),
90
+ "keys_digest": digest_config(keys.sort(["subject_id", "prediction_time"]).write_json()),
91
+ }
92
+ context = getattr(request, "context", None)
93
+ if isinstance(context, PredictionContext):
94
+ context.read(index) # Disjoint keys and permitted split names, before algorithm code.
95
+ task = getattr(request, "task", None)
96
+ if task is not None:
97
+ if task.predicates_file != request.predicates_file:
98
+ raise ValueError("Task predicates and request predicates must agree")
99
+ inputs["task"] = task.provenance()
100
+ if resolved_predicates is None:
101
+ algorithm.validate_inputs(request)
102
+ metadata = None
103
+ details = {
104
+ "algorithm": {
105
+ "name": algorithm.name,
106
+ "implementation": f"{type(algorithm).__module__}.{type(algorithm).__qualname__}",
107
+ "config_type": f"{type(algorithm.config).__module__}.{type(algorithm.config).__qualname__}",
108
+ "config_schema_version": algorithm.config.schema_version,
109
+ "config": algorithm.config.to_dict(),
110
+ },
111
+ "invocation": {
112
+ "profile": profile,
113
+ "request": request.to_dict(),
114
+ "evaluation_labels_dir": None
115
+ if evaluation_labels_dir is None
116
+ else str(evaluation_labels_dir),
117
+ },
118
+ }
119
+ with measured_artifact(
120
+ destination,
121
+ artifact_type=request.result_type.kind.value,
122
+ command=step,
123
+ producer=f"{type(algorithm).__module__}.{type(algorithm).__qualname__}.{step}",
124
+ payload_format=lambda: f"{metadata.format}/v{metadata.version}",
125
+ inputs=inputs,
126
+ config_digest=digest_config(
127
+ {
128
+ "config": algorithm.config.to_dict(),
129
+ "request": request.to_dict(),
130
+ "feature_predicates": inputs.get("feature_predicates"),
131
+ }
132
+ ),
133
+ semantics_digest=None if task is None else task.semantics_digest(),
134
+ external_predicates_file=request.predicates_file if resolved_predicates is None else None,
135
+ overwrite=request.execution.overwrite,
136
+ repo=self.repo,
137
+ manifest_fields=details,
138
+ ) as staging:
139
+ working = replace(request, output=ArtifactDestination(staging))
140
+ if resolved_predicates is not None:
141
+ effective_path = staging / RESOLVED_PREDICATES_FILENAME
142
+ effective_path.write_bytes(resolved_predicates)
143
+ working = replace(working, predicates_file=effective_path)
144
+ algorithm.validate_inputs(working)
145
+ metadata = getattr(algorithm, step)(working)
146
+ if resolved_predicates is not None and (
147
+ not effective_path.is_file() or effective_path.read_bytes() != resolved_predicates
148
+ ):
149
+ raise ValueError("Algorithm modified the runner-owned resolved feature predicates")
150
+ if algorithm.config.to_dict() != details["algorithm"]["config"]:
151
+ raise ValueError("Algorithm configuration changed during execution")
152
+ if not isinstance(metadata, PayloadMetadata):
153
+ raise TypeError("Algorithm methods must return PayloadMetadata")
154
+ if (staging / "manifest.yaml").exists():
155
+ raise ValueError("Only the runner may write manifest.yaml")
156
+ if request.result_type is PredictionsArtifact:
157
+ self._validate_predictions(staging, index, evaluation_labels_dir)
158
+ if profile.startswith("zero_shot_"):
159
+ RelaxationLedger.read(staging)
160
+ for reserved in ("config.json", "request.json"):
161
+ if (staging / reserved).exists():
162
+ raise ValueError(f"{reserved} is reserved for runner provenance")
163
+ algorithm.config.save(staging / "config.json")
164
+ request.save(staging / "request.json")
165
+ return request.result_type.open(destination)
166
+
167
+ @staticmethod
168
+ def _validate_predictions(staging, index, evaluation_labels_dir):
169
+ import polars as pl
170
+
171
+ from .evaluation.predictions import attach_ground_truth, check_decoded
172
+
173
+ paths = sorted(staging.rglob("*.parquet"))
174
+ if not paths:
175
+ raise ValueError("Predictions must contain Parquet shards")
176
+ frames = [pl.read_parquet(path) for path in paths]
177
+ if any("boolean_value" in frame.columns for frame in frames):
178
+ raise ValueError(
179
+ "Algorithm predictions must not contain ground truth; runner attaches it after scoring"
180
+ )
181
+ check_decoded(index.read(), pl.concat(frames))
182
+ if evaluation_labels_dir is not None:
183
+ for path, frame in zip(paths, frames, strict=True):
184
+ attach_ground_truth(frame, evaluation_labels_dir, index.splits).write_parquet(path)
185
+
186
+
187
+ class AlgorithmRepository:
188
+ """Only a profile-to-implementation association; signatures and order live on the ABCs."""
189
+
190
+ def __init__(self, implementations: dict[str, type[Algorithm]], *, repo: Path | None = None):
191
+ if not implementations:
192
+ raise ValueError("At least one profile implementation is required")
193
+ for profile, implementation in implementations.items():
194
+ if not isinstance(implementation, type) or not issubclass(implementation, profile_class(profile)):
195
+ raise TypeError(f"Implementation must inherit {profile_class(profile).__name__}")
196
+ if implementation.__abstractmethods__:
197
+ raise TypeError(
198
+ f"Unimplemented abstract methods: {sorted(implementation.__abstractmethods__)}"
199
+ )
200
+ self.implementations = dict(implementations)
201
+ self.profiles = tuple(implementations)
202
+ self.runner = StepRunner(repo=repo)
203
+
204
+ def select_profile(self, profile=None):
205
+ if profile is None:
206
+ if len(self.profiles) != 1:
207
+ raise ValueError("Several profiles are declared; select one with --profile")
208
+ return self.profiles[0]
209
+ if profile not in self.implementations:
210
+ raise ValueError(f"Profile {profile!r} is not declared")
211
+ return profile
212
+
213
+ def profile_commands(self, profile=None):
214
+ return profile_class(self.select_profile(profile)).steps
215
+
216
+ def run(self, step: str, arguments: dict, *, profile=None):
217
+ profile = self.select_profile(profile)
218
+ request_cls = request_type(profile, step)
219
+ arguments = dict(arguments)
220
+ implementation = self.implementations[profile]
221
+ config_file = arguments.pop("config_file", None)
222
+ config = (
223
+ implementation.config_type.from_file(config_file) if config_file else implementation.config_type()
224
+ )
225
+ evaluation = arguments.pop("external_evaluation_labels_dir", None)
226
+ return self.runner.run(
227
+ implementation(config),
228
+ step,
229
+ request_cls.from_arguments(arguments),
230
+ profile=profile,
231
+ evaluation_labels_dir=evaluation,
232
+ )
@@ -0,0 +1 @@
1
+ """Optional-platform resource sampling, explicit algorithm provenance and subprocess execution."""
@@ -0,0 +1,50 @@
1
+ import os
2
+ import signal
3
+ import subprocess
4
+ from contextlib import suppress
5
+
6
+ RUN_TIMEOUT = 5400
7
+
8
+
9
+ def run(cmd, *, timeout=RUN_TIMEOUT, env=None):
10
+ """Run in an owned process group and stop its descendants if interrupted or timed out.
11
+
12
+ Descendants must remain in the group; deliberately detached services are not supported.
13
+ Windows uses taskkill /T to terminate the child tree while the parent is still alive.
14
+ """
15
+ process = subprocess.Popen(
16
+ [str(value) for value in cmd],
17
+ stdout=subprocess.PIPE,
18
+ stderr=subprocess.PIPE,
19
+ text=True,
20
+ start_new_session=os.name == "posix",
21
+ env=None if env is None else {**os.environ, **env},
22
+ )
23
+ try:
24
+ stdout, stderr = process.communicate(timeout=timeout)
25
+ except BaseException:
26
+ try:
27
+ if os.name == "posix":
28
+ with suppress(ProcessLookupError):
29
+ os.killpg(process.pid, signal.SIGKILL)
30
+ else:
31
+ subprocess.run(
32
+ ["taskkill", "/PID", str(process.pid), "/T", "/F"],
33
+ check=True,
34
+ capture_output=True,
35
+ timeout=30,
36
+ )
37
+ finally:
38
+ if process.poll() is None:
39
+ process.kill()
40
+ process.wait()
41
+ process.stdout.close()
42
+ process.stderr.close()
43
+ raise
44
+ result = subprocess.CompletedProcess(process.args, process.returncode, stdout, stderr)
45
+ if result.returncode:
46
+ raise AssertionError(
47
+ f"command failed ({result.returncode}): {' '.join(map(str, cmd))}\n"
48
+ f"--- stdout ---\n{result.stdout}\n--- stderr ---\n{result.stderr}"
49
+ )
50
+ return result
@@ -0,0 +1,31 @@
1
+ """Record the caller's algorithm checkout; never infer it from the core package or cwd."""
2
+
3
+ import subprocess
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+
8
+ def code_provenance(repo: Path | str | None) -> dict[str, Any]:
9
+ unknown = {
10
+ "repository": None if repo is None else str(Path(repo).resolve()),
11
+ "git_commit": None,
12
+ "dirty": None,
13
+ }
14
+ if repo is None:
15
+ return unknown
16
+ root = Path(repo).resolve()
17
+ # Do not accidentally attribute an unpacked wheel to an enclosing unrelated checkout.
18
+ if not (root / ".git").exists():
19
+ return unknown
20
+
21
+ def git(*args):
22
+ return subprocess.run(
23
+ ["git", "-C", str(root), *args], check=True, capture_output=True, text=True
24
+ ).stdout.strip()
25
+
26
+ try:
27
+ commit = git("rev-parse", "HEAD")
28
+ status = git("status", "--porcelain")
29
+ except (OSError, subprocess.CalledProcessError):
30
+ return unknown
31
+ return {"repository": str(root), "git_commit": commit, "dirty": bool(status)}
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import threading
6
+ from pathlib import Path
7
+
8
+ try:
9
+ import resource
10
+ except ImportError:
11
+ resource = None
12
+
13
+
14
+ def _rss_bytes(value: int) -> int:
15
+ # Linux reports KiB; macOS reports bytes. sys.platform is deliberately avoided so unusual Unix
16
+ # implementations can override/report their method rather than receiving a false portable claim.
17
+ return value * 1024 if sys.platform != "darwin" else value
18
+
19
+
20
+ class _ProcessTreeMemorySampler:
21
+ """Sample aggregate PSS/RSS for this process and its live descendants on Linux."""
22
+
23
+ def __init__(self, interval_seconds: float = 0.25) -> None:
24
+ self.interval_seconds = interval_seconds
25
+ self.peak_pss_bytes: int | None = None
26
+ self.peak_rss_bytes: int | None = None
27
+ self.source = "linux_proc_smaps_rollup" if Path("/proc/self/smaps_rollup").is_file() else "getrusage"
28
+ self.includes_descendants = self.source == "linux_proc_smaps_rollup"
29
+ self._stop = threading.Event()
30
+ self._thread = threading.Thread(target=self._run, name="meds-memory-sampler", daemon=True)
31
+
32
+ def start(self) -> None:
33
+ self._sample()
34
+ self._thread.start()
35
+
36
+ def stop(self) -> None:
37
+ self._sample()
38
+ self._stop.set()
39
+ self._thread.join(timeout=max(1.0, self.interval_seconds * 4))
40
+ self._sample()
41
+ if self.peak_rss_bytes is None:
42
+ self.includes_descendants = False
43
+ if resource is not None:
44
+ self.source = "getrusage"
45
+ usage = resource.getrusage(resource.RUSAGE_SELF)
46
+ self.peak_rss_bytes = _rss_bytes(usage.ru_maxrss)
47
+ else:
48
+ self.source = "unavailable"
49
+
50
+ def _run(self) -> None:
51
+ while not self._stop.wait(self.interval_seconds):
52
+ self._sample()
53
+
54
+ @staticmethod
55
+ def _process_tree(root_pid: int) -> set[int]:
56
+ found: set[int] = set()
57
+ pending = [root_pid]
58
+ while pending:
59
+ pid = pending.pop()
60
+ if pid in found:
61
+ continue
62
+ found.add(pid)
63
+ try:
64
+ tasks = list(Path(f"/proc/{pid}/task").iterdir())
65
+ except OSError:
66
+ continue
67
+ for task in tasks:
68
+ try:
69
+ pending.extend(int(value) for value in (task / "children").read_text().split())
70
+ except (OSError, ValueError):
71
+ continue
72
+ return found
73
+
74
+ @staticmethod
75
+ def _smaps(pid: int) -> tuple[int, int] | None:
76
+ try:
77
+ values = {}
78
+ for line in Path(f"/proc/{pid}/smaps_rollup").read_text().splitlines():
79
+ key, separator, rest = line.partition(":")
80
+ if separator and key in {"Pss", "Rss"}:
81
+ values[key] = int(rest.split()[0]) * 1024
82
+ return values["Pss"], values["Rss"]
83
+ except (FileNotFoundError, KeyError, PermissionError, ProcessLookupError, ValueError):
84
+ return None
85
+
86
+ def _sample(self) -> None:
87
+ if self.source != "linux_proc_smaps_rollup":
88
+ return
89
+ samples = [sample for pid in self._process_tree(os.getpid()) if (sample := self._smaps(pid))]
90
+ if not samples:
91
+ return
92
+ if len(samples) != len(self._process_tree(os.getpid())):
93
+ self.includes_descendants = False
94
+ pss = sum(sample[0] for sample in samples)
95
+ rss = sum(sample[1] for sample in samples)
96
+ self.peak_pss_bytes = max(self.peak_pss_bytes or 0, pss)
97
+ self.peak_rss_bytes = max(self.peak_rss_bytes or 0, rss)