halib 0.2.7__py3-none-any.whl → 0.2.9__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.
@@ -1,168 +1,147 @@
1
- import os
2
- from rich.pretty import pprint
3
1
  from abc import ABC, abstractmethod
4
- from typing import List, Optional, TypeVar, Generic
5
-
6
- from abc import ABC, abstractmethod
7
- from dataclasses import dataclass
8
- from dataclass_wizard import YAMLWizard
9
-
10
-
11
- class NamedCfg(ABC):
12
- """
13
- Base class for named configurations.
14
- All configurations should have a name.
15
- """
16
-
17
- @abstractmethod
18
- def get_name(self):
19
- """
20
- Get the name of the configuration.
21
- This method should be implemented in subclasses.
22
- """
23
- pass
24
-
25
-
26
- @dataclass
27
- class AutoNamedCfg(YAMLWizard, NamedCfg):
28
- """
29
- Mixin that automatically implements get_name() by returning self.name.
30
- Classes using this MUST have a 'name' field.
31
- """
32
-
33
- name: Optional[str] = None
34
-
35
- def get_name(self):
36
- return self.name
37
-
38
- def __post_init__(self):
39
- # Enforce the "MUST" rule here
40
- if self.name is None:
41
- # We allow None during initial load, but it must be set before usage
42
- # or handled by the loader.
43
- pass
44
-
45
-
46
- T = TypeVar("T", bound=AutoNamedCfg)
2
+ from typing import Tuple, Any, Optional
3
+ from .base_config import ExpBaseCfg
4
+ from ..perf.perfcalc import PerfCalc
5
+ from ..perf.perfmetrics import MetricsBackend
47
6
 
48
7
 
49
- class BaseSelectorCfg(Generic[T]):
50
- """
51
- Base class to handle the logic of selecting an item from a list by name.
52
- """
53
-
54
- def _resolve_selection(self, items: List[T], selected_name: str, context: str) -> T:
55
- if selected_name is None:
56
- raise ValueError(f"No {context} selected in the configuration.")
57
-
58
- # Create a lookup dict for O(1) access, or just iterate if list is short
59
- for item in items:
60
- if item.name == selected_name:
61
- return item
8
+ class ExpHook:
9
+ """Base interface for all experiment hooks."""
10
+ def on_before_run(self, exp): pass
11
+ def on_after_run(self, exp, results): pass
62
12
 
63
- raise ValueError(
64
- f"{context.capitalize()} '{selected_name}' not found in the configuration list."
65
- )
66
13
 
67
-
68
- class ExpBaseCfg(ABC, YAMLWizard):
14
+ # ! SEE https://github.com/hahv/base_exp for sample usage
15
+ class BaseExp(PerfCalc, ABC):
69
16
  """
70
- Base class for configuration objects.
71
- What a cfg class must have:
72
- 1 - a dataset cfg
73
- 2 - a metric cfg
74
- 3 - a method cfg
17
+ Base class for experiments.
18
+ Orchestrates the experiment pipeline using a pluggable metrics backend.
75
19
  """
76
20
 
77
- cfg_name: Optional[str] = None
78
-
79
- # Save to yaml fil
80
- def save_to_outdir(
81
- self, filename: str = "__config.yaml", outdir=None, override: bool = False
82
- ) -> None:
83
- """
84
- Save the configuration to the output directory.
85
- """
86
- if outdir is not None:
87
- output_dir = outdir
88
- else:
89
- output_dir = self.get_outdir()
90
- os.makedirs(output_dir, exist_ok=True)
91
- assert (output_dir is not None) and (
92
- os.path.isdir(output_dir)
93
- ), f"Output directory '{output_dir}' does not exist or is not a directory."
94
- file_path = os.path.join(output_dir, filename)
95
- if os.path.exists(file_path) and not override:
96
- pprint(
97
- f"File '{file_path}' already exists. Use 'override=True' to overwrite."
98
- )
99
- else:
100
- # method of YAMLWizard to_yaml_file
101
- self.to_yaml_file(file_path)
102
-
103
- @classmethod
21
+ def __init__(self, config: ExpBaseCfg):
22
+ self.config = config
23
+ self.metric_backend = None
24
+ # Flag to track if init_general/prepare_dataset has run
25
+ self._is_env_ready = False
26
+ self.hooks = []
27
+
28
+ def register_hook(self, hook: ExpHook):
29
+ self.hooks.append(hook)
30
+
31
+ def _trigger_hooks(self, method_name: str, *args, **kwargs):
32
+ for hook in self.hooks:
33
+ method = getattr(hook, method_name, None)
34
+ if callable(method):
35
+ method(*args, **kwargs)
36
+
37
+ # -----------------------
38
+ # PerfCalc Required Methods
39
+ # -----------------------
40
+ def get_dataset_name(self):
41
+ return self.config.get_dataset_cfg().get_name()
42
+
43
+ def get_experiment_name(self):
44
+ return self.config.get_cfg_name()
45
+
46
+ def get_metric_backend(self):
47
+ if not self.metric_backend:
48
+ self.metric_backend = self.prepare_metrics(self.config.get_metric_cfg())
49
+ return self.metric_backend
50
+
51
+ # -----------------------
52
+ # Abstract Experiment Steps
53
+ # -----------------------
104
54
  @abstractmethod
105
- # load from a custom YAML file
106
- def from_custom_yaml_file(cls, yaml_file: str):
107
- """Load a configuration from a custom YAML file."""
55
+ def init_general(self, general_cfg):
56
+ """Setup general settings like SEED, logging, env variables."""
108
57
  pass
109
58
 
110
- def get_cfg_name(self, sep: str = "__", *args, **kwargs) -> str:
111
- if self.cfg_name is None:
112
- # auto get the config name from dataset, method, metric
113
- # 2. Generate the canonical Config Name
114
- name_parts = []
115
- general_info = self.get_general_cfg().get_name()
116
- dataset_info = self.get_dataset_cfg().get_name()
117
- method_info = self.get_method_cfg().get_name()
118
- name_parts = [
119
- general_info,
120
- f"ds_{dataset_info}",
121
- f"mt_{method_info}",
122
- ]
123
- if "extra" in kwargs:
124
- extra_info = kwargs["extra"]
125
- assert isinstance(extra_info, str), "'extra' kwarg must be a string."
126
- name_parts.append(extra_info)
127
- self.cfg_name = sep.join(name_parts)
128
- return self.cfg_name
129
-
130
59
  @abstractmethod
131
- def get_outdir(self):
132
- """
133
- Get the output directory for the configuration.
134
- This method should be implemented in subclasses.
135
- """
136
- return None
60
+ def prepare_dataset(self, dataset_cfg):
61
+ """Load/prepare dataset."""
62
+ pass
137
63
 
138
64
  @abstractmethod
139
- def get_general_cfg(self) -> NamedCfg:
65
+ def prepare_metrics(self, metric_cfg) -> MetricsBackend:
140
66
  """
141
- Get the general configuration like output directory, log settings, SEED, etc.
67
+ Prepare the metrics for the experiment.
142
68
  This method should be implemented in subclasses.
143
69
  """
144
70
  pass
145
71
 
146
72
  @abstractmethod
147
- def get_dataset_cfg(self) -> NamedCfg:
148
- """
149
- Get the dataset configuration.
150
- This method should be implemented in subclasses.
73
+ def exec_exp(self, *args, **kwargs) -> Optional[Tuple[Any, Any]]:
74
+ """Run experiment process, e.g.: training/evaluation loop.
75
+ Return: either `None` or a tuple of (raw_metrics_data, extra_data) for calc_and_save_exp_perfs
151
76
  """
152
77
  pass
153
78
 
154
- @abstractmethod
155
- def get_method_cfg(self) -> NamedCfg:
79
+ # -----------------------
80
+ # Internal Helpers
81
+ # -----------------------
82
+ def _validate_and_unpack(self, results):
83
+ if results is None:
84
+ return None
85
+ if not isinstance(results, (tuple, list)) or len(results) != 2:
86
+ raise ValueError("exec must return (metrics_data, extra_data)")
87
+ return results[0], results[1]
88
+
89
+ def _prepare_environment(self, force_reload: bool = False):
156
90
  """
157
- Get the method configuration.
158
- This method should be implemented in subclasses.
91
+ Common setup. Skips if already initialized, unless force_reload is True.
159
92
  """
160
- pass
93
+ if self._is_env_ready and not force_reload:
94
+ # Environment is already prepared, skipping setup.
95
+ return
161
96
 
162
- @abstractmethod
163
- def get_metric_cfg(self) -> NamedCfg:
97
+ # 1. Run Setup
98
+ self.init_general(self.config.get_general_cfg())
99
+ self.prepare_dataset(self.config.get_dataset_cfg())
100
+
101
+ # 2. Update metric backend (refresh if needed)
102
+ self.metric_backend = self.prepare_metrics(self.config.get_metric_cfg())
103
+
104
+ # 3. Mark as ready
105
+ self._is_env_ready = True
106
+
107
+ # -----------------------
108
+ # Main Experiment Runner
109
+ # -----------------------
110
+ def run_exp(self, should_calc_metrics=True, reload_env=False, *args, **kwargs):
164
111
  """
165
- Get the metric configuration.
166
- This method should be implemented in subclasses.
112
+ Run the whole experiment pipeline.
113
+ :param reload_env: If True, forces dataset/general init to run again.
114
+ :param should_calc_metrics: Whether to calculate and save metrics after execution.
115
+ :kwargs Params:
116
+ + 'outfile' to save csv file results,
117
+ + 'outdir' to set output directory for experiment results.
118
+ + 'return_df' to return a DataFrame of results instead of a dictionary.
119
+
120
+ Full pipeline:
121
+ 1. Init
122
+ 2. Prepare Environment (General + Dataset + Metrics)
123
+ 3. Save Config
124
+ 4. Execute
125
+ 5. Calculate & Save Metrics
167
126
  """
168
- pass
127
+ self._prepare_environment(force_reload=reload_env)
128
+
129
+ self._trigger_hooks("before_run", self)
130
+
131
+ # Save config before running
132
+ self.config.save_to_outdir()
133
+
134
+ # Execute experiment
135
+ results = self.exec_exp(*args, **kwargs)
136
+
137
+ if should_calc_metrics and results is not None:
138
+ metrics_data, extra_data = self._validate_and_unpack(results)
139
+ # Calculate & Save metrics
140
+ perf_results = self.calc_perfs(
141
+ raw_metrics_data=metrics_data, extra_data=extra_data, *args, **kwargs
142
+ )
143
+ self._trigger_hooks("after_run", self, perf_results)
144
+ return perf_results
145
+ else:
146
+ self._trigger_hooks("after_run", self, results)
147
+ return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.7
3
+ Version: 0.2.9
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -53,7 +53,7 @@ Dynamic: summary
53
53
 
54
54
  # Helper package for coding and automation
55
55
 
56
- **Version 0.2.7**
56
+ **Version 0.2.9**
57
57
  + reorganize packages with most changes in `research` package; also rename `research` to `exp` (package for experiment management and utilities)
58
58
  + update `exp/perfcalc.py` to allow save computed performance to csv file (without explicit calling method `calc_perfs`)
59
59
 
@@ -22,7 +22,7 @@ halib/common/rich_color.py,sha256=tyK5fl3Dtv1tKsfFzt_5Rco4Fj72QliA-w5aGXaVuqQ,63
22
22
  halib/exp/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
23
23
  halib/exp/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  halib/exp/core/base_config.py,sha256=Js2oVDt7qwT7eV_sOUWw6XXl569G1bX6ls-VYAx2gWY,5032
25
- halib/exp/core/base_exp.py,sha256=XjRHXbUHE-DCZLRDTteDF5gsxKN3mhGEe2zWL24JP80,5131
25
+ halib/exp/core/base_exp.py,sha256=fknJVmW6ubbapOggbkrbNWgc1ZXcUz_FE3wMyuIGX7M,5180
26
26
  halib/exp/core/param_gen.py,sha256=I9JHrDCaep4CjvApDoX0QzFuw38zMC2PsDFueuA7pjM,4271
27
27
  halib/exp/core/wandb_op.py,sha256=powL2QyLBqF-6PUGAOqd60s1npHLLKJxPns3S4hKeNo,4160
28
28
  halib/exp/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -101,8 +101,8 @@ halib/utils/list.py,sha256=BM-8sRhYyqF7bh4p7TQtV7P_gnFruUCA6DTUOombaZg,337
101
101
  halib/utils/listop.py,sha256=Vpa8_2fI0wySpB2-8sfTBkyi_A4FhoFVVvFiuvW8N64,339
102
102
  halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
103
103
  halib/utils/video.py,sha256=zLoj5EHk4SmP9OnoHjO8mLbzPdtq6gQPzTQisOEDdO8,3261
104
- halib-0.2.7.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
105
- halib-0.2.7.dist-info/METADATA,sha256=jSjp5DPZ8A8ohlO-QQ__7mE0Z-fO7sdkZ5Bz6ssKnhU,6836
106
- halib-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
- halib-0.2.7.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
108
- halib-0.2.7.dist-info/RECORD,,
104
+ halib-0.2.9.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
105
+ halib-0.2.9.dist-info/METADATA,sha256=qO311QkpE58ZSiy6NOMsRowRhunVHWa0gr__QW_6iQY,6836
106
+ halib-0.2.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
+ halib-0.2.9.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
108
+ halib-0.2.9.dist-info/RECORD,,
File without changes