katabatic 0.2.0__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.
- katabatic/__init__.py +15 -0
- katabatic/artifacts/__init__.py +27 -0
- katabatic/artifacts/base.py +21 -0
- katabatic/artifacts/dataset_split.py +138 -0
- katabatic/artifacts/ids.py +19 -0
- katabatic/artifacts/local.py +33 -0
- katabatic/artifacts/refs.py +88 -0
- katabatic/cli/__init__.py +3 -0
- katabatic/cli/commands/__init__.py +3 -0
- katabatic/cli/commands/model_init.py +201 -0
- katabatic/cli/commands/pin_notebook_kernel.py +101 -0
- katabatic/cli/commands/register_dataset.py +34 -0
- katabatic/cli/main.py +101 -0
- katabatic/datasets/__init__.py +9 -0
- katabatic/datasets/adult.csv +32562 -0
- katabatic/datasets/car.csv +1729 -0
- katabatic/datasets/car1.csv +10 -0
- katabatic/datasets/compatibility.py +45 -0
- katabatic/datasets/magic.csv +19021 -0
- katabatic/datasets/nursery.csv +12961 -0
- katabatic/datasets/profile.py +85 -0
- katabatic/datasets/registry.py +109 -0
- katabatic/datasets/shuttle.csv +58001 -0
- katabatic/evaluate/base_evaluation.py +24 -0
- katabatic/evaluate/consistency/__init__.py +3 -0
- katabatic/evaluate/consistency/evaluation.py +255 -0
- katabatic/evaluate/diversity/__init__.py +3 -0
- katabatic/evaluate/diversity/evaluation.py +256 -0
- katabatic/evaluate/fidelity/__init__.py +3 -0
- katabatic/evaluate/fidelity/evaluation.py +212 -0
- katabatic/evaluate/privacy/__init__.py +3 -0
- katabatic/evaluate/privacy/evaluation.py +259 -0
- katabatic/evaluate/report/__init__.py +3 -0
- katabatic/evaluate/report/composite.py +156 -0
- katabatic/evaluate/stability/__init__.py +3 -0
- katabatic/evaluate/stability/evaluation.py +191 -0
- katabatic/evaluate/tstr/evaluation.py +204 -0
- katabatic/evaluate/utility/__init__.py +3 -0
- katabatic/evaluate/utility/evaluation.py +268 -0
- katabatic/models/__init__.py +9 -0
- katabatic/models/base_model.py +67 -0
- katabatic/models/codi/README.md +67 -0
- katabatic/models/codi/__init__.py +12 -0
- katabatic/models/codi/image.png +0 -0
- katabatic/models/codi/models.py +685 -0
- katabatic/models/codi/utils.py +549 -0
- katabatic/models/ctgan/__init__.py +5 -0
- katabatic/models/ctgan/models.py +596 -0
- katabatic/models/ctgan/utils.py +194 -0
- katabatic/models/ganblr/README.md +79 -0
- katabatic/models/ganblr/__init__.py +14 -0
- katabatic/models/ganblr/models.py +833 -0
- katabatic/models/ganblr/utils.py +137 -0
- katabatic/models/great/great_dataset.py +97 -0
- katabatic/models/great/great_start.py +167 -0
- katabatic/models/great/great_trainer.py +46 -0
- katabatic/models/great/great_utils.py +185 -0
- katabatic/models/great/models.py +919 -0
- katabatic/models/medgan/README.md +73 -0
- katabatic/models/medgan/__init__.py +11 -0
- katabatic/models/medgan/models.py +384 -0
- katabatic/models/medgan/utils.py +201 -0
- katabatic/models/pategan/README.md +327 -0
- katabatic/models/pategan/__init__.py +4 -0
- katabatic/models/pategan/models.py +717 -0
- katabatic/models/pategan/utils.py +320 -0
- katabatic/models/registry.py +150 -0
- katabatic/models/tabddpm/__init__.py +5 -0
- katabatic/models/tabddpm/models.py +680 -0
- katabatic/models/tabddpm/utils.py +225 -0
- katabatic/models/tabsyn/__init__.py +5 -0
- katabatic/models/tabsyn/models.py +185 -0
- katabatic/models/tabsyn/utils.py +859 -0
- katabatic/pipeline/base_pipeline.py +19 -0
- katabatic/pipeline/cross_validation/pipeline.py +24 -0
- katabatic/pipeline/evaluation_pipeline.py +278 -0
- katabatic/pipeline/train_test_split/pipeline.py +370 -0
- katabatic/utils/__init__.py +1 -0
- katabatic/utils/column_types.py +43 -0
- katabatic/utils/preprocess.py +94 -0
- katabatic/utils/split_dataset.py +133 -0
- katabatic/utils/train_test_consistency.py +50 -0
- katabatic-0.2.0.dist-info/METADATA +730 -0
- katabatic-0.2.0.dist-info/RECORD +86 -0
- katabatic-0.2.0.dist-info/WHEEL +4 -0
- katabatic-0.2.0.dist-info/licenses/LICENSE +21 -0
katabatic/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Katabatic package initializer.
|
|
3
|
+
Synthetic tabular data generation, pipelines, and evaluation.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
__version__ = version("katabatic")
|
|
10
|
+
except PackageNotFoundError:
|
|
11
|
+
__version__ = "0.0.0.dev0"
|
|
12
|
+
|
|
13
|
+
from . import models, pipeline, utils
|
|
14
|
+
|
|
15
|
+
__all__ = ["__version__", "models", "pipeline", "utils"]
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from katabatic.artifacts.base import ArtifactStore
|
|
2
|
+
from katabatic.artifacts.dataset_split import (
|
|
3
|
+
write_dataset_artifact,
|
|
4
|
+
write_dataset_artifact_presplit,
|
|
5
|
+
)
|
|
6
|
+
from katabatic.artifacts.ids import new_eval_id, new_split_id, new_train_id
|
|
7
|
+
from katabatic.artifacts.local import LocalArtifactStore
|
|
8
|
+
from katabatic.artifacts.refs import (
|
|
9
|
+
DatasetRef,
|
|
10
|
+
EvaluationRef,
|
|
11
|
+
ModelRef,
|
|
12
|
+
artifact_path_segment,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"ArtifactStore",
|
|
17
|
+
"LocalArtifactStore",
|
|
18
|
+
"DatasetRef",
|
|
19
|
+
"ModelRef",
|
|
20
|
+
"EvaluationRef",
|
|
21
|
+
"artifact_path_segment",
|
|
22
|
+
"write_dataset_artifact",
|
|
23
|
+
"write_dataset_artifact_presplit",
|
|
24
|
+
"new_split_id",
|
|
25
|
+
"new_train_id",
|
|
26
|
+
"new_eval_id",
|
|
27
|
+
]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ArtifactStore(ABC):
|
|
8
|
+
@abstractmethod
|
|
9
|
+
def save_json(self, path: str, data: dict) -> None: ...
|
|
10
|
+
|
|
11
|
+
@abstractmethod
|
|
12
|
+
def load_json(self, path: str) -> dict: ...
|
|
13
|
+
|
|
14
|
+
@abstractmethod
|
|
15
|
+
def save_bytes(self, path: str, data: bytes) -> None: ...
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def open_path(self, path: str) -> Path: ...
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def exists(self, path: str) -> bool: ...
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from katabatic.artifacts.base import ArtifactStore
|
|
9
|
+
from katabatic.artifacts.ids import new_split_id
|
|
10
|
+
from katabatic.artifacts.refs import DatasetRef
|
|
11
|
+
from katabatic.utils.split_dataset import compute_train_test_split
|
|
12
|
+
from katabatic.utils.train_test_consistency import sanity_check_train_test
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _copy_extra_assets(
|
|
16
|
+
source_dir: Path | None,
|
|
17
|
+
extra_dir: Path,
|
|
18
|
+
) -> None:
|
|
19
|
+
"""Copy info.json and TabSyn-style *.npy from source_dir into extra_dir if present."""
|
|
20
|
+
if source_dir is None or not source_dir.is_dir():
|
|
21
|
+
return
|
|
22
|
+
extra_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
info = source_dir / "info.json"
|
|
24
|
+
if info.exists():
|
|
25
|
+
shutil.copy2(info, extra_dir / "info.json")
|
|
26
|
+
for name in (
|
|
27
|
+
"X_num_train.npy",
|
|
28
|
+
"X_cat_train.npy",
|
|
29
|
+
"y_train.npy",
|
|
30
|
+
"X_num_test.npy",
|
|
31
|
+
"X_cat_test.npy",
|
|
32
|
+
"y_test.npy",
|
|
33
|
+
):
|
|
34
|
+
p = source_dir / name
|
|
35
|
+
if p.exists():
|
|
36
|
+
shutil.copy2(p, extra_dir / name)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _persist_frames_to_store(
|
|
40
|
+
store: ArtifactStore,
|
|
41
|
+
ref: DatasetRef,
|
|
42
|
+
df_train: pd.DataFrame,
|
|
43
|
+
df_test: pd.DataFrame,
|
|
44
|
+
extra_source_dir: Path | None,
|
|
45
|
+
) -> tuple[pd.Series, pd.Series]:
|
|
46
|
+
label_name = df_train.columns[-1]
|
|
47
|
+
X_train, y_train = df_train.iloc[:, :-1], df_train.iloc[:, -1]
|
|
48
|
+
X_test, y_test = df_test.iloc[:, :-1], df_test.iloc[:, -1]
|
|
49
|
+
y_train.name = label_name
|
|
50
|
+
y_test.name = label_name
|
|
51
|
+
|
|
52
|
+
train_dir = store.open_path(ref.train_relpath)
|
|
53
|
+
test_dir = store.open_path(ref.test_relpath)
|
|
54
|
+
extra_dir = store.open_path(ref.extra_relpath)
|
|
55
|
+
train_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
test_dir.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
extra_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
df_train.to_csv(train_dir / "train_full.csv", index=False)
|
|
60
|
+
df_test.to_csv(test_dir / "test_full.csv", index=False)
|
|
61
|
+
X_train.to_csv(train_dir / "x_train.csv", index=False)
|
|
62
|
+
y_train.to_csv(train_dir / "y_train.csv", index=False, header=True)
|
|
63
|
+
X_test.to_csv(test_dir / "x_test.csv", index=False)
|
|
64
|
+
y_test.to_csv(test_dir / "y_test.csv", index=False, header=True)
|
|
65
|
+
|
|
66
|
+
shutil.copy2(train_dir / "x_train.csv", extra_dir / "x_train.csv")
|
|
67
|
+
shutil.copy2(train_dir / "y_train.csv", extra_dir / "y_train.csv")
|
|
68
|
+
shutil.copy2(test_dir / "x_test.csv", extra_dir / "x_test.csv")
|
|
69
|
+
shutil.copy2(test_dir / "y_test.csv", extra_dir / "y_test.csv")
|
|
70
|
+
|
|
71
|
+
_copy_extra_assets(extra_source_dir, extra_dir)
|
|
72
|
+
return y_train, y_test
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def write_dataset_artifact(
|
|
76
|
+
store: ArtifactStore,
|
|
77
|
+
*,
|
|
78
|
+
input_csv: str | Path,
|
|
79
|
+
dataset_name: str,
|
|
80
|
+
test_size: float = 0.2,
|
|
81
|
+
seed: int = 42,
|
|
82
|
+
dataset_version: str | None = None,
|
|
83
|
+
extra_source_dir: str | Path | None = None,
|
|
84
|
+
) -> DatasetRef:
|
|
85
|
+
"""
|
|
86
|
+
Split input_csv into train/ and test/ under datasets/<name>/<version>/,
|
|
87
|
+
optional extra/ from extra_source_dir (e.g. TabSyn npy + info.json).
|
|
88
|
+
"""
|
|
89
|
+
input_csv = Path(input_csv)
|
|
90
|
+
version = dataset_version or new_split_id()
|
|
91
|
+
ref = DatasetRef(dataset_name=dataset_name, dataset_version=version)
|
|
92
|
+
|
|
93
|
+
df = pd.read_csv(input_csv)
|
|
94
|
+
print(f"Loaded data with shape: {df.shape}")
|
|
95
|
+
|
|
96
|
+
df_train, df_test, _, _, _, _ = compute_train_test_split(
|
|
97
|
+
df, test_size=test_size, seed=seed
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
src = Path(extra_source_dir) if extra_source_dir else input_csv.parent
|
|
101
|
+
y_train, y_test = _persist_frames_to_store(store, ref, df_train, df_test, src)
|
|
102
|
+
|
|
103
|
+
print("Train label distribution:\n", y_train.value_counts(normalize=True))
|
|
104
|
+
print("Test label distribution:\n", y_test.value_counts(normalize=True))
|
|
105
|
+
print(f"Saved dataset artifact under {ref.root_relpath}")
|
|
106
|
+
return ref
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def write_dataset_artifact_presplit(
|
|
110
|
+
store: ArtifactStore,
|
|
111
|
+
*,
|
|
112
|
+
train_csv: str | Path,
|
|
113
|
+
test_csv: str | Path,
|
|
114
|
+
dataset_name: str,
|
|
115
|
+
dataset_version: str | None = None,
|
|
116
|
+
extra_source_dir: str | Path | None = None,
|
|
117
|
+
) -> DatasetRef:
|
|
118
|
+
"""
|
|
119
|
+
Write user-provided train/test CSVs under datasets/<name>/<version>/
|
|
120
|
+
without performing a random split.
|
|
121
|
+
"""
|
|
122
|
+
train_csv = Path(train_csv)
|
|
123
|
+
test_csv = Path(test_csv)
|
|
124
|
+
version = dataset_version or new_split_id()
|
|
125
|
+
ref = DatasetRef(dataset_name=dataset_name, dataset_version=version)
|
|
126
|
+
|
|
127
|
+
df_train = pd.read_csv(train_csv)
|
|
128
|
+
df_test = pd.read_csv(test_csv)
|
|
129
|
+
print(f"Loaded presplit train {df_train.shape}, test {df_test.shape}")
|
|
130
|
+
sanity_check_train_test(df_train, df_test)
|
|
131
|
+
|
|
132
|
+
src = Path(extra_source_dir) if extra_source_dir else train_csv.parent
|
|
133
|
+
y_train, y_test = _persist_frames_to_store(store, ref, df_train, df_test, src)
|
|
134
|
+
|
|
135
|
+
print("Train label distribution:\n", y_train.value_counts(normalize=True))
|
|
136
|
+
print("Test label distribution:\n", y_test.value_counts(normalize=True))
|
|
137
|
+
print(f"Saved dataset artifact under {ref.root_relpath}")
|
|
138
|
+
return ref
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import UTC, datetime
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _utc_stamp() -> str:
|
|
7
|
+
return datetime.now(UTC).strftime("%Y%m%d-%H%M%S")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def new_split_id() -> str:
|
|
11
|
+
return f"split-{_utc_stamp()}"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def new_train_id() -> str:
|
|
15
|
+
return f"train-{_utc_stamp()}"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def new_eval_id() -> str:
|
|
19
|
+
return f"eval-{_utc_stamp()}"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from katabatic.artifacts.base import ArtifactStore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LocalArtifactStore(ArtifactStore):
|
|
10
|
+
def __init__(self, root: str | Path = "artifacts") -> None:
|
|
11
|
+
self.root = Path(root)
|
|
12
|
+
|
|
13
|
+
def _full(self, path: str) -> Path:
|
|
14
|
+
return self.root / path
|
|
15
|
+
|
|
16
|
+
def save_json(self, path: str, data: dict) -> None:
|
|
17
|
+
full = self._full(path)
|
|
18
|
+
full.parent.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
full.write_text(json.dumps(data, indent=2), encoding="utf-8")
|
|
20
|
+
|
|
21
|
+
def load_json(self, path: str) -> dict:
|
|
22
|
+
return json.loads(self._full(path).read_text(encoding="utf-8"))
|
|
23
|
+
|
|
24
|
+
def save_bytes(self, path: str, data: bytes) -> None:
|
|
25
|
+
full = self._full(path)
|
|
26
|
+
full.parent.mkdir(parents=True, exist_ok=True)
|
|
27
|
+
full.write_bytes(data)
|
|
28
|
+
|
|
29
|
+
def open_path(self, path: str) -> Path:
|
|
30
|
+
return self._full(path)
|
|
31
|
+
|
|
32
|
+
def exists(self, path: str) -> bool:
|
|
33
|
+
return self._full(path).exists()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def artifact_path_segment(name: str) -> str:
|
|
8
|
+
"""
|
|
9
|
+
Sanitize a logical name for use inside artifact directory / file segments.
|
|
10
|
+
Allows lowercase letters, digits, hyphen; coerces other chars to hyphen.
|
|
11
|
+
"""
|
|
12
|
+
s = name.strip().lower()
|
|
13
|
+
s = re.sub(r"[^a-z0-9-]+", "-", s)
|
|
14
|
+
s = re.sub(r"-{2,}", "-", s).strip("-")
|
|
15
|
+
return s or "unnamed"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class DatasetRef:
|
|
20
|
+
dataset_name: str
|
|
21
|
+
dataset_version: str
|
|
22
|
+
"""Root relative to artifact store: datasets/<name>/<version>"""
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def root_relpath(self) -> str:
|
|
26
|
+
return f"datasets/{self.dataset_name}/{self.dataset_version}"
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def train_relpath(self) -> str:
|
|
30
|
+
return f"{self.root_relpath}/train"
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def test_relpath(self) -> str:
|
|
34
|
+
return f"{self.root_relpath}/test"
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def extra_relpath(self) -> str:
|
|
38
|
+
return f"{self.root_relpath}/extra"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass(frozen=True)
|
|
42
|
+
class ModelRef:
|
|
43
|
+
model_name: str
|
|
44
|
+
dataset_name: str
|
|
45
|
+
dataset_version: str
|
|
46
|
+
train_run_id: str
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def root_relpath(self) -> str:
|
|
50
|
+
m = artifact_path_segment(self.model_name)
|
|
51
|
+
d = artifact_path_segment(self.dataset_name)
|
|
52
|
+
return f"models/{m}_{d}_{self.train_run_id}"
|
|
53
|
+
|
|
54
|
+
@property
|
|
55
|
+
def state_relpath(self) -> str:
|
|
56
|
+
return f"{self.root_relpath}/state"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def synthetic_relpath(self) -> str:
|
|
60
|
+
return f"{self.root_relpath}/synthetic"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class EvaluationRef:
|
|
65
|
+
evaluation_type: str
|
|
66
|
+
eval_run_id: str
|
|
67
|
+
model_name: str
|
|
68
|
+
dataset_name: str
|
|
69
|
+
dataset_version: str
|
|
70
|
+
train_run_id: str
|
|
71
|
+
test_dataset_version: str
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def root_relpath(self) -> str:
|
|
75
|
+
"""Mirrors :attr:`ModelRef.root_relpath` stem under ``evaluations/``."""
|
|
76
|
+
m = artifact_path_segment(self.model_name)
|
|
77
|
+
d = artifact_path_segment(self.dataset_name)
|
|
78
|
+
return f"evaluations/{m}_{d}_{self.train_run_id}"
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def metrics_relpath(self) -> str:
|
|
82
|
+
et = artifact_path_segment(self.evaluation_type)
|
|
83
|
+
return f"{self.root_relpath}/{et}_metrics.json"
|
|
84
|
+
|
|
85
|
+
@property
|
|
86
|
+
def report_relpath(self) -> str:
|
|
87
|
+
et = artifact_path_segment(self.evaluation_type)
|
|
88
|
+
return f"{self.root_relpath}/{et}_report.csv"
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _find_models_dict_bounds(text: str) -> tuple[int, int, str]:
|
|
7
|
+
"""
|
|
8
|
+
Return (open_brace_idx, close_brace_idx, indent) for the _models dict.
|
|
9
|
+
Uses brace counting and skips over strings.
|
|
10
|
+
"""
|
|
11
|
+
m = re.search(r"_models\s*:\s*Dict\[[^\]]*\]\s*=\s*{", text)
|
|
12
|
+
if not m:
|
|
13
|
+
m = re.search(r"_models\s*=\s*{", text)
|
|
14
|
+
if not m:
|
|
15
|
+
raise ValueError("Could not find `_models = { ... }` in registry.py")
|
|
16
|
+
|
|
17
|
+
open_idx = text.find("{", m.start())
|
|
18
|
+
if open_idx == -1:
|
|
19
|
+
raise ValueError("Could not find opening `{` for _models dictionary")
|
|
20
|
+
|
|
21
|
+
line_start = text.rfind("\n", 0, open_idx) + 1
|
|
22
|
+
indent = re.match(r"[ \t]*", text[line_start:open_idx]).group(0)
|
|
23
|
+
|
|
24
|
+
depth = 0
|
|
25
|
+
i = open_idx
|
|
26
|
+
in_str: str | None = None
|
|
27
|
+
escaped = False
|
|
28
|
+
while i < len(text):
|
|
29
|
+
ch = text[i]
|
|
30
|
+
if in_str:
|
|
31
|
+
if escaped:
|
|
32
|
+
escaped = False
|
|
33
|
+
elif ch == "\\":
|
|
34
|
+
escaped = True
|
|
35
|
+
elif ch == in_str:
|
|
36
|
+
in_str = None
|
|
37
|
+
else:
|
|
38
|
+
if ch in ("'", '"'):
|
|
39
|
+
in_str = ch
|
|
40
|
+
elif ch == "{":
|
|
41
|
+
depth += 1
|
|
42
|
+
elif ch == "}":
|
|
43
|
+
depth -= 1
|
|
44
|
+
if depth == 0:
|
|
45
|
+
return open_idx, i, indent
|
|
46
|
+
i += 1
|
|
47
|
+
|
|
48
|
+
raise ValueError("Unbalanced braces in _models dictionary")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _serialize_list_str(xs: list[str]) -> str:
|
|
52
|
+
return "[" + ", ".join(repr(s) for s in xs) + "]"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _find_project_root() -> Path:
|
|
56
|
+
"""Return repo root when run from a checkout, else cwd."""
|
|
57
|
+
for candidate in [Path.cwd(), *Path.cwd().parents]:
|
|
58
|
+
if (candidate / "pyproject.toml").is_file() and (
|
|
59
|
+
candidate / "katabatic"
|
|
60
|
+
).is_dir():
|
|
61
|
+
return candidate
|
|
62
|
+
return Path.cwd()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def init_model(model_name: str, dependencies: list[str] | None = None) -> None:
|
|
66
|
+
"""Initialize a new model structure aligned with the base `Model` API.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
model_name: Name of the model to create (e.g. "Rlig" or "rlig")
|
|
70
|
+
dependencies: Optional list of Python package dependencies for the model.
|
|
71
|
+
These will be embedded in the subclass' get_required_dependencies().
|
|
72
|
+
"""
|
|
73
|
+
dir_name = re.sub(r"(?<!^)(?=[A-Z])", "_", model_name).lower()
|
|
74
|
+
if not re.fullmatch(r"[a-z][a-z0-9_]*", dir_name):
|
|
75
|
+
raise ValueError(
|
|
76
|
+
"Invalid model name. Use letters, numbers and underscores; start with a letter."
|
|
77
|
+
)
|
|
78
|
+
class_name = "".join(word.capitalize() for word in dir_name.split("_"))
|
|
79
|
+
|
|
80
|
+
from katabatic.models.registry import ModelRegistry
|
|
81
|
+
|
|
82
|
+
models_dir = Path(__file__).parents[2] / "models"
|
|
83
|
+
new_model_dir = models_dir / dir_name
|
|
84
|
+
|
|
85
|
+
if new_model_dir.exists():
|
|
86
|
+
raise ValueError(f"Model directory {dir_name} already exists")
|
|
87
|
+
|
|
88
|
+
if dir_name in ModelRegistry.get_available_models():
|
|
89
|
+
raise ValueError(f"Model {dir_name} is already registered")
|
|
90
|
+
|
|
91
|
+
new_model_dir.mkdir(parents=True)
|
|
92
|
+
|
|
93
|
+
# __init__.py
|
|
94
|
+
(new_model_dir / "__init__.py").write_text(
|
|
95
|
+
f'''"""Module for {class_name} model."""
|
|
96
|
+
from .models import {class_name}
|
|
97
|
+
|
|
98
|
+
__all__ = ["{class_name}"]
|
|
99
|
+
'''
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
deps = dependencies or []
|
|
103
|
+
|
|
104
|
+
(new_model_dir / "models.py").write_text(
|
|
105
|
+
f'''"""Implementation of {class_name} model."""
|
|
106
|
+
from typing import Any, Union
|
|
107
|
+
import numpy as np
|
|
108
|
+
import pandas as pd
|
|
109
|
+
|
|
110
|
+
from katabatic.models.base_model import Model
|
|
111
|
+
from . import utils as _utils
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class {class_name}(Model):
|
|
115
|
+
"""Implementation of {class_name} model."""
|
|
116
|
+
|
|
117
|
+
def __init__(self):
|
|
118
|
+
super().__init__()
|
|
119
|
+
# Validate runtime dependencies early to fail fast
|
|
120
|
+
self.check_dependencies()
|
|
121
|
+
|
|
122
|
+
@classmethod
|
|
123
|
+
def get_required_dependencies(cls) -> list[str]:
|
|
124
|
+
# Generated by init_model; adjust as your model evolves.
|
|
125
|
+
return {deps!r}
|
|
126
|
+
|
|
127
|
+
def train(self, *args, **kwargs) -> "Model":
|
|
128
|
+
\"\"\"Train the model on the given data.\"\"\"
|
|
129
|
+
raise NotImplementedError
|
|
130
|
+
|
|
131
|
+
def evaluate(self, *args, **kwargs) -> float:
|
|
132
|
+
\"\"\"Evaluate the model performance and return a score.\"\"\"
|
|
133
|
+
raise NotImplementedError
|
|
134
|
+
|
|
135
|
+
def sample(self, *args, **kwargs) -> Union[np.ndarray, pd.DataFrame]:
|
|
136
|
+
\"\"\"Generate synthetic samples.\"\"\"
|
|
137
|
+
raise NotImplementedError
|
|
138
|
+
'''
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
(new_model_dir / "utils.py").write_text(
|
|
142
|
+
f'''"""Utility functions for {class_name} model."""
|
|
143
|
+
|
|
144
|
+
# Add your utility functions here.
|
|
145
|
+
'''
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
registry_path = models_dir / "registry.py"
|
|
149
|
+
registry_content = registry_path.read_text()
|
|
150
|
+
|
|
151
|
+
open_idx, close_idx, indent = _find_models_dict_bounds(registry_content)
|
|
152
|
+
|
|
153
|
+
entry_indent = indent + " "
|
|
154
|
+
new_entry = (
|
|
155
|
+
f"{entry_indent}'{dir_name}': {{\n"
|
|
156
|
+
f"{entry_indent} 'module': 'katabatic.models.{dir_name}.models',\n"
|
|
157
|
+
f"{entry_indent} 'class': '{class_name}',\n"
|
|
158
|
+
f"{entry_indent} 'dependencies': {_serialize_list_str(deps)},\n"
|
|
159
|
+
f"{entry_indent} 'extra': '{dir_name}'\n"
|
|
160
|
+
f"{entry_indent}}}"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Preserve the indented closing brace line (close_idx points at `}` only).
|
|
164
|
+
line_start = registry_content.rfind("\n", 0, close_idx) + 1
|
|
165
|
+
before = registry_content[:line_start].rstrip()
|
|
166
|
+
after = registry_content[line_start:]
|
|
167
|
+
|
|
168
|
+
needs_comma = not before.endswith("{")
|
|
169
|
+
if needs_comma and not before.endswith(","):
|
|
170
|
+
before = before + ","
|
|
171
|
+
|
|
172
|
+
if not before.endswith("\n"):
|
|
173
|
+
before = before + "\n"
|
|
174
|
+
|
|
175
|
+
new_registry = before + new_entry + ",\n" + after
|
|
176
|
+
|
|
177
|
+
registry_path.write_text(new_registry)
|
|
178
|
+
|
|
179
|
+
scripts_dir = _find_project_root() / "scripts"
|
|
180
|
+
scripts_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
setup_script_path = scripts_dir / f"setup_{dir_name}.sh"
|
|
182
|
+
setup_script_path.write_text(
|
|
183
|
+
f"""#!/bin/bash
|
|
184
|
+
# Setup script for {class_name} model dependencies
|
|
185
|
+
|
|
186
|
+
pip install katabatic[{dir_name}]
|
|
187
|
+
"""
|
|
188
|
+
)
|
|
189
|
+
os.chmod(setup_script_path, 0o755) # nosec B103: executable bit on generated setup script, intended
|
|
190
|
+
|
|
191
|
+
print(f"""Successfully:
|
|
192
|
+
1. Created {class_name} model structure in {new_model_dir}
|
|
193
|
+
2. Added model to registry
|
|
194
|
+
3. Created setup script at {setup_script_path}
|
|
195
|
+
|
|
196
|
+
Next steps:
|
|
197
|
+
1. Add your model's dependencies to pyproject.toml in:
|
|
198
|
+
- [tool.poetry.dependencies] (as optional = true)
|
|
199
|
+
- [tool.poetry.extras] {dir_name} = [...]
|
|
200
|
+
2. Implement train/evaluate/sample in {new_model_dir}/models.py
|
|
201
|
+
3. Add any utility functions in {new_model_dir}/utils.py""")
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Pin Jupyter kernelspec for notebooks to the project virtualenv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
DEFAULT_KERNEL = "katabatic-venv"
|
|
11
|
+
DEFAULT_DISPLAY = "Python (Katabatic .venv)"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def find_project_root(start: Path | None = None) -> Path:
|
|
15
|
+
"""Walk up from *start* (default cwd) to find the Katabatic project root."""
|
|
16
|
+
for candidate in [start or Path.cwd(), *(start or Path.cwd()).parents]:
|
|
17
|
+
if (candidate / "pyproject.toml").is_file() and (
|
|
18
|
+
candidate / "katabatic"
|
|
19
|
+
).is_dir():
|
|
20
|
+
return candidate
|
|
21
|
+
return start or Path.cwd()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def install_kernelspec(
|
|
25
|
+
*,
|
|
26
|
+
venv_prefix: Path,
|
|
27
|
+
python_exe: Path,
|
|
28
|
+
name: str,
|
|
29
|
+
display: str,
|
|
30
|
+
) -> Path:
|
|
31
|
+
subprocess.run(
|
|
32
|
+
[
|
|
33
|
+
str(python_exe),
|
|
34
|
+
"-m",
|
|
35
|
+
"ipykernel",
|
|
36
|
+
"install",
|
|
37
|
+
"--prefix",
|
|
38
|
+
str(venv_prefix),
|
|
39
|
+
"--name",
|
|
40
|
+
name,
|
|
41
|
+
"--display-name",
|
|
42
|
+
display,
|
|
43
|
+
],
|
|
44
|
+
check=True,
|
|
45
|
+
)
|
|
46
|
+
return venv_prefix / "share" / "jupyter" / "kernels" / name
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def patch_notebook(path: Path, *, name: str, display: str) -> None:
|
|
50
|
+
nb = json.loads(path.read_text(encoding="utf-8"))
|
|
51
|
+
meta = nb.setdefault("metadata", {})
|
|
52
|
+
meta["kernelspec"] = {
|
|
53
|
+
"display_name": display,
|
|
54
|
+
"language": "python",
|
|
55
|
+
"name": name,
|
|
56
|
+
}
|
|
57
|
+
path.write_text(json.dumps(nb, indent=1), encoding="utf-8")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_pin_notebook_kernel(
|
|
61
|
+
notebooks: list[str | Path],
|
|
62
|
+
*,
|
|
63
|
+
repo_root: Path | None = None,
|
|
64
|
+
kernel_name: str = DEFAULT_KERNEL,
|
|
65
|
+
display_name: str = DEFAULT_DISPLAY,
|
|
66
|
+
) -> int:
|
|
67
|
+
root = (repo_root or find_project_root()).resolve()
|
|
68
|
+
py = root / ".venv" / "bin" / "python"
|
|
69
|
+
if not py.is_file():
|
|
70
|
+
print(f"error: expected venv python at {py}", file=sys.stderr)
|
|
71
|
+
print(" run: poetry install", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
print(f"Installing kernelspec {kernel_name!r} into {root / '.venv'} ...")
|
|
75
|
+
dest = install_kernelspec(
|
|
76
|
+
venv_prefix=root / ".venv",
|
|
77
|
+
python_exe=py,
|
|
78
|
+
name=kernel_name,
|
|
79
|
+
display=display_name,
|
|
80
|
+
)
|
|
81
|
+
print(f" -> {dest}")
|
|
82
|
+
|
|
83
|
+
patched = 0
|
|
84
|
+
for nb_path in notebooks:
|
|
85
|
+
nb = Path(nb_path).resolve()
|
|
86
|
+
if nb.suffix != ".ipynb":
|
|
87
|
+
print(f"skip (not .ipynb): {nb}", file=sys.stderr)
|
|
88
|
+
continue
|
|
89
|
+
patch_notebook(nb, name=kernel_name, display=display_name)
|
|
90
|
+
print(f"Patched kernelspec in {nb}")
|
|
91
|
+
patched += 1
|
|
92
|
+
|
|
93
|
+
if patched == 0:
|
|
94
|
+
print("error: no notebooks were patched", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
|
|
97
|
+
print("\nIn Cursor: reload window, then Select Kernel → pick", repr(display_name))
|
|
98
|
+
print(
|
|
99
|
+
"If the picker still spins, use Command Palette → Python: Select Interpreter → .venv"
|
|
100
|
+
)
|
|
101
|
+
return 0
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from katabatic.artifacts.local import LocalArtifactStore
|
|
6
|
+
from katabatic.datasets.compatibility import check_dataset_for_model
|
|
7
|
+
from katabatic.datasets.registry import DatasetRegistry
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def register_dataset_cli(
|
|
11
|
+
dataset_name: str,
|
|
12
|
+
csv_path: str,
|
|
13
|
+
*,
|
|
14
|
+
target_column: str | None = None,
|
|
15
|
+
artifact_root: str | None = None,
|
|
16
|
+
check_model: str | None = None,
|
|
17
|
+
) -> None:
|
|
18
|
+
root = Path(artifact_root or "artifacts")
|
|
19
|
+
store = LocalArtifactStore(root)
|
|
20
|
+
reg = DatasetRegistry(store)
|
|
21
|
+
entry = reg.register_if_absent(dataset_name, csv_path, target_column=target_column)
|
|
22
|
+
print(
|
|
23
|
+
f"Registered dataset {dataset_name!r} at {store.root / 'registry' / 'datasets.json'}"
|
|
24
|
+
)
|
|
25
|
+
print(
|
|
26
|
+
f" task={entry['task']!r}, n_rows={entry['n_rows']}, target={entry['target_column']!r}"
|
|
27
|
+
)
|
|
28
|
+
if check_model:
|
|
29
|
+
ok, msg = check_dataset_for_model(entry, check_model)
|
|
30
|
+
print(
|
|
31
|
+
f" compatibility with {check_model!r}: {msg}" + ("" if ok else " (failed)")
|
|
32
|
+
)
|
|
33
|
+
if not ok:
|
|
34
|
+
raise SystemExit(1)
|