melite 0.1.11__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.
melite/__init__.py ADDED
@@ -0,0 +1,34 @@
1
+ # SPDX-License-Identifier: LGPL-3.0-or-later
2
+ """MELITE — multi-model selection, cross-validation and export toolkit.
3
+
4
+ Public API
5
+ ----------
6
+ The following symbols are part of the stable public API:
7
+
8
+ from melite import Config
9
+ from melite import load_dataset
10
+ from melite import ResultManager
11
+ from melite import plot_cv_distributions
12
+ from melite import predict
13
+ from melite import __version__
14
+ """
15
+
16
+ import logging
17
+
18
+ from .config import Config
19
+ from .load_dataset import load_dataset
20
+ from .result_manager import ResultManager
21
+ from .plot_metrics import plot_cv_distributions
22
+ from .predict import predict
23
+ from .version import __version__
24
+
25
+ __all__ = [
26
+ "Config",
27
+ "load_dataset",
28
+ "ResultManager",
29
+ "plot_cv_distributions",
30
+ "predict",
31
+ "__version__",
32
+ ]
33
+
34
+ logging.getLogger(__name__).addHandler(logging.NullHandler())
melite/cli.py ADDED
@@ -0,0 +1,174 @@
1
+ # SPDX-License-Identifier: LGPL-3.0-or-later
2
+ """Unified CLI entry point for MELITE.
3
+
4
+ This module provides the ``melite`` command registered in ``pyproject.toml``
5
+ under ``[project.scripts]``. It exposes two subcommands:
6
+
7
+ - ``melite run`` — execute the full benchmarking pipeline.
8
+ - ``melite export`` — retrain a selected model and export a ``.pkl`` artifact.
9
+
10
+ Global flags (``--verbose``, ``--config``, ``--version``) are available to
11
+ all subcommands via argparse parent parsers.
12
+ """
13
+
14
+ import argparse
15
+ import logging
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ from .version import __version__
20
+
21
+ __all__ = ["main"]
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+
26
+ def _global_parser() -> argparse.ArgumentParser:
27
+ """Return a parent parser with shared flags for all subcommands."""
28
+ parent = argparse.ArgumentParser(add_help=False)
29
+ parent.add_argument(
30
+ "--verbose",
31
+ action="store_true",
32
+ help="Enable progress logging (INFO level).",
33
+ )
34
+ parent.add_argument(
35
+ "--config",
36
+ type=Path,
37
+ default=None,
38
+ metavar="PATH",
39
+ help="Path to a user TOML configuration file. Overrides defaults.",
40
+ )
41
+ return parent
42
+
43
+
44
+ def _build_parser() -> argparse.ArgumentParser:
45
+ global_parent = _global_parser()
46
+
47
+ parser = argparse.ArgumentParser(
48
+ prog="melite",
49
+ description="MELITE — multi-model selection, cross-validation and export toolkit.",
50
+ formatter_class=argparse.RawDescriptionHelpFormatter,
51
+ parents=[global_parent],
52
+ )
53
+ parser.add_argument(
54
+ "--version",
55
+ action="version",
56
+ version=f"MELITE {__version__}",
57
+ )
58
+
59
+ subparsers = parser.add_subparsers(dest="command", metavar="COMMAND")
60
+ subparsers.required = True
61
+
62
+ # ------------------------------------------------------------------ #
63
+ # melite run
64
+ # ------------------------------------------------------------------ #
65
+ run_parser = subparsers.add_parser(
66
+ "run",
67
+ help="Run the full benchmarking pipeline.",
68
+ description="Run grid search and cross-validation over all configured datasets and models.",
69
+ parents=[global_parent],
70
+ )
71
+ run_parser.add_argument(
72
+ "--smoke",
73
+ action="store_true",
74
+ help=(
75
+ "Lightweight mode: single-value grids and 3-fold CV. "
76
+ "Results are not benchmark-quality."
77
+ ),
78
+ )
79
+
80
+ # ------------------------------------------------------------------ #
81
+ # melite export
82
+ # ------------------------------------------------------------------ #
83
+ export_parser = subparsers.add_parser(
84
+ "export",
85
+ help="Export a selected model from results.csv.",
86
+ description="Retrain a selected model on all available data and save a .pkl artifact.",
87
+ parents=[global_parent],
88
+ )
89
+ export_parser.add_argument(
90
+ "--row",
91
+ type=int,
92
+ default=None,
93
+ metavar="INDEX",
94
+ help="Row index from results.csv to export without interactive prompt.",
95
+ )
96
+ export_parser.add_argument(
97
+ "--csv",
98
+ type=Path,
99
+ default=None,
100
+ metavar="PATH",
101
+ help="Path to results CSV file. Defaults to output/results.csv.",
102
+ )
103
+ export_parser.add_argument(
104
+ "--outdir",
105
+ type=Path,
106
+ default=None,
107
+ metavar="PATH",
108
+ help="Destination directory for the .pkl artifact. Defaults to output/.",
109
+ )
110
+ export_parser.add_argument(
111
+ "--force",
112
+ action="store_true",
113
+ help="Override smoke-mode export guard. Use with caution.",
114
+ )
115
+
116
+ return parser
117
+
118
+
119
+ def _configure_logging(verbose: bool) -> None:
120
+ level = logging.INFO if verbose else logging.WARNING
121
+ logging.basicConfig(
122
+ level=level,
123
+ format="%(levelname)s:%(name)s:%(message)s",
124
+ )
125
+
126
+
127
+ def _run(args: argparse.Namespace) -> None:
128
+ from .main import Main
129
+ Main(smoke=args.smoke, user_config=args.config).run()
130
+
131
+
132
+ def _export(args: argparse.Namespace) -> None:
133
+ from .config import Config
134
+ from .export_best_model import Finalizer
135
+
136
+ config = Config(user_config=args.config)
137
+ csv_path = args.csv or Path(config.PATHS["OUTPUT"]) / "results.csv"
138
+ outdir = args.outdir or Path(config.PATHS["OUTPUT"])
139
+ Finalizer(csv_path, outdir, config, row_index=args.row,
140
+ force=getattr(args, "force", False)).run()
141
+
142
+
143
+ def main() -> None:
144
+ """Entry point for the ``melite`` CLI command.
145
+
146
+ Registered in ``pyproject.toml`` as::
147
+
148
+ [project.scripts]
149
+ melite = "melite.cli:main"
150
+
151
+ Parses arguments, configures logging, and dispatches to the appropriate
152
+ subcommand handler (``_run`` or ``_export``).
153
+
154
+ Notes
155
+ -----
156
+ The ``--verbose`` flag sets the root logger to ``INFO`` level, which
157
+ exposes progress messages from all ``melite.*`` modules. Without it,
158
+ only ``WARNING`` and above are shown.
159
+ """
160
+ parser = _build_parser()
161
+ args = parser.parse_args()
162
+ _configure_logging(args.verbose)
163
+
164
+ if args.command == "run":
165
+ _run(args)
166
+ elif args.command == "export":
167
+ _export(args)
168
+ else:
169
+ parser.print_help()
170
+ sys.exit(1)
171
+
172
+
173
+ if __name__ == "__main__":
174
+ main()
melite/config.py ADDED
@@ -0,0 +1,263 @@
1
+ # SPDX-License-Identifier: LGPL-3.0-or-later
2
+ """Configuration loader for MELITE.
3
+
4
+ Reads ``melite/config_default.toml`` as the base configuration.
5
+ An optional user-supplied TOML file can override any key via deep merge.
6
+ Hyperparameter grids are defined here in Python — they are developer-facing
7
+ and not expected to change between runs.
8
+
9
+ The :class:`Config` object is the single entry point for all runtime
10
+ settings. It is designed to be instantiated without filesystem side effects;
11
+ call :meth:`Config.setup` explicitly from pipeline entry points.
12
+ """
13
+
14
+ import os
15
+ import random
16
+ import tomllib
17
+ from pathlib import Path
18
+
19
+ import numpy as np
20
+ from sklearn.model_selection import ParameterGrid
21
+
22
+ __all__ = ["Config"]
23
+
24
+ # Path to the default configuration file bundled with the package
25
+ _DEFAULT_CONFIG = Path(__file__).parent / "config_default.toml"
26
+
27
+
28
+ def _load_toml(path: Path) -> dict:
29
+ with open(path, "rb") as f:
30
+ return tomllib.load(f)
31
+
32
+
33
+ def _deep_merge(base: dict, override: dict) -> dict:
34
+ """Merge *override* into *base* recursively. Override wins on conflict."""
35
+ result = base.copy()
36
+ for key, value in override.items():
37
+ if isinstance(value, dict) and isinstance(result.get(key), dict):
38
+ result[key] = _deep_merge(result[key], value)
39
+ else:
40
+ result[key] = value
41
+ return result
42
+
43
+
44
+ class Config:
45
+ """Configuration container for MELITE.
46
+
47
+ Loads defaults from ``melite/config_default.toml``. If *user_config* is
48
+ provided, its values are merged over the defaults — user values win and
49
+ missing keys fall back to defaults.
50
+
51
+ Parameters
52
+ ----------
53
+ smoke : bool, optional
54
+ If ``True``, use reduced CV settings and single-value hyperparameter
55
+ grids for lightweight runs. Default is ``False``.
56
+ user_config : pathlib.Path or None, optional
57
+ Path to a user-supplied TOML file. Only the keys present in this file
58
+ override the defaults. Default is ``None``.
59
+
60
+ Attributes
61
+ ----------
62
+ SMOKE : bool
63
+ Whether the instance was created in smoke mode.
64
+ PATHS : dict
65
+ Dictionary with keys ``"INPUT"``, ``"DATASET"``, and ``"OUTPUT"``
66
+ mapping to the corresponding directory paths as strings.
67
+ RESULTS_FILE : str
68
+ Full path to the TXT results file (``output/results.txt`` by default).
69
+ RANDOM_STATE : int
70
+ Global random seed. Default is ``42``.
71
+ REDUCTION_TYPES : list of str
72
+ Reduction methods to benchmark (e.g. ``["PCA", "UMAP"]``).
73
+ REDUCTION_LEVELS : list of int
74
+ Variance retention levels to benchmark (e.g. ``[70, 75, 80, 85, 90, 95]``).
75
+ ACTIVE_MODELS : list of str
76
+ Model keys to include in the benchmark (e.g. ``["svc", "rf", "xgb"]``).
77
+ CV_CONFIG : dict
78
+ Cross-validation settings with keys ``n_splits``, ``n_repeats``, and
79
+ ``random_state``.
80
+ PARAM_GRID : list of dict
81
+ Raw hyperparameter grid definitions, one entry per model configuration.
82
+ PARAM_GRID_BY_MODEL : dict
83
+ Compiled :class:`~sklearn.model_selection.ParameterGrid` objects keyed
84
+ by model name (``"svc"``, ``"rf"``, ``"xgb"``).
85
+
86
+ Examples
87
+ --------
88
+ Default configuration:
89
+
90
+ >>> cfg = Config()
91
+ >>> cfg.RANDOM_STATE
92
+ 42
93
+
94
+ Smoke mode with a user override:
95
+
96
+ >>> cfg = Config(smoke=True, user_config=Path("my_config.toml"))
97
+ >>> cfg.CV_CONFIG["n_splits"]
98
+ 3
99
+ """
100
+
101
+ def __init__(
102
+ self,
103
+ smoke: bool = False,
104
+ user_config: Path | None = None,
105
+ ):
106
+ self.SMOKE = smoke
107
+
108
+ # Load and merge configuration
109
+ cfg = _load_toml(_DEFAULT_CONFIG)
110
+ if user_config is not None:
111
+ user_cfg = _load_toml(Path(user_config))
112
+ cfg = _deep_merge(cfg, user_cfg)
113
+
114
+ # Paths
115
+ self.PATHS = {
116
+ "INPUT": cfg["paths"]["input"],
117
+ "DATASET": cfg["paths"]["dataset"],
118
+ "OUTPUT": cfg["paths"]["output"],
119
+ }
120
+ self.RESULTS_FILE = os.path.join(self.PATHS["OUTPUT"], "results.txt")
121
+
122
+ # Benchmark settings
123
+ self.RANDOM_STATE = cfg["benchmark"]["random_state"]
124
+ self.REDUCTION_TYPES = cfg["benchmark"]["reduction_types"]
125
+ self.REDUCTION_LEVELS = cfg["benchmark"]["levels"]
126
+ self.ACTIVE_MODELS = cfg["models"]["active"]
127
+
128
+ # Cross-validation
129
+ cv_section = cfg["cv_smoke"] if smoke else cfg["cv"]
130
+ self.CV_CONFIG = {
131
+ "n_splits": cv_section["n_splits"],
132
+ "n_repeats": cv_section["n_repeats"],
133
+ "random_state": self.RANDOM_STATE,
134
+ }
135
+
136
+ # Hyperparameter grids — developer-facing, defined in Python
137
+ self.PARAM_GRID = self._build_param_grid()
138
+ self.PARAM_GRID_BY_MODEL = self._group_param_grid_by_model()
139
+
140
+ # ------------------------------------------------------------------ #
141
+ # Hyperparameter grids
142
+ # ------------------------------------------------------------------ #
143
+
144
+ def _build_param_grid(self) -> list:
145
+ if self.SMOKE:
146
+ return [
147
+ {
148
+ "model": ["svc"],
149
+ "kernel": ["linear"],
150
+ "C": [1],
151
+ },
152
+ {
153
+ "model": ["rf"],
154
+ "n_estimators": [50],
155
+ "max_depth": [5],
156
+ "max_features": ["sqrt"],
157
+ "min_samples_split": [2],
158
+ "min_samples_leaf": [1],
159
+ },
160
+ {
161
+ "model": ["xgb"],
162
+ "n_estimators": [20],
163
+ "learning_rate": [0.1],
164
+ "max_depth": [3],
165
+ "subsample": [0.8],
166
+ "colsample_bytree": [1.0],
167
+ "gamma": [0],
168
+ "reg_alpha": [0],
169
+ "reg_lambda": [1],
170
+ },
171
+ ]
172
+ return [
173
+ {
174
+ "model": ["svc"],
175
+ "kernel": ["poly"],
176
+ "C": [0.01, 0.1, 1, 10],
177
+ "coef0": [0.0, 0.1, 0.02, 0.6, 0.8, 1],
178
+ "gamma": [0.001, 0.002, 0.004, 0.008, 0.01, 0.02, 0.04, 0.08, 0.1, 0.2],
179
+ "degree": [3, 4, 5],
180
+ },
181
+ {
182
+ "model": ["svc"],
183
+ "kernel": ["rbf"],
184
+ "C": [0.01, 0.02, 0.1, 0.2, 1, 2, 10, 20],
185
+ "gamma": [0.001, 0.002, 0.004, 0.008, 0.01, 0.02, 0.04, 0.08, 0.1, 0.2],
186
+ },
187
+ {
188
+ "model": ["rf"],
189
+ "n_estimators": [200, 400, 800],
190
+ "max_depth": [None, 10, 20, 30, 40],
191
+ "max_features": ["sqrt", "log2"],
192
+ "min_samples_split": [2, 5],
193
+ "min_samples_leaf": [1, 2],
194
+ },
195
+ {
196
+ "model": ["xgb"],
197
+ "n_estimators": [300, 400, 600],
198
+ "learning_rate": [0.01, 0.05, 0.1],
199
+ "max_depth": [4, 6, 8],
200
+ "subsample": [0.7, 0.85],
201
+ "colsample_bytree": [0.7, 1.0],
202
+ "gamma": [0, 0.01, 1, 5],
203
+ "reg_alpha": [0, 0.5],
204
+ "reg_lambda": [1, 2],
205
+ },
206
+ ]
207
+
208
+ def _group_param_grid_by_model(self) -> dict:
209
+ grids: dict = {}
210
+ for entry in self.PARAM_GRID:
211
+ model = entry["model"][0]
212
+ grids.setdefault(model, []).append(
213
+ {k: v for k, v in entry.items() if k != "model"}
214
+ )
215
+ return {m: ParameterGrid(g) for m, g in grids.items()}
216
+
217
+ # ------------------------------------------------------------------ #
218
+ # Public accessors
219
+ # ------------------------------------------------------------------ #
220
+
221
+ def get_cv_config(self) -> dict:
222
+ """Return the cross-validation configuration dictionary.
223
+
224
+ Returns
225
+ -------
226
+ dict
227
+ Dictionary with keys ``n_splits``, ``n_repeats``, and
228
+ ``random_state``.
229
+ """
230
+ return self.CV_CONFIG
231
+
232
+ def get_param_grid(self, model: str) -> ParameterGrid:
233
+ """Return the compiled hyperparameter grid for a given model.
234
+
235
+ Parameters
236
+ ----------
237
+ model : str
238
+ Model key. One of ``"svc"``, ``"rf"``, or ``"xgb"``.
239
+
240
+ Returns
241
+ -------
242
+ sklearn.model_selection.ParameterGrid
243
+ Iterable of hyperparameter combinations for the requested model.
244
+ """
245
+ return self.PARAM_GRID_BY_MODEL[model]
246
+
247
+ def setup(self) -> None:
248
+ """Create output directories and set random seeds.
249
+
250
+ This method must be called once from the pipeline entry point before
251
+ any data is loaded or models are trained. It is intentionally separated
252
+ from ``__init__`` so that :class:`Config` can be instantiated in tests
253
+ without creating directories or modifying global random state.
254
+
255
+ Notes
256
+ -----
257
+ Directories are created with ``exist_ok=True``, so calling ``setup``
258
+ multiple times is safe.
259
+ """
260
+ for path in self.PATHS.values():
261
+ os.makedirs(path, exist_ok=True)
262
+ random.seed(self.RANDOM_STATE)
263
+ np.random.seed(self.RANDOM_STATE)
@@ -0,0 +1,25 @@
1
+ # MELITE default configuration
2
+ # Copy this file and pass it with --config to override any of these values.
3
+ # Only the keys you include in your custom file will be overridden.
4
+
5
+ [paths]
6
+ input = "raw/"
7
+ dataset = "data/"
8
+ output = "output/"
9
+
10
+ [benchmark]
11
+ reduction_types = ["PCA", "UMAP"]
12
+ levels = [70, 75, 80, 85, 90, 95]
13
+ random_state = 42
14
+
15
+ [cv]
16
+ n_splits = 10
17
+ n_repeats = 5
18
+
19
+ [cv_smoke]
20
+ n_splits = 3
21
+ n_repeats = 1
22
+
23
+ [models]
24
+ # Active models to benchmark. Remove any entry to skip that model.
25
+ active = ["svc", "rf", "xgb"]