modelmark 0.1.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.
modelmark/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Reusable testing framework."""
2
+
3
+ __version__ = "0.1.0"
modelmark/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ import sys
2
+ from modelmark import run
3
+
4
+ if __name__ == "__main__":
5
+ sys.exit(run())
@@ -0,0 +1,116 @@
1
+ from __future__ import annotations
2
+
3
+ import torch
4
+ from torch.utils.data import Dataset
5
+
6
+ import copy
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ from modelmark.common.utils import load_config
11
+ config = load_config()
12
+
13
+ class CausalDataset(Dataset):
14
+ """
15
+ Sliding-window dataset for ETT.
16
+
17
+ Returns:
18
+ x: [seq_len, num_features]
19
+ y: [pred_len, 1]
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ data_config: dict,
25
+ seq_len: int,
26
+ pred_len: int,
27
+ split: str = "train",
28
+ mean: np.ndarray | None = None,
29
+ std: np.ndarray | None = None,
30
+ ):
31
+ super().__init__()
32
+
33
+ self.seq_len = seq_len
34
+ self.pred_len = pred_len
35
+
36
+ # Extract config
37
+ data_path = config.data_path + data_config["path"]
38
+ features = copy.deepcopy(data_config["input_features"])
39
+ targets = data_config["output_features"]
40
+ features.extend([item for item in targets if item not in features])
41
+ train_ratio = data_config["train_ratio"]
42
+ val_ratio = data_config["val_ratio"]
43
+
44
+ # Load, extract, convert
45
+ df = pd.read_csv(data_path)
46
+
47
+ df = df[features]
48
+ data = df.values.astype(np.float32)
49
+ self.input_indices = torch.tensor(df.columns.get_indexer(data_config["input_features"]), dtype=torch.int)
50
+ self.output_indices = torch.tensor(df.columns.get_indexer(data_config["output_features"]), dtype=torch.int)
51
+
52
+ n = len(data)
53
+
54
+ train_end = int(n * train_ratio)
55
+ val_end = int(n * (train_ratio + val_ratio))
56
+
57
+ if split == "train":
58
+ start = 0
59
+ end = train_end
60
+ elif split == "val":
61
+ start = train_end
62
+ end = val_end
63
+ elif split == "test":
64
+ start = val_end
65
+ end = n
66
+ else:
67
+ raise ValueError(f"Unknown split: {split}")
68
+
69
+ # Save scaling information
70
+ if split == "train":
71
+ self.mean = data[start:train_end].mean(axis=0)
72
+ self.std = data[start:train_end].std(axis=0)
73
+
74
+ # Prevent division by zero
75
+ self.std[self.std < 1e-8] = 1.0
76
+ else:
77
+ if mean is None or std is None:
78
+ raise ValueError(
79
+ "Validation/test datasets need training mean and std."
80
+ )
81
+
82
+ self.mean = mean
83
+ self.std = std
84
+
85
+ # Scale using TRAIN statistics
86
+ data = (data - self.mean) / self.std
87
+
88
+ # Keep only the split region
89
+ self.data = data[start:end]
90
+
91
+ # Number of valid windows
92
+ self.length = len(self.data) - seq_len - pred_len + 1
93
+
94
+ if self.length <= 0:
95
+ raise ValueError(
96
+ f"Split '{split}' is too short for "
97
+ f"seq_len={seq_len}, pred_len={pred_len}"
98
+ )
99
+
100
+ def __len__(self):
101
+ return self.length
102
+
103
+ def __getitem__(self, idx):
104
+ x_start = idx
105
+ x_end = x_start + self.seq_len
106
+
107
+ y_start = x_end
108
+ y_end = y_start + self.pred_len
109
+
110
+ x = self.data[x_start:x_end, self.input_indices]
111
+ y = self.data[y_start:y_end, self.output_indices]
112
+
113
+ return (
114
+ torch.from_numpy(x),
115
+ torch.from_numpy(y),
116
+ )
@@ -0,0 +1,88 @@
1
+ """Download time-series datasets to a local directory.
2
+
3
+ Supports:
4
+ - ETT (ETTh1, ETTh2, ETTm1, ETTm2) — from the ETDataset GitHub repo
5
+ - Air Quality (UCI Repository)
6
+ - Weather (Max Planck Institute / Jena climate dataset)
7
+ """
8
+
9
+ import argparse
10
+ import shutil
11
+ import urllib.request
12
+ import zipfile
13
+ from pathlib import Path
14
+
15
+ import logging
16
+ logger = logging.getLogger(__name__)
17
+ from rich.console import Console
18
+ console = Console()
19
+
20
+ DATASETS = {
21
+ "ett": {
22
+ "dir": "data/ett",
23
+ "kind": "multi_csv",
24
+ "base_url": "https://raw.githubusercontent.com/zhouhaoyi/ETDataset/main/ETT-small",
25
+ "files": ["ETTh1", "ETTh2", "ETTm1", "ETTm2"],
26
+ },
27
+ "air_quality": {
28
+ "dir": "data/air_quality",
29
+ "kind": "zip",
30
+ "url": "https://archive.ics.uci.edu/static/public/360/air+quality.zip",
31
+ "zip_name": "AirQualityUCI.zip",
32
+ },
33
+ "weather": {
34
+ "dir": "data/weather",
35
+ "kind": "zip",
36
+ "url": "https://storage.googleapis.com/tensorflow/tf-keras-datasets/jena_climate_2009_2016.csv.zip",
37
+ "zip_name": "jena_climate_2009_2016.csv.zip",
38
+ },
39
+ }
40
+
41
+
42
+ def download_ett(cfg: dict, out_dir: Path) -> None:
43
+ out_dir.mkdir(parents=True, exist_ok=True)
44
+ for name in cfg["files"]:
45
+ url = f"{cfg['base_url']}/{name}.csv"
46
+ path = out_dir / f"{name}.csv"
47
+ if path.exists():
48
+ logger.info(f"{name}.csv already exists, skipping.")
49
+ continue
50
+ logger.info(f"Downloading {name}...")
51
+ console.print(f"Downloading {name}...")
52
+ urllib.request.urlretrieve(url, path)
53
+ logger.info(f"Saved to {path}")
54
+ console.print(f"Saved to {path}", style="green")
55
+
56
+
57
+ def download_zip_dataset(cfg: dict, out_dir: Path) -> None:
58
+ out_dir.mkdir(parents=True, exist_ok=True)
59
+ zip_path = out_dir / cfg["zip_name"]
60
+
61
+ if not zip_path.exists():
62
+ logger.info(f"Downloading {cfg['zip_name']}...")
63
+ console.print(f"Downloading {cfg['zip_name']}...")
64
+ urllib.request.urlretrieve(cfg["url"], zip_path)
65
+ logger.info(f"Saved to {zip_path}")
66
+ console.print(f"Saved to {zip_path}", style="green")
67
+ else:
68
+ logger.info(f"{cfg['zip_name']} already exists, skipping download.")
69
+ console.print(f"{cfg['zip_name']} already exists, skipping download.", style="yellow")
70
+
71
+ logger.info(f"Extracting {zip_path.name}...")
72
+ console.print(f"Extracting {zip_path.name}...")
73
+ with zipfile.ZipFile(zip_path) as zf:
74
+ zf.extractall(out_dir)
75
+ console.print(f"Extracted to {out_dir}", color="green")
76
+
77
+ def download_dataset(name: str) -> None:
78
+ if name not in DATASETS:
79
+ valid = ", ".join(DATASETS)
80
+ raise ValueError(f"Unknown dataset '{name}'. Choose from: {valid}")
81
+
82
+ cfg = DATASETS[name]
83
+ out_dir = Path(cfg["dir"])
84
+
85
+ if cfg["kind"] == "multi_csv":
86
+ download_ett(cfg, out_dir)
87
+ elif cfg["kind"] == "zip":
88
+ download_zip_dataset(cfg, out_dir)
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ from modelmark.common.dataset import CausalDataset
4
+ from torch.utils.data import DataLoader
5
+
6
+ import logging
7
+ logger = logging.getLogger(__name__)
8
+ from modelmark.common.utils import load_config
9
+ config = load_config()
10
+
11
+ class Loader:
12
+
13
+ def __init__(self, file_config : dict, context_size : int):
14
+
15
+ self.get_dataloaders(file_config, context_size)
16
+ logger.debug(f"Dataset len stats: train={len(self.train_loader)} val={len(self.val_loader)} test={len(self.test_loader)}")
17
+
18
+ def get_dataloaders(self, file_config : dict, context_size : int) -> tuple[DataLoader, DataLoader, DataLoader]:
19
+ """Load the data, pack to datasets, create the loaders and return them"""
20
+
21
+ train_ds = CausalDataset(data_config = file_config,
22
+ seq_len = context_size,
23
+ pred_len = context_size,
24
+ split = "train")
25
+
26
+ val_ds = CausalDataset(data_config = file_config,
27
+ seq_len = context_size,
28
+ pred_len = context_size,
29
+ split = "val",
30
+ mean = train_ds.mean,
31
+ std = train_ds.std)
32
+
33
+ test_ds = CausalDataset(data_config = file_config,
34
+ seq_len = context_size,
35
+ pred_len = context_size,
36
+ split = "test",
37
+ mean = train_ds.mean,
38
+ std = train_ds.std)
39
+
40
+ self.train_loader = DataLoader(
41
+ train_ds,
42
+ batch_size = config.test_config["optim"]["batch_size"],
43
+ shuffle = True,
44
+ num_workers = 0,
45
+ pin_memory = True,
46
+ )
47
+
48
+ self.val_loader = DataLoader(
49
+ val_ds,
50
+ batch_size = config.test_config["optim"]["batch_size"],
51
+ shuffle = False,
52
+ num_workers = 0,
53
+ pin_memory = True,
54
+ )
55
+
56
+ self.test_loader = DataLoader(
57
+ test_ds,
58
+ batch_size = config.test_config["optim"]["batch_size"],
59
+ shuffle = False,
60
+ num_workers = 0,
61
+ pin_memory = True,
62
+ )
63
+
@@ -0,0 +1,34 @@
1
+ import logging
2
+ import sys
3
+ from typing import Optional
4
+
5
+ def setup_logging(log_format: Optional[str] = None) -> None:
6
+ """
7
+ Configure logging for the entire application.
8
+
9
+ Args:
10
+ log_format: Optional custom format string. If None, a default format is used.
11
+ """
12
+
13
+ if log_format is None:
14
+ log_format = "%(asctime)s [%(levelname)s] [%(name)s] - %(message)s"
15
+
16
+ logging.basicConfig(
17
+ level = logging.DEBUG,
18
+ filemode = 'w',
19
+ filename='modelmark_files/modelmark.log',
20
+ format = log_format,
21
+ datefmt = '%H:%M:%S',
22
+ )
23
+
24
+ def get_logger(name: str) -> logging.Logger:
25
+ """
26
+ Convenience function to get a logger for a module.
27
+
28
+ Args:
29
+ name: Usually __name__.
30
+
31
+ Returns:
32
+ A configured logger instance.
33
+ """
34
+ return logging.getLogger(name)
@@ -0,0 +1,134 @@
1
+ import argparse
2
+ import sys
3
+ import shutil
4
+ import pandas as pd
5
+
6
+ from modelmark.common.downloader import download_dataset
7
+ import modelmark.constants as constants
8
+
9
+ import logging
10
+ logger = logging.getLogger(__name__)
11
+ from rich.console import Console
12
+ console = Console()
13
+
14
+ class Parser:
15
+
16
+ def __init__(self):
17
+ """Parse the arguments"""
18
+
19
+ # Create parser
20
+ self.parser = argparse.ArgumentParser(
21
+ formatter_class=argparse.RawTextHelpFormatter,
22
+ description = constants.PARSER_DESC
23
+ )
24
+
25
+ # Add arguments
26
+ self.parser.add_argument(
27
+ "-t", "--task",
28
+ type = str,
29
+ default = "help",
30
+ help = constants.PARSER_HELP
31
+ )
32
+
33
+ # Run without args / help request - print help
34
+ if len(sys.argv) == 1:
35
+ self.parser.print_help()
36
+ sys.exit(1)
37
+
38
+ # Parse argument values
39
+ self.args = self.parser.parse_args()
40
+
41
+ # Setup other
42
+ pd.set_option('display.colheader_justify', 'center')
43
+
44
+ def run(self):
45
+ """Select the task and run"""
46
+
47
+ # Config initialization #
48
+ if self.args.task == "init":
49
+ self.init_config()
50
+ return 0
51
+
52
+ # Dataset download #
53
+ if self.args.task == "load":
54
+ download_dataset("ett")
55
+ return 0
56
+
57
+ # Run the test #
58
+ if self.args.task == "run":
59
+ return None
60
+
61
+ # Clean the config folder #
62
+ if self.args.task == "clear":
63
+ self.clear_config()
64
+ return 0
65
+
66
+ # Reset the modelmark config #
67
+ if self.args.task == "reset":
68
+ self.reset_config()
69
+ return 0
70
+
71
+ # Print the modelmark usage #
72
+ if self.args.task == "help":
73
+ self.parser.print_help()
74
+ return 0
75
+
76
+ # Check if task is correct #
77
+ if self.args.task != "run":
78
+ logger.error(f"Unknown task: {self.args.task}, run 'modelmark' for more info")
79
+ console.print(f"Unknown task: {self.args.task}, run 'modelmark' for more info", style="red")
80
+ return 1
81
+
82
+ # Otherwise return error
83
+ return 1
84
+
85
+ def init_config(self):
86
+ "Copy the config example file to the user dir"
87
+
88
+ user_config_dir = constants.USER_CONFIG_DIR
89
+ user_models_dir = constants.USER_MODELS_DIR
90
+
91
+ package_config = constants.PACKAGE_CONFIG_PATH
92
+ user_config = constants.USER_CONFIG_PATH
93
+ package_model = constants.PACKAGE_MODEL_PATH
94
+ user_model = constants.USER_MODEL_PATH
95
+
96
+ # Make the dirs
97
+ user_config_dir.mkdir(parents=True, exist_ok=True)
98
+ user_models_dir.mkdir(parents=True, exist_ok=True)
99
+
100
+ if not user_config.exists():
101
+ shutil.copy(package_config, user_config)
102
+
103
+ logger.info(f"Customizable config created at: {user_config}")
104
+ console.print(f"Customizable config created at: {user_config}")
105
+
106
+ shutil.copy(package_model, user_model)
107
+
108
+ logger.info(f"Model example file created at: {user_model}")
109
+ console.print(f"Model example file created at: {user_model}")
110
+
111
+ console.print(f"Initialization complete.", style="green")
112
+ else:
113
+
114
+ console.print(f"config.py already exists", style="yellow")
115
+ self.parser.print_help()
116
+
117
+ def clear_config(self):
118
+ """Remove the user config dir"""
119
+ user_config_path = constants.USER_CONFIG_DIR
120
+ user_example_model_path = constants.USER_MODEL_PATH
121
+
122
+ if user_config_path.exists():
123
+ shutil.rmtree(user_config_path)
124
+ logger.info(f"Folder {user_config_path} removed.")
125
+ console.print(f"Folder {user_config_path} removed.")
126
+ if user_example_model_path.exists():
127
+ user_example_model_path.unlink(missing_ok=True)
128
+ logger.info(f"File {user_example_model_path} removed.")
129
+ console.print(f"File {user_example_model_path} removed.")
130
+
131
+ def reset_config(self):
132
+ self.clear_config()
133
+ self.init_config()
134
+