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.
- meds_algorithm/ARCHITECTURE.md +376 -0
- meds_algorithm/CONTRACT.md +567 -0
- meds_algorithm/__init__.py +94 -0
- meds_algorithm/algorithms.py +157 -0
- meds_algorithm/artifacts/__init__.py +1 -0
- meds_algorithm/artifacts/inputs.py +38 -0
- meds_algorithm/artifacts/manifest.py +142 -0
- meds_algorithm/artifacts/publication.py +68 -0
- meds_algorithm/artifacts/references.py +79 -0
- meds_algorithm/artifacts/results.py +76 -0
- meds_algorithm/configuration.py +64 -0
- meds_algorithm/conformance/__init__.py +1 -0
- meds_algorithm/conformance/assertions.py +62 -0
- meds_algorithm/conformance/end_to_end.py +150 -0
- meds_algorithm/conformance/meds_dev.py +127 -0
- meds_algorithm/conformance/reports.py +49 -0
- meds_algorithm/conformance/signal.py +141 -0
- meds_algorithm/conformance/types.py +29 -0
- meds_algorithm/contracts/__init__.py +1 -0
- meds_algorithm/contracts/commands.py +182 -0
- meds_algorithm/contracts/enums.py +28 -0
- meds_algorithm/data/__init__.py +1 -0
- meds_algorithm/data/index.py +56 -0
- meds_algorithm/data/labels.py +55 -0
- meds_algorithm/data/predicates.py +60 -0
- meds_algorithm/data/validation.py +25 -0
- meds_algorithm/evaluation/__init__.py +1 -0
- meds_algorithm/evaluation/predictions.py +56 -0
- meds_algorithm/integrations/__init__.py +1 -0
- meds_algorithm/integrations/meds_dev.py +51 -0
- meds_algorithm/requests.py +262 -0
- meds_algorithm/runner.py +232 -0
- meds_algorithm/runtime/__init__.py +1 -0
- meds_algorithm/runtime/process.py +50 -0
- meds_algorithm/runtime/provenance.py +31 -0
- meds_algorithm/runtime/resources.py +97 -0
- meds_algorithm/serialization.py +122 -0
- meds_algorithm/spec.py +23 -0
- meds_algorithm/tasks/__init__.py +1 -0
- meds_algorithm/tasks/spec.py +136 -0
- meds_algorithm/version.py +3 -0
- meds_algorithm-0.4.0a4.dist-info/METADATA +212 -0
- meds_algorithm-0.4.0a4.dist-info/RECORD +46 -0
- meds_algorithm-0.4.0a4.dist-info/WHEEL +4 -0
- meds_algorithm-0.4.0a4.dist-info/entry_points.txt +4 -0
- meds_algorithm-0.4.0a4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Strict, explicit-class serialization for immutable configuration and requests."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import math
|
|
7
|
+
import types
|
|
8
|
+
from dataclasses import fields, is_dataclass
|
|
9
|
+
from enum import Enum
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import ClassVar, Union, get_args, get_origin, get_type_hints
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def encode(value):
|
|
15
|
+
"""JSON values only; never turn unsupported Python objects into arbitrary strings."""
|
|
16
|
+
if is_dataclass(value) and not isinstance(value, type):
|
|
17
|
+
if not value.__dataclass_params__.frozen:
|
|
18
|
+
raise TypeError("Serializable dataclasses must be frozen")
|
|
19
|
+
return {field.name: encode(getattr(value, field.name)) for field in fields(value)}
|
|
20
|
+
if isinstance(value, Enum):
|
|
21
|
+
return value.value
|
|
22
|
+
if isinstance(value, Path):
|
|
23
|
+
return str(value)
|
|
24
|
+
if isinstance(value, tuple):
|
|
25
|
+
return [encode(item) for item in value]
|
|
26
|
+
if value is None or type(value) in (str, bool, int):
|
|
27
|
+
return value
|
|
28
|
+
if type(value) is float and math.isfinite(value):
|
|
29
|
+
return value
|
|
30
|
+
raise TypeError(f"Unsupported serialization value: {type(value).__name__}")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def decode(annotation, value, *, base: Path):
|
|
34
|
+
origin = get_origin(annotation)
|
|
35
|
+
args = get_args(annotation)
|
|
36
|
+
if origin in (Union, types.UnionType):
|
|
37
|
+
for option in args:
|
|
38
|
+
try:
|
|
39
|
+
return decode(option, value, base=base)
|
|
40
|
+
except (TypeError, ValueError):
|
|
41
|
+
pass
|
|
42
|
+
raise TypeError(f"Value does not match {annotation}")
|
|
43
|
+
if annotation is type(None) and value is None:
|
|
44
|
+
return None
|
|
45
|
+
if annotation is Path:
|
|
46
|
+
if not isinstance(value, (str, Path)) or not str(value).strip():
|
|
47
|
+
raise TypeError("Expected a nonempty path")
|
|
48
|
+
path = Path(value)
|
|
49
|
+
return (base / path).resolve() if not path.is_absolute() else path.resolve()
|
|
50
|
+
if origin is tuple:
|
|
51
|
+
if not isinstance(value, (tuple, list)):
|
|
52
|
+
raise TypeError("Expected an array")
|
|
53
|
+
if len(args) == 2 and args[1] is Ellipsis:
|
|
54
|
+
return tuple(decode(args[0], item, base=base) for item in value)
|
|
55
|
+
if len(args) != len(value):
|
|
56
|
+
raise TypeError("Wrong tuple length")
|
|
57
|
+
return tuple(decode(kind, item, base=base) for kind, item in zip(args, value, strict=True))
|
|
58
|
+
if isinstance(annotation, type) and issubclass(annotation, Enum):
|
|
59
|
+
return annotation(value)
|
|
60
|
+
if is_dataclass(annotation):
|
|
61
|
+
if isinstance(value, annotation):
|
|
62
|
+
value = encode(value)
|
|
63
|
+
if not isinstance(value, dict):
|
|
64
|
+
raise TypeError(f"Expected an object for {annotation.__name__}")
|
|
65
|
+
names = {field.name for field in fields(annotation)}
|
|
66
|
+
if unknown := value.keys() - names:
|
|
67
|
+
raise ValueError(f"Unknown fields for {annotation.__name__}: {sorted(unknown)}")
|
|
68
|
+
hints = get_type_hints(annotation)
|
|
69
|
+
return annotation(**{key: decode(hints[key], item, base=base) for key, item in value.items()})
|
|
70
|
+
if annotation is float and type(value) in (int, float) and math.isfinite(value):
|
|
71
|
+
return float(value)
|
|
72
|
+
if annotation in (str, int, bool) and type(value) is annotation:
|
|
73
|
+
return value
|
|
74
|
+
raise TypeError(f"Expected {annotation}, got {type(value).__name__}")
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Serializable:
|
|
78
|
+
"""Subclass with a frozen dataclass. Loading never imports a class named by a file."""
|
|
79
|
+
|
|
80
|
+
schema_version: ClassVar[int] = 1
|
|
81
|
+
|
|
82
|
+
def __post_init__(self):
|
|
83
|
+
hints = get_type_hints(type(self))
|
|
84
|
+
for field in fields(self):
|
|
85
|
+
object.__setattr__(
|
|
86
|
+
self, field.name, decode(hints[field.name], getattr(self, field.name), base=Path.cwd())
|
|
87
|
+
)
|
|
88
|
+
encode(self)
|
|
89
|
+
self.validate()
|
|
90
|
+
|
|
91
|
+
def validate(self) -> None:
|
|
92
|
+
"""Override to enforce algorithm-specific value constraints."""
|
|
93
|
+
|
|
94
|
+
def to_dict(self) -> dict:
|
|
95
|
+
return encode(self)
|
|
96
|
+
|
|
97
|
+
@classmethod
|
|
98
|
+
def from_dict(cls, values: dict, *, base: Path | None = None):
|
|
99
|
+
return decode(cls, values, base=Path.cwd() if base is None else Path(base).resolve())
|
|
100
|
+
|
|
101
|
+
def save(self, path: Path | str) -> Path:
|
|
102
|
+
path = Path(path)
|
|
103
|
+
envelope = {
|
|
104
|
+
"schema_version": self.schema_version,
|
|
105
|
+
"type": f"{type(self).__module__}.{type(self).__qualname__}",
|
|
106
|
+
"values": self.to_dict(),
|
|
107
|
+
}
|
|
108
|
+
path.write_text(json.dumps(envelope, sort_keys=True, indent=2, allow_nan=False), encoding="utf-8")
|
|
109
|
+
return path
|
|
110
|
+
|
|
111
|
+
@classmethod
|
|
112
|
+
def load(cls, path: Path | str):
|
|
113
|
+
path = Path(path).resolve()
|
|
114
|
+
envelope = json.loads(path.read_text(encoding="utf-8"))
|
|
115
|
+
if not isinstance(envelope, dict) or set(envelope) != {"schema_version", "type", "values"}:
|
|
116
|
+
raise ValueError("Invalid serialized envelope")
|
|
117
|
+
if (
|
|
118
|
+
envelope["schema_version"] != cls.schema_version
|
|
119
|
+
or envelope["type"] != f"{cls.__module__}.{cls.__qualname__}"
|
|
120
|
+
):
|
|
121
|
+
raise ValueError("Serialized type or schema version mismatch")
|
|
122
|
+
return cls.from_dict(envelope["values"], base=path.parent)
|
meds_algorithm/spec.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Read the one normative specification shipped with the installed core version."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def specification() -> str:
|
|
8
|
+
return files("meds_algorithm").joinpath("CONTRACT.md").read_text(encoding="utf-8")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def architecture() -> str:
|
|
12
|
+
"""Read the bundled API/UML guide maintained as ARCHITECTURE.md in the repository."""
|
|
13
|
+
return files("meds_algorithm").joinpath("ARCHITECTURE.md").read_text(encoding="utf-8")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
18
|
+
parser.parse_args()
|
|
19
|
+
print(specification())
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
if __name__ == "__main__":
|
|
23
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Task identity and declared semantic relaxations."""
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""The zero-shot task boundary: task specification, label-blind index, and relaxation ledger.
|
|
2
|
+
|
|
3
|
+
A supervised algorithm learns *what* to predict from labels. A zero-shot algorithm is never shown labels,
|
|
4
|
+
so the task definition itself must cross the contract boundary. The task language is ACES: a criteria
|
|
5
|
+
file plus, optionally, a dataset predicates file.
|
|
6
|
+
|
|
7
|
+
The contract fixes only the inputs (task, index, MEDS data), the final predictions, the semantics digest
|
|
8
|
+
that ties every artifact to the exact task it answers, and a minimal ledger of how the answered task
|
|
9
|
+
differs from the requested one. How a algorithm turns the task into predictions is its own; the
|
|
10
|
+
implementation guide recommends an encode -> infer -> decode shape.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
from collections.abc import Mapping, Sequence
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
INDEX_COLUMNS = ("subject_id", "prediction_time")
|
|
24
|
+
RELAXATIONS_FILENAME = "relaxations.yaml"
|
|
25
|
+
PREDICTED_PROBABILITY = "predicted_boolean_probability"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _digest_file(path: Path) -> str:
|
|
29
|
+
return f"sha256:{hashlib.sha256(path.read_bytes()).hexdigest()}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class TaskSpec:
|
|
34
|
+
"""A task in the overall task language: an ACES criteria file and its optional predicates file."""
|
|
35
|
+
|
|
36
|
+
criteria_file: Path
|
|
37
|
+
predicates_file: Path | None = None
|
|
38
|
+
name: str | None = None
|
|
39
|
+
|
|
40
|
+
def __post_init__(self) -> None:
|
|
41
|
+
object.__setattr__(self, "criteria_file", Path(self.criteria_file))
|
|
42
|
+
if self.predicates_file is not None:
|
|
43
|
+
object.__setattr__(self, "predicates_file", Path(self.predicates_file))
|
|
44
|
+
for path in (self.criteria_file, self.predicates_file):
|
|
45
|
+
if path is not None and not path.is_file():
|
|
46
|
+
raise FileNotFoundError(f"Task file {path} does not exist")
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def from_arguments(cls, arguments: Mapping[str, Any]) -> TaskSpec:
|
|
50
|
+
"""Read the standard ``external_task_criteria_file`` / ``external_predicates_file`` arguments."""
|
|
51
|
+
criteria = arguments.get("external_task_criteria_file")
|
|
52
|
+
if not criteria:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
"zero-shot commands require external_task_criteria_file=<ACES task YAML>; without it the "
|
|
55
|
+
"algorithm cannot know what to predict"
|
|
56
|
+
)
|
|
57
|
+
return cls(
|
|
58
|
+
criteria_file=criteria,
|
|
59
|
+
predicates_file=arguments.get("external_predicates_file") or None,
|
|
60
|
+
name=arguments.get("task_name") or None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def semantics_digest(self) -> str:
|
|
64
|
+
"""A digest of exactly what was asked: the criteria bytes and the predicates bytes.
|
|
65
|
+
|
|
66
|
+
Artifacts may be combined only when their semantics digests agree. The digest changes when either
|
|
67
|
+
file changes, including when the same criteria are bound to another dataset's predicates.
|
|
68
|
+
"""
|
|
69
|
+
digest = hashlib.sha256()
|
|
70
|
+
for label, path in (("criteria", self.criteria_file), ("predicates", self.predicates_file)):
|
|
71
|
+
digest.update(label.encode())
|
|
72
|
+
digest.update(b"\0" if path is None else hashlib.sha256(path.read_bytes()).digest())
|
|
73
|
+
return f"sha256:{digest.hexdigest()}"
|
|
74
|
+
|
|
75
|
+
def provenance(self) -> dict[str, Any]:
|
|
76
|
+
"""The manifest ``inputs`` entry recording which task was answered."""
|
|
77
|
+
|
|
78
|
+
def entry(path: Path | None) -> dict[str, str] | None:
|
|
79
|
+
return None if path is None else {"path": str(path.resolve()), "digest": _digest_file(path)}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
"name": self.name,
|
|
83
|
+
"criteria_file": entry(self.criteria_file),
|
|
84
|
+
"predicates_file": entry(self.predicates_file),
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass(frozen=True)
|
|
89
|
+
class RelaxationLedger:
|
|
90
|
+
"""What a zero-shot prediction actually estimates, written as ``relaxations.yaml``.
|
|
91
|
+
|
|
92
|
+
Only ``query_language``, ``decoded_quantity``, and ``exact`` are required. ``steps`` is free-form: one
|
|
93
|
+
mapping per rewrite or assumption between the requested and the answered task. The implementation
|
|
94
|
+
guide recommends ``name``, ``kind``, ``direction``, and ``detail`` keys; the contract does not validate
|
|
95
|
+
them. An exact algorithm writes ``exact: true`` and no steps.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
query_language: str
|
|
99
|
+
decoded_quantity: str
|
|
100
|
+
exact: bool
|
|
101
|
+
steps: Sequence[Mapping[str, Any]] = field(default_factory=tuple)
|
|
102
|
+
|
|
103
|
+
def __post_init__(self) -> None:
|
|
104
|
+
for name in ("query_language", "decoded_quantity"):
|
|
105
|
+
if not isinstance(getattr(self, name), str) or not getattr(self, name).strip():
|
|
106
|
+
raise ValueError(f"{RELAXATIONS_FILENAME} requires a non-empty {name}")
|
|
107
|
+
if not isinstance(self.exact, bool):
|
|
108
|
+
raise ValueError(f"{RELAXATIONS_FILENAME} requires exact: true or false")
|
|
109
|
+
object.__setattr__(self, "steps", tuple(dict(step) for step in self.steps))
|
|
110
|
+
|
|
111
|
+
def write(self, directory: Path | str) -> Path:
|
|
112
|
+
path = Path(directory) / RELAXATIONS_FILENAME
|
|
113
|
+
value = {
|
|
114
|
+
"query_language": self.query_language,
|
|
115
|
+
"decoded_quantity": self.decoded_quantity,
|
|
116
|
+
"exact": self.exact,
|
|
117
|
+
"steps": [dict(step) for step in self.steps],
|
|
118
|
+
}
|
|
119
|
+
path.write_text(yaml.safe_dump(value, sort_keys=False))
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def read(cls, directory: Path | str) -> RelaxationLedger:
|
|
124
|
+
path = Path(directory) / RELAXATIONS_FILENAME
|
|
125
|
+
if not path.is_file():
|
|
126
|
+
raise FileNotFoundError(f"{directory} has no {RELAXATIONS_FILENAME}")
|
|
127
|
+
value = yaml.safe_load(path.read_text()) or {}
|
|
128
|
+
missing = {"query_language", "decoded_quantity", "exact"} - set(value)
|
|
129
|
+
if missing:
|
|
130
|
+
raise ValueError(f"{path} lacks required fields {sorted(missing)}")
|
|
131
|
+
return cls(
|
|
132
|
+
query_language=value["query_language"],
|
|
133
|
+
decoded_quantity=value["decoded_quantity"],
|
|
134
|
+
exact=value["exact"],
|
|
135
|
+
steps=value.get("steps") or (),
|
|
136
|
+
)
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meds-algorithm
|
|
3
|
+
Version: 0.4.0a4
|
|
4
|
+
Summary: Shared MEDS algorithm contracts and conformance boundaries.
|
|
5
|
+
Project-URL: Repository, https://github.com/McDermottHealthAI/meds-algorithm
|
|
6
|
+
Project-URL: Documentation, https://github.com/McDermottHealthAI/meds-algorithm/blob/main/ARCHITECTURE.md
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: pyyaml>=6
|
|
11
|
+
Provides-Extra: conformance
|
|
12
|
+
Requires-Dist: meds-evaluation==0.0.3; extra == 'conformance'
|
|
13
|
+
Requires-Dist: polars>=1; extra == 'conformance'
|
|
14
|
+
Requires-Dist: pyarrow>=15; extra == 'conformance'
|
|
15
|
+
Requires-Dist: pytest>=8; extra == 'conformance'
|
|
16
|
+
Provides-Extra: data
|
|
17
|
+
Requires-Dist: polars>=1; extra == 'data'
|
|
18
|
+
Provides-Extra: test
|
|
19
|
+
Requires-Dist: meds-evaluation==0.0.3; extra == 'test'
|
|
20
|
+
Requires-Dist: polars>=1; extra == 'test'
|
|
21
|
+
Requires-Dist: pyarrow>=15; extra == 'test'
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# meds-algorithm
|
|
26
|
+
|
|
27
|
+
[](https://www.python.org/downloads/)
|
|
28
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/RELEASING.md)
|
|
29
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/actions/workflows/ci.yml)
|
|
30
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/ARCHITECTURE.md)
|
|
31
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/graphs/contributors)
|
|
32
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/pulls)
|
|
33
|
+
[](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
Shared infrastructure for implementing and validating MEDS algorithm repositories. The package defines
|
|
36
|
+
algorithm profiles and command contracts, manages artifacts and run provenance, reads prediction indices
|
|
37
|
+
and training targets, validates prediction outputs, and provides reusable conformance tests.
|
|
38
|
+
|
|
39
|
+
Algorithm repositories import these utilities so fixes can be distributed through a package update.
|
|
40
|
+
Algorithm architectures, tokenizers, training objectives and task-specific logic stay in the algorithm repository.
|
|
41
|
+
|
|
42
|
+
See the [architecture and UML diagrams](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/ARCHITECTURE.md) for the profile hierarchy, typed requests,
|
|
43
|
+
algorithm-owned configuration, artifact references and shared execution path.
|
|
44
|
+
|
|
45
|
+
## Package layout
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
src/meds_algorithm/
|
|
49
|
+
├── CONTRACT.md # canonical normative specification, included in the wheel
|
|
50
|
+
├── spec.py # installed specification reader
|
|
51
|
+
├── algorithms.py # Algorithm and profile ABCs, lifecycle order
|
|
52
|
+
├── requests.py # frozen per-step requests and CLI field mappings
|
|
53
|
+
├── configuration.py # AlgorithmConfig, NNConfig and execution options
|
|
54
|
+
├── serialization.py # strict, versioned config/request JSON serialization
|
|
55
|
+
├── runner.py # validate, execute, measure, publish; CLI adapter
|
|
56
|
+
├── contracts/commands.py # derived compatibility views and legacy dispatch
|
|
57
|
+
├── artifacts/
|
|
58
|
+
│ ├── references.py # lazy typed artifact references and payload metadata
|
|
59
|
+
│ ├── manifest.py # measured artifacts, envelope reading and digests
|
|
60
|
+
│ ├── publication.py # unique staging, exclusive writers and overwrite rollback
|
|
61
|
+
│ ├── inputs.py # resolve recorded upstream artifact paths
|
|
62
|
+
│ └── results.py # structured run evidence
|
|
63
|
+
├── runtime/
|
|
64
|
+
│ ├── provenance.py # explicitly supplied algorithm checkout
|
|
65
|
+
│ ├── resources.py # process-tree memory sampling and platform fallback
|
|
66
|
+
│ └── process.py # bounded subprocess execution
|
|
67
|
+
├── data/ # split selection, key-only index reads, context targets
|
|
68
|
+
├── tasks/spec.py # TaskSpec and RelaxationLedger
|
|
69
|
+
├── evaluation/predictions.py # keyed probabilities and post-score truth attachment
|
|
70
|
+
├── integrations/meds_dev.py # per-profile registration and CLI
|
|
71
|
+
└── conformance/ # assertions, synthetic fixtures, full-lifecycle checks,
|
|
72
|
+
# opt-in MEDS-DEV provisioning and algorithm adapter result types
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Installation and dependencies
|
|
76
|
+
|
|
77
|
+
Install a reviewed revision with
|
|
78
|
+
`pip install 'meds-algorithm @ git+https://github.com/McDermottHealthAI/meds-algorithm.git@<full-commit-sha>'`.
|
|
79
|
+
The template currently records the exact SHA. For package-index release checks and publishing setup,
|
|
80
|
+
see the [release guide](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/RELEASING.md).
|
|
81
|
+
|
|
82
|
+
- Base package: PyYAML; no dataframe engine, test runner, GPU or algorithm framework is imported by base APIs.
|
|
83
|
+
- `data` extra: Polars for index/target reads and prediction validation.
|
|
84
|
+
- `conformance` extra: pytest, Polars, PyArrow and the pinned MEDS evaluation schema implementation.
|
|
85
|
+
- `test` extra: dependencies for developing and testing this package.
|
|
86
|
+
|
|
87
|
+
The installed CLI provides `meds-algorithm-spec`, `meds-algorithm-resolve-input`, and
|
|
88
|
+
`meds-algorithm-add-to-meds-dev`. Algorithm repositories provide their own `meds-algorithm` entry point.
|
|
89
|
+
|
|
90
|
+
New algorithm repositories implement `Algorithm` profile subclasses, receive frozen typed requests, and
|
|
91
|
+
execute through `AlgorithmRepository`/`StepRunner`. Their own frozen `AlgorithmConfig` subclass defines
|
|
92
|
+
hyperparameters, defaults and validation. Use `config_file=...` for a JSON/YAML values mapping or a
|
|
93
|
+
previously saved `config.json`. Typed prediction execution requires the `data` extra. See the
|
|
94
|
+
[architecture guide](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/ARCHITECTURE.md) for the complete ownership and serialization algorithm.
|
|
95
|
+
|
|
96
|
+
Neural algorithms can extend `NNConfig` for a capacity multiplier, learning rate, batch size,
|
|
97
|
+
weight decay and dropout. Concrete algorithms define the multiplier-to-architecture mapping and training
|
|
98
|
+
defaults. See [neural configuration and scaling](https://github.com/McDermottHealthAI/meds-algorithm/blob/main/ARCHITECTURE.md#neural-network-configuration-and-scaling-experiments)
|
|
99
|
+
for an extensible example and the recommended upstream ownership of data scaling.
|
|
100
|
+
|
|
101
|
+
## Ownership
|
|
102
|
+
|
|
103
|
+
The template imports this package directly; it does not generate a `meds_algorithm_contract` package or
|
|
104
|
+
copy these implementations. Generated files contain algorithm command skeletons, selected profile YAMLs,
|
|
105
|
+
execution adapters in `tests/e2e.py`, algorithm-specific tests, and project/cluster configuration.
|
|
106
|
+
Family sharing belongs inside one algorithm-family repository with several profiles. Tokenizers,
|
|
107
|
+
architectures, objectives, payload layouts and task translators remain algorithm-owned.
|
|
108
|
+
|
|
109
|
+
`CONTRACT.md` is the only normative document. Template documentation renders it from the installed
|
|
110
|
+
package. Algorithm `SPEC.md` files point to it; algorithm implementation reports record local decisions.
|
|
111
|
+
|
|
112
|
+
MEDS-DEV's upstream interface still uses `model.yaml`, `src/MEDS_DEV/models`, `meds-dev-model`,
|
|
113
|
+
`model=`, and `{model_initialization_dir}`. Registration translates the local `algorithms/<profile>/`
|
|
114
|
+
layout into that interface; these upstream names are not part of this package's rename.
|
|
115
|
+
|
|
116
|
+
## Publication, provenance and measurement
|
|
117
|
+
|
|
118
|
+
Pass the algorithm checkout explicitly to `StepRunner(repo=algorithm_root)` or, for legacy implementations,
|
|
119
|
+
`measured_artifact(..., repo=algorithm_root)`. The package never
|
|
120
|
+
infers the algorithm from its own installation path or the working directory. Without a supplied checkout,
|
|
121
|
+
commit and dirty state are unknown (`null`); wheel deployments must retain algorithm build provenance
|
|
122
|
+
separately rather than inventing a checkout revision.
|
|
123
|
+
|
|
124
|
+
Publication uses a unique sibling staging directory and an exclusive destination lock. A new artifact
|
|
125
|
+
appears in one rename. Overwrite retains the old directory as a backup until the new directory is
|
|
126
|
+
published, and restores it if publication fails. Replacement uses two renames, so concurrent readers
|
|
127
|
+
can briefly observe an absent destination. Prefer immutable output paths for concurrent readers.
|
|
128
|
+
This is not a power-loss transaction: after a process crash, inspect the `.lock`, `.staging-*` and
|
|
129
|
+
`.backup-*` siblings before manually recovering. If restoration itself fails, the exception names the
|
|
130
|
+
preserved backup; it is never silently deleted. Locks apply to cooperating core writers.
|
|
131
|
+
|
|
132
|
+
Linux sampling visits the children of every process thread, including subprocesses launched by worker
|
|
133
|
+
threads. Sampling can miss short-lived peaks; inaccessible descendants are not a complete measurement.
|
|
134
|
+
On platforms without `/proc`, use process-only `getrusage` when available; otherwise report unavailable
|
|
135
|
+
values as `null`. GPU instrumentation remains the algorithm/framework's responsibility.
|
|
136
|
+
|
|
137
|
+
## Validation
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
uv sync --extra test --group dev
|
|
141
|
+
uv run ruff check .
|
|
142
|
+
uv run ruff format --check .
|
|
143
|
+
uv run pytest -q
|
|
144
|
+
uv build
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Tests inject publication/restoration failures, launch a child from a worker thread, check explicit
|
|
148
|
+
algorithm provenance, reject missing/mixed/overlapping label inputs, and exercise all six lifecycles with
|
|
149
|
+
synthetic command implementations. Full-lifecycle conformance reruns algorithm adapters with forbidden
|
|
150
|
+
outcomes removed and fresh artifacts; algorithm-specific cutoff, frozen-weight, compatibility and primary
|
|
151
|
+
behavior tests remain required. Package tests do not establish scientific validity of a downstream algorithm.
|
|
152
|
+
|
|
153
|
+
MEDS-DEV provisioning requires explicit `-m meds_dev` or `-m real_data` selection; setting
|
|
154
|
+
`MEDS_DEV_DIR` only supplies a source checkout. Unit tests do not download a real dataset or launch
|
|
155
|
+
training. Keep each algorithm's data-access and cluster approval rules in its own repository.
|
|
156
|
+
|
|
157
|
+
## Changes
|
|
158
|
+
|
|
159
|
+
### 0.4.0a4
|
|
160
|
+
|
|
161
|
+
- Rename the repository/distribution from `meds-algorithm-core` to `meds-algorithm`, the Python package
|
|
162
|
+
from `meds_algorithm_core` to `meds_algorithm`, and the contract command to `meds-algorithm-spec`.
|
|
163
|
+
Update imports and dependency pins together. No old-name aliases are installed; serialized envelopes
|
|
164
|
+
with old package-qualified type names require an explicit migration before loading.
|
|
165
|
+
|
|
166
|
+
### 0.4.0a3
|
|
167
|
+
|
|
168
|
+
- Add optional `NNConfig(AlgorithmConfig)` with a scalar capacity multiplier and strict immutable settings,
|
|
169
|
+
inherited serialization, validation and runner provenance, without neural-framework dependencies.
|
|
170
|
+
- Document capacity semantics, configuration sweeps and the distinction between implemented configuration
|
|
171
|
+
and future upstream data-scaling/consumption-reporting integrations.
|
|
172
|
+
|
|
173
|
+
### 0.4.0a2
|
|
174
|
+
|
|
175
|
+
- Add optional `AlgorithmConfig.additional_predicates_file` for dataset-specific feature predicates.
|
|
176
|
+
Preprocessing composes it with the base, rejects duplicate names/keys, preserves the resolved file
|
|
177
|
+
and source digests, and keeps task predicates unchanged. Later steps do not reopen temporary sources.
|
|
178
|
+
|
|
179
|
+
### 0.4.0a1
|
|
180
|
+
|
|
181
|
+
- Add profile ABCs, typed requests/artifact references and a shared runner that owns validation,
|
|
182
|
+
measured publication and post-score truth attachment.
|
|
183
|
+
- Add strict immutable algorithm-owned configuration and versioned config/request serialization; record
|
|
184
|
+
complete resolved values, upstream manifest identities and key-only index identities.
|
|
185
|
+
- Derive profile/step metadata from the new classes. Rename the package, Python API, CLI and artifact
|
|
186
|
+
types/arguments to algorithm terminology under contract version 4. No old-name aliases are installed;
|
|
187
|
+
migrations remain separate. Mapping-based execution is available as `LegacyAlgorithmRepository`.
|
|
188
|
+
- Include the canonical API/UML architecture guide in the wheel for downstream documentation.
|
|
189
|
+
|
|
190
|
+
### 0.3.0a3
|
|
191
|
+
|
|
192
|
+
- Compare seeded full-lifecycle predictions with and without forbidden labels, preserving separate
|
|
193
|
+
training, index and evaluation inputs.
|
|
194
|
+
- Validate original and rerun probabilities across every prediction shard, and accept a separate
|
|
195
|
+
evaluation index when checking context-label overlap.
|
|
196
|
+
- Terminate subprocess trees on timeout or interruption; deliberately detached services are unsupported.
|
|
197
|
+
|
|
198
|
+
### 0.3.0a2
|
|
199
|
+
|
|
200
|
+
- Complete the extraction of shared artifact, runtime, data, task, evaluation, MEDS-DEV and
|
|
201
|
+
conformance utilities from the template; replace the reserved placeholder modules with implementations.
|
|
202
|
+
- Add exclusive artifact writers, overwrite rollback, explicit algorithm-checkout provenance and
|
|
203
|
+
process-tree memory sampling with platform fallbacks.
|
|
204
|
+
- Reject ambiguous splits, overlapping context/evaluation keys, invalid probabilities and malformed
|
|
205
|
+
artifact manifests; rerun complete profile lifecycles to check for outcome-label leakage.
|
|
206
|
+
- Make the core package the source of the contract and shared tests, removing generated copies from
|
|
207
|
+
the template. Migration of existing algorithm repositories is separate.
|
|
208
|
+
|
|
209
|
+
### 0.3.0a1
|
|
210
|
+
|
|
211
|
+
- Introduce contract version 3, profile/command definitions, the installed specification reader and
|
|
212
|
+
a key-only prediction index reader.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
meds_algorithm/CONTRACT.md,sha256=xntPBoiX20BrgKIwINrMIuGfZ69jWSXzD8xhgTg1rTc,42152
|
|
2
|
+
meds_algorithm/__init__.py,sha256=Mu7qfI8R8ErHNkvhalMsxa6G12cGt-5YPtZyzLKlt5A,2526
|
|
3
|
+
meds_algorithm/algorithms.py,sha256=4C9r9qHSBDK3RldbL2oSQ-MsQuCSpYp7eMxbFdelypU,5653
|
|
4
|
+
meds_algorithm/configuration.py,sha256=JeCl14sHTTso2PQNJ3x4YKlT_CtF2UPTiV8w9mY2xqE,2163
|
|
5
|
+
meds_algorithm/requests.py,sha256=Jhouni-ifB698j007WowPPjP36ZDGGlQd2Zrzty16R0,10196
|
|
6
|
+
meds_algorithm/runner.py,sha256=1o3eQ7Zq79YYXPVG8-Ur_-eTf7KzfRWOZ33acTcPJtM,10988
|
|
7
|
+
meds_algorithm/serialization.py,sha256=A2Kw3eIqgKv5T1iWzc8m3DiK_beob-33lip_PtD2BJM,5059
|
|
8
|
+
meds_algorithm/spec.py,sha256=905AEeaHiltB0WpjZnzKWcmmEn6xuaLWYabh7UinbhE,637
|
|
9
|
+
meds_algorithm/version.py,sha256=ZZ9-aff2JbQFi-0cPGgZlE0q46rN6lNLQIrLTrd68lU,93
|
|
10
|
+
meds_algorithm/artifacts/__init__.py,sha256=U0LxpaI6dp7bUj-6UwyNgONOeSYLjC1l4j7SMZYbUvQ,74
|
|
11
|
+
meds_algorithm/artifacts/inputs.py,sha256=ReLGWzdicazWw25pXXKKgbP2UvVz8STyonW9Yw_gO8A,1561
|
|
12
|
+
meds_algorithm/artifacts/manifest.py,sha256=ogw27Mki-P4J9ZGpGbSV4rM2u9YzkG17cUmJ1s-1tck,5914
|
|
13
|
+
meds_algorithm/artifacts/publication.py,sha256=hPS96rGzmkORz3x7-oVSsaD9z8gjw9zWwgPHOIx8bUk,2922
|
|
14
|
+
meds_algorithm/artifacts/references.py,sha256=GBMsBrm9INRtjnaHNbPe8thKyZe_f0jJW_L610Gnw8Q,2139
|
|
15
|
+
meds_algorithm/artifacts/results.py,sha256=ov00gdQzldVv0igd47gkS-UJy7S8IXPNoW9WPyNxC_A,2786
|
|
16
|
+
meds_algorithm/conformance/__init__.py,sha256=6yj79ojbhg2QuUCQW6f3TtZWHDN4bffTrfuAcpkQ0_Q,96
|
|
17
|
+
meds_algorithm/conformance/assertions.py,sha256=sT90qNusDYxN5K1GMv-lN7aVbaWYqNiHjcV-CAORneU,2975
|
|
18
|
+
meds_algorithm/conformance/end_to_end.py,sha256=odQ--nDDPLjrmgzAdseBkfmxzUHq0mtIiJ-IC13ilt4,7207
|
|
19
|
+
meds_algorithm/conformance/meds_dev.py,sha256=ap1vpHiA_4ONZWhB2m0_U6VSVg-lip0syKa4j8T9dTE,5649
|
|
20
|
+
meds_algorithm/conformance/reports.py,sha256=DfY90OZiVsh9EmhglGXKKKnD6wKD5aIu3hyKlKaEyd4,1907
|
|
21
|
+
meds_algorithm/conformance/signal.py,sha256=l8Q0dbGTuKLvLU9sHB6FogH-61cdCoBa9ccspya4NL0,5919
|
|
22
|
+
meds_algorithm/conformance/types.py,sha256=LpZp4R2nQSDa3EHjzkHOK_-HTuOIQBTNhmQjhG3JcEs,1017
|
|
23
|
+
meds_algorithm/contracts/__init__.py,sha256=MIWkD45lGJ4LpQ9TAaY3hQkyRVuE3kFTPipof8dik8M,61
|
|
24
|
+
meds_algorithm/contracts/commands.py,sha256=h4UPFAF_R1ac7NTxXZvSeUEhUNDj-AKgLpL2sXuBSpc,7390
|
|
25
|
+
meds_algorithm/contracts/enums.py,sha256=Ln7qqjTeiuPVO6oPh-7SL8Do9wWOB_i8T3V2s64JS7o,948
|
|
26
|
+
meds_algorithm/data/__init__.py,sha256=Ln7Tf2l5OX_pOFUOo84ZAHmaNCBqaeeWyRNitsRk7k0,84
|
|
27
|
+
meds_algorithm/data/index.py,sha256=EXuJDsiir0UWSyz7WQEPhs_pkELa-OSFghww-mQRJ7I,2580
|
|
28
|
+
meds_algorithm/data/labels.py,sha256=G2131UJL-g2XEVm6i4iMrQ-hzDudtagc1ig0l_x_FmE,2316
|
|
29
|
+
meds_algorithm/data/predicates.py,sha256=n7Gx3nofly8Nbx7GKDvm21rpHRkDlUNrGiEZhKk9TYw,2623
|
|
30
|
+
meds_algorithm/data/validation.py,sha256=qTbhiaiT-cIaqPsoDuksajRakz561IZx3WpIv3i_RoQ,1002
|
|
31
|
+
meds_algorithm/evaluation/__init__.py,sha256=m40EM0hLh7S7jFydZ1QpPU7L_gUjLbCqg4nclO_xV48,67
|
|
32
|
+
meds_algorithm/evaluation/predictions.py,sha256=LIyu_VUMG_k00FacKS_ahMlqQjFMraotKye2iU06ZWk,2437
|
|
33
|
+
meds_algorithm/integrations/__init__.py,sha256=P0aiVlwgV1CC1EdgW_TXOw652yZL62Xly81seP9FGgE,60
|
|
34
|
+
meds_algorithm/integrations/meds_dev.py,sha256=z5e7UVrg8AjOqq0GGNCJmUxbhEEvyzl2AlzmNVj8FoI,2190
|
|
35
|
+
meds_algorithm/runtime/__init__.py,sha256=tbgzQJiN1yQ9TmgTwLsyUshHjkMlRsLhVdu25ilW0K0,99
|
|
36
|
+
meds_algorithm/runtime/process.py,sha256=1v1PIcNyw1RJ0_lWrmKM1k2jcMLhRMwoKxZOVl69A0s,1726
|
|
37
|
+
meds_algorithm/runtime/provenance.py,sha256=uDaA9xMfpEDwf7hLuYNs9vI0LsMRGctdw_2YUk4nDWY,1019
|
|
38
|
+
meds_algorithm/runtime/resources.py,sha256=K9rPREOL2x1uU0k6Sk_7_aaP72zD0QrROhO2r31JqG8,3623
|
|
39
|
+
meds_algorithm/tasks/__init__.py,sha256=I8JslXTkVYHfQc19k_voL7CqxAHBliNarTQnl4brJE4,55
|
|
40
|
+
meds_algorithm/tasks/spec.py,sha256=59AB902iPIcaxRGSNKdnG50UxhIBkGHZVttc1TfnRqw,5961
|
|
41
|
+
meds_algorithm/ARCHITECTURE.md,sha256=X7ytGqAEdZMA2YNEMlyI7AQVdvFnQlsYk85cV7uBwwo,18026
|
|
42
|
+
meds_algorithm-0.4.0a4.dist-info/METADATA,sha256=ZTIEi0SALEx93WbH8eFfFPlKujFSMGdpCb949LETjpg,13444
|
|
43
|
+
meds_algorithm-0.4.0a4.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
44
|
+
meds_algorithm-0.4.0a4.dist-info/entry_points.txt,sha256=mmxSsbl65W6_GFEIiMYlYeH6ulSGA-iITYQP3IQRYfs,208
|
|
45
|
+
meds_algorithm-0.4.0a4.dist-info/licenses/LICENSE,sha256=k0wqQurs6KfFG-mjx2fUCVlEqpUSNyiyO2sGtU8U-Ac,1119
|
|
46
|
+
meds_algorithm-0.4.0a4.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matthew McDermott and the MEDS_algorithm_template contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|