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.
Files changed (46) hide show
  1. arcasleep/__init__.py +8 -0
  2. arcasleep/artifacts/__init__.py +0 -0
  3. arcasleep/artifacts/provenance.py +196 -0
  4. arcasleep/artifacts/publish.py +1468 -0
  5. arcasleep/artifacts/templates/quickstart.md +52 -0
  6. arcasleep/artifacts/upload.py +120 -0
  7. arcasleep/cli.py +1241 -0
  8. arcasleep/data/batching.py +197 -0
  9. arcasleep/data/bundle.py +62 -0
  10. arcasleep/data/cache.py +121 -0
  11. arcasleep/data/constants.py +19 -0
  12. arcasleep/data/prepare.py +538 -0
  13. arcasleep/data/pseudonyms.py +44 -0
  14. arcasleep/data/splits.py +285 -0
  15. arcasleep/evaluation/__init__.py +12 -0
  16. arcasleep/evaluation/run.py +587 -0
  17. arcasleep/evaluation/staging_metrics.py +405 -0
  18. arcasleep/ggir.py +420 -0
  19. arcasleep/models.py +186 -0
  20. arcasleep/predict.py +611 -0
  21. arcasleep/reporting/__init__.py +5 -0
  22. arcasleep/reporting/compare.py +473 -0
  23. arcasleep/reporting/hypnogram.py +399 -0
  24. arcasleep/reporting/prediction_plot.py +224 -0
  25. arcasleep/reporting/summary.py +251 -0
  26. arcasleep/training/balanced.py +126 -0
  27. arcasleep/training/config.py +281 -0
  28. arcasleep/training/driver.py +267 -0
  29. arcasleep/training/folds.py +142 -0
  30. arcasleep-0.1.1.dist-info/METADATA +161 -0
  31. arcasleep-0.1.1.dist-info/RECORD +46 -0
  32. arcasleep-0.1.1.dist-info/WHEEL +5 -0
  33. arcasleep-0.1.1.dist-info/entry_points.txt +2 -0
  34. arcasleep-0.1.1.dist-info/licenses/LICENSE +21 -0
  35. arcasleep-0.1.1.dist-info/top_level.txt +3 -0
  36. arcasleep_bidoze/__init__.py +1 -0
  37. arcasleep_bidoze/family.py +100 -0
  38. arcasleep_bidoze/model/__init__.py +9 -0
  39. arcasleep_bidoze/model/constants.py +18 -0
  40. arcasleep_bidoze/model/definition.py +3591 -0
  41. arcasleep_bidoze/training.py +374 -0
  42. arcasleep_unet/__init__.py +1 -0
  43. arcasleep_unet/family.py +89 -0
  44. arcasleep_unet/model/__init__.py +8 -0
  45. arcasleep_unet/model/definition.py +535 -0
  46. 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
+ }