halib 0.2.8__py3-none-any.whl → 0.2.10__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
@@ -0,0 +1,6 @@
1
+ pc_name;abbr;working_disk
2
+ DESKTOP-JQD9K01;MainPC;E:
3
+ DESKTOP-5IRHU87;MSI_Laptop;D:
4
+ DESKTOP-96HQCNO;4090_SV;E:
5
+ DESKTOP-Q2IKLC0;4GPU_SV;D:
6
+ DESKTOP-QNS3DNF;1GPU_SV;D:
halib/system/path.py CHANGED
@@ -3,28 +3,39 @@ from ..filetype import csvfile
3
3
  import pandas as pd
4
4
  import platform
5
5
  import re # <--- [FIX 1] Added missing import
6
-
7
- PC_TO_ABBR = {
8
- "DESKTOP-JQD9K01": "MainPC",
9
- "DESKTOP-5IRHU87": "MSI_Laptop",
10
- "DESKTOP-96HQCNO": "4090_SV",
11
- "DESKTOP-Q2IKLC0": "4GPU_SV",
12
- "DESKTOP-QNS3DNF": "1GPU_SV",
13
- }
14
-
15
- ABBR_DISK_MAP = {
16
- "MainPC": "E:",
17
- "MSI_Laptop": "D:",
18
- "4090_SV": "E:",
19
- "4GPU_SV": "D:",
20
- }
6
+ import csv
7
+ from importlib import resources
8
+
9
+ PC_TO_ABBR = {}
10
+ ABBR_DISK_MAP = {}
11
+ pc_df = None
12
+ cPlatform = platform.system().lower()
13
+
14
+
15
+ def load_pc_meta_info():
16
+ # 1. Define the package where the file lives (dotted notation)
17
+ # Since the file is in 'halib/system/', the package is 'halib.system'
18
+ package_name = "halib.system"
19
+ file_name = "_list_pc.csv"
20
+
21
+ # 2. Locate the file
22
+ csv_path = resources.files(package_name).joinpath(file_name)
23
+ global PC_TO_ABBR, ABBR_DISK_MAP, pc_df
24
+ pc_df = pd.read_csv(csv_path, sep=';', encoding='utf-8')
25
+ PC_TO_ABBR = dict(zip(pc_df['pc_name'], pc_df['abbr']))
26
+ ABBR_DISK_MAP = dict(zip(pc_df['abbr'], pc_df['working_disk']))
27
+ pprint("Loaded PC meta info:")
28
+ pprint(ABBR_DISK_MAP)
29
+ pprint(PC_TO_ABBR)
30
+ # ! must be called at the module load time
31
+ load_pc_meta_info()
21
32
 
22
33
 
23
34
  def list_PCs(show=True):
24
- df = pd.DataFrame(list(PC_TO_ABBR.items()), columns=["PC Name", "Abbreviation"])
35
+ global pc_df
25
36
  if show:
26
- csvfile.fn_display_df(df)
27
- return df
37
+ csvfile.fn_display_df(pc_df)
38
+ return pc_df
28
39
 
29
40
 
30
41
  def get_PC_name():
@@ -44,11 +55,12 @@ def get_working_disk(abbr_disk_map=ABBR_DISK_MAP):
44
55
  pc_abbr = get_PC_abbr_name()
45
56
  return abbr_disk_map.get(pc_abbr, None)
46
57
 
58
+ cDisk = get_working_disk()
47
59
 
48
60
  # ! This funcction search for full paths in the obj and normalize them according to the current platform and working disk
49
61
  # ! E.g: "E:/zdataset/DFire", but working_disk: "D:", current_platform: "windows" => "D:/zdataset/DFire"
50
62
  # ! E.g: "E:/zdataset/DFire", but working_disk: "D:", current_platform: "linux" => "/mnt/d/zdataset/DFire"
51
- def normalize_paths(obj, working_disk=None, current_platform=None):
63
+ def normalize_paths(obj, working_disk=cDisk, current_platform=cPlatform):
52
64
  # [FIX 3] Resolve defaults inside function to be safer/cleaner
53
65
  if working_disk is None:
54
66
  working_disk = get_working_disk()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.8
3
+ Version: 0.2.10
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.8**
56
+ **Version 0.2.10**
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
@@ -89,9 +89,10 @@ halib/sys/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
89
89
  halib/sys/cmd.py,sha256=b2x7JPcNnFjLGheIESVYvqAb-w2UwBM1PAwYxMZ5YjA,228
90
90
  halib/sys/filesys.py,sha256=ERpnELLDKJoTIIKf-AajgkY62nID4qmqmX5TkE95APU,2931
91
91
  halib/system/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
92
+ halib/system/_list_pc.csv,sha256=IIQnC32RZ-j5uRS_C1HD0nvTlpxv-ItphZNmEns65iU,172
92
93
  halib/system/cmd.py,sha256=b2x7JPcNnFjLGheIESVYvqAb-w2UwBM1PAwYxMZ5YjA,228
93
94
  halib/system/filesys.py,sha256=102J2fkQhmH1_-HQVy2FQ4NOU8LTjMWV3hToT_APtq8,4401
94
- halib/system/path.py,sha256=uCXTltd943L_2heU2HLwvXcgYJRd8-ZEzDe435419OQ,3165
95
+ halib/system/path.py,sha256=CvWMiGKtrqSq816W6rkiy6QRq3FGb4GWlnNkPZjIvnc,3688
95
96
  halib/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
97
  halib/utils/dataclass_util.py,sha256=rj2IMLlUzbm2OlF5_B2dRTk9njZOaF7tTjYkOsq8uLY,1416
97
98
  halib/utils/dict.py,sha256=wYE6Iw-_CnCWdMg9tpJ2Y2-e2ESkW9FxmdBkZkbUh80,299
@@ -101,8 +102,8 @@ halib/utils/list.py,sha256=BM-8sRhYyqF7bh4p7TQtV7P_gnFruUCA6DTUOombaZg,337
101
102
  halib/utils/listop.py,sha256=Vpa8_2fI0wySpB2-8sfTBkyi_A4FhoFVVvFiuvW8N64,339
102
103
  halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
103
104
  halib/utils/video.py,sha256=zLoj5EHk4SmP9OnoHjO8mLbzPdtq6gQPzTQisOEDdO8,3261
104
- halib-0.2.8.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
105
- halib-0.2.8.dist-info/METADATA,sha256=QU-YIW5BNbgDy9j9ttQsleQ2zkBmWjpVazjjb23vIo8,6836
106
- halib-0.2.8.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
107
- halib-0.2.8.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
108
- halib-0.2.8.dist-info/RECORD,,
105
+ halib-0.2.10.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
106
+ halib-0.2.10.dist-info/METADATA,sha256=EEhbKj7cVxfoVEyOWZRkJZTSFg4esZFIVqwa0-Idnh0,6838
107
+ halib-0.2.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ halib-0.2.10.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
109
+ halib-0.2.10.dist-info/RECORD,,
File without changes