arcasleep 0.1.1__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.
- arcasleep/__init__.py +8 -0
- arcasleep/artifacts/__init__.py +0 -0
- arcasleep/artifacts/provenance.py +196 -0
- arcasleep/artifacts/publish.py +1468 -0
- arcasleep/artifacts/templates/quickstart.md +52 -0
- arcasleep/artifacts/upload.py +120 -0
- arcasleep/cli.py +1241 -0
- arcasleep/data/batching.py +197 -0
- arcasleep/data/bundle.py +62 -0
- arcasleep/data/cache.py +121 -0
- arcasleep/data/constants.py +19 -0
- arcasleep/data/prepare.py +538 -0
- arcasleep/data/pseudonyms.py +44 -0
- arcasleep/data/splits.py +285 -0
- arcasleep/evaluation/__init__.py +12 -0
- arcasleep/evaluation/run.py +587 -0
- arcasleep/evaluation/staging_metrics.py +405 -0
- arcasleep/ggir.py +420 -0
- arcasleep/models.py +186 -0
- arcasleep/predict.py +611 -0
- arcasleep/reporting/__init__.py +5 -0
- arcasleep/reporting/compare.py +473 -0
- arcasleep/reporting/hypnogram.py +399 -0
- arcasleep/reporting/prediction_plot.py +224 -0
- arcasleep/reporting/summary.py +251 -0
- arcasleep/training/balanced.py +126 -0
- arcasleep/training/config.py +281 -0
- arcasleep/training/driver.py +267 -0
- arcasleep/training/folds.py +142 -0
- arcasleep-0.1.1.dist-info/METADATA +161 -0
- arcasleep-0.1.1.dist-info/RECORD +46 -0
- arcasleep-0.1.1.dist-info/WHEEL +5 -0
- arcasleep-0.1.1.dist-info/entry_points.txt +2 -0
- arcasleep-0.1.1.dist-info/licenses/LICENSE +21 -0
- arcasleep-0.1.1.dist-info/top_level.txt +3 -0
- arcasleep_bidoze/__init__.py +1 -0
- arcasleep_bidoze/family.py +100 -0
- arcasleep_bidoze/model/__init__.py +9 -0
- arcasleep_bidoze/model/constants.py +18 -0
- arcasleep_bidoze/model/definition.py +3591 -0
- arcasleep_bidoze/training.py +374 -0
- arcasleep_unet/__init__.py +1 -0
- arcasleep_unet/family.py +89 -0
- arcasleep_unet/model/__init__.py +8 -0
- arcasleep_unet/model/definition.py +535 -0
- arcasleep_unet/training.py +137 -0
arcasleep/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Arcascope's sleep-stage models: shared data, training, evaluation and release tooling."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError, version as _distribution_version
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
__version__ = _distribution_version("arcasleep")
|
|
7
|
+
except PackageNotFoundError: # pragma: no cover - running from a source tree
|
|
8
|
+
__version__ = "0.0.0"
|
|
File without changes
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Deterministic dataset manifests and run-provenance primitives.
|
|
2
|
+
|
|
3
|
+
The manifest hash identifies an ordered set of byte-addressed inputs. S3 ETags
|
|
4
|
+
are deliberately excluded: multipart ETags are not portable content hashes.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import base64
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from dataclasses import asdict, dataclass
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Iterable
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def canonical_json(value: object) -> str:
|
|
18
|
+
"""Canonical JSON used for manifest lines and their aggregate digest."""
|
|
19
|
+
return json.dumps(value, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def sha256_bytes(value: bytes) -> str:
|
|
23
|
+
return hashlib.sha256(value).hexdigest()
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def sha256_file(path: Path, *, block_size: int = 1024 * 1024) -> str:
|
|
27
|
+
digest = hashlib.sha256()
|
|
28
|
+
with Path(path).open("rb") as handle:
|
|
29
|
+
while block := handle.read(block_size):
|
|
30
|
+
digest.update(block)
|
|
31
|
+
return digest.hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class ManifestEntry:
|
|
36
|
+
path: str
|
|
37
|
+
size_bytes: int
|
|
38
|
+
sha256: str
|
|
39
|
+
object_version_id: str | None = None
|
|
40
|
+
|
|
41
|
+
def canonical_line(self) -> str:
|
|
42
|
+
return canonical_json(asdict(self))
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def manifest_payload(entries: Iterable[ManifestEntry]) -> bytes:
|
|
46
|
+
lines = [entry.canonical_line() for entry in sorted(entries, key=lambda item: item.path)]
|
|
47
|
+
return ("\n".join(lines) + ("\n" if lines else "")).encode("utf-8")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def manifest_digest(entries: Iterable[ManifestEntry]) -> str:
|
|
51
|
+
return sha256_bytes(manifest_payload(entries))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def write_manifest(entries: Iterable[ManifestEntry], output: Path) -> str:
|
|
55
|
+
"""Write deterministic UTF-8 JSONL and return its SHA-256 digest."""
|
|
56
|
+
payload = manifest_payload(entries)
|
|
57
|
+
output = Path(output)
|
|
58
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
output.write_bytes(payload)
|
|
60
|
+
return sha256_bytes(payload)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def build_local_manifest(root: Path) -> list[ManifestEntry]:
|
|
64
|
+
"""Hash every regular file beneath ``root`` using normalized relative paths."""
|
|
65
|
+
root = Path(root).resolve()
|
|
66
|
+
if not root.is_dir():
|
|
67
|
+
raise ValueError(f"dataset root is not a directory: {root}")
|
|
68
|
+
entries: list[ManifestEntry] = []
|
|
69
|
+
for path in sorted(item for item in root.rglob("*") if item.is_file()):
|
|
70
|
+
relative = path.relative_to(root).as_posix()
|
|
71
|
+
entries.append(
|
|
72
|
+
ManifestEntry(path=relative, size_bytes=path.stat().st_size, sha256=sha256_file(path))
|
|
73
|
+
)
|
|
74
|
+
return entries
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _s3_checksum_to_hex(value: str | None) -> str | None:
|
|
78
|
+
if not value:
|
|
79
|
+
return None
|
|
80
|
+
return base64.b64decode(value).hex()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def build_s3_manifest(
|
|
84
|
+
bucket: str, prefix: str, *, require_sha256: bool = True, s3_client=None
|
|
85
|
+
) -> list[ManifestEntry]:
|
|
86
|
+
"""Build an immutable S3 object inventory from version IDs and SHA-256 checksums.
|
|
87
|
+
|
|
88
|
+
Objects must have had an S3 SHA-256 checksum supplied when written. This is
|
|
89
|
+
the only S3 metadata accepted as a byte-content identity; an ETag is never
|
|
90
|
+
substituted.
|
|
91
|
+
"""
|
|
92
|
+
if s3_client is None:
|
|
93
|
+
import boto3
|
|
94
|
+
|
|
95
|
+
s3_client = boto3.client("s3")
|
|
96
|
+
prefix = prefix.lstrip("/")
|
|
97
|
+
paginator = s3_client.get_paginator("list_objects_v2")
|
|
98
|
+
entries: list[ManifestEntry] = []
|
|
99
|
+
for page in paginator.paginate(Bucket=bucket, Prefix=prefix):
|
|
100
|
+
for object_summary in page.get("Contents", []):
|
|
101
|
+
key = object_summary["Key"]
|
|
102
|
+
if key.endswith("/"):
|
|
103
|
+
continue
|
|
104
|
+
head = s3_client.head_object(Bucket=bucket, Key=key, ChecksumMode="ENABLED")
|
|
105
|
+
checksum = _s3_checksum_to_hex(head.get("ChecksumSHA256"))
|
|
106
|
+
if require_sha256 and checksum is None:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
f"s3://{bucket}/{key} has no S3 SHA-256 checksum; re-upload it with "
|
|
109
|
+
"ChecksumAlgorithm=SHA256 or create a verified local manifest."
|
|
110
|
+
)
|
|
111
|
+
if checksum is None:
|
|
112
|
+
continue
|
|
113
|
+
rel_key = key[len(prefix) :].lstrip("/") if prefix else key
|
|
114
|
+
entries.append(
|
|
115
|
+
ManifestEntry(
|
|
116
|
+
path=rel_key,
|
|
117
|
+
size_bytes=int(head["ContentLength"]),
|
|
118
|
+
sha256=checksum,
|
|
119
|
+
object_version_id=head.get("VersionId"),
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
return entries
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
DEFAULT_MANIFEST_PATH = Path("build/manifest/manifest.jsonl")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_fingerprint(*, data_digest: str, code_digest: str, length: int = 12) -> str:
|
|
129
|
+
"""Short, stable identifier for the pair (input bytes, code that ran).
|
|
130
|
+
|
|
131
|
+
Two runs share a fingerprint exactly when they saw the same feature cache
|
|
132
|
+
and ran the same code, which is the property that makes it worth recording
|
|
133
|
+
on every row of a results CSV: rows from reproducible runs collide, and
|
|
134
|
+
rows that differ in either half do not.
|
|
135
|
+
"""
|
|
136
|
+
payload = canonical_json({"code": code_digest, "data": data_digest})
|
|
137
|
+
return sha256_bytes(payload.encode("utf-8"))[:length]
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def data_digest(manifest_path: Path | str | None = None) -> str | None:
|
|
141
|
+
"""SHA-256 of the feature manifest, or None when it is not present.
|
|
142
|
+
|
|
143
|
+
The manifest, not the image tag: a tag is a claim about the bytes, the
|
|
144
|
+
manifest is a checkable record of them.
|
|
145
|
+
"""
|
|
146
|
+
path = Path(manifest_path or os.environ.get("BIDOZE_MANIFEST_PATH") or DEFAULT_MANIFEST_PATH)
|
|
147
|
+
return sha256_file(path) if path.is_file() else None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def code_digest() -> str | None:
|
|
151
|
+
"""Identify the running code: ``GIT_COMMIT`` if baked in, else ask git.
|
|
152
|
+
|
|
153
|
+
Images carry GIT_COMMIT because they have neither git nor a .git dir. A
|
|
154
|
+
working tree with uncommitted changes gets a ``-dirty`` suffix, so the
|
|
155
|
+
digest never claims a commit whose tree is not what actually ran.
|
|
156
|
+
"""
|
|
157
|
+
baked = os.environ.get("GIT_COMMIT")
|
|
158
|
+
if baked:
|
|
159
|
+
return baked
|
|
160
|
+
import subprocess
|
|
161
|
+
|
|
162
|
+
def _git(*args: str) -> str | None:
|
|
163
|
+
try:
|
|
164
|
+
done = subprocess.run(
|
|
165
|
+
["git", *args], capture_output=True, text=True, timeout=5, check=False
|
|
166
|
+
)
|
|
167
|
+
except (OSError, subprocess.SubprocessError):
|
|
168
|
+
return None
|
|
169
|
+
return done.stdout.strip() if done.returncode == 0 else None
|
|
170
|
+
|
|
171
|
+
commit = _git("rev-parse", "--short", "HEAD")
|
|
172
|
+
if commit is None:
|
|
173
|
+
return None
|
|
174
|
+
return f"{commit}-dirty" if _git("status", "--porcelain") else commit
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def run_provenance(
|
|
178
|
+
*, package_version: str, raw_manifest_sha256: str, split_manifest_sha256: str,
|
|
179
|
+
derived_manifest_sha256: str | None, processing_config_sha256: str,
|
|
180
|
+
training_config_sha256: str, model_config_sha256: str, seed: int,
|
|
181
|
+
git_commit: str | None = None, image_digest: str | None = None,
|
|
182
|
+
) -> dict[str, object]:
|
|
183
|
+
"""Structured provenance shared by training, evaluation, and publishing."""
|
|
184
|
+
return {
|
|
185
|
+
"schema_version": 1,
|
|
186
|
+
"package_version": package_version,
|
|
187
|
+
"git_commit": git_commit or os.environ.get("GIT_COMMIT"),
|
|
188
|
+
"container_image_digest": image_digest or os.environ.get("CONTAINER_IMAGE_DIGEST"),
|
|
189
|
+
"raw_dataset_manifest_sha256": raw_manifest_sha256,
|
|
190
|
+
"split_manifest_sha256": split_manifest_sha256,
|
|
191
|
+
"derived_feature_manifest_sha256": derived_manifest_sha256,
|
|
192
|
+
"processing_config_sha256": processing_config_sha256,
|
|
193
|
+
"training_config_sha256": training_config_sha256,
|
|
194
|
+
"model_config_sha256": model_config_sha256,
|
|
195
|
+
"seed": int(seed),
|
|
196
|
+
}
|