halib 0.2.21__py3-none-any.whl → 0.2.27__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.
@@ -4,6 +4,11 @@ import numpy as np
4
4
  from itertools import product
5
5
  from typing import Dict, Any, List, Iterator, Optional
6
6
  from ...filetype import yamlfile
7
+ from ...utils.dict import DictUtils
8
+
9
+ # Assuming DictUtils is available in the scope or imported
10
+ # from .dict_utils import DictUtils
11
+
7
12
 
8
13
  class ParamGen:
9
14
  """
@@ -38,6 +43,7 @@ class ParamGen:
38
43
  keys (List[str]): List of flattened dot-notation keys being swept.
39
44
  values (List[List[Any]]): List of value options for each key.
40
45
  """
46
+
41
47
  def __init__(
42
48
  self, sweep_cfg: Dict[str, Any], base_cfg: Optional[Dict[str, Any]] = None
43
49
  ):
@@ -50,7 +56,12 @@ class ParamGen:
50
56
  self.base_cfg = base_cfg if base_cfg is not None else {}
51
57
 
52
58
  # Recursively flatten the nested sweep config into dot-notation keys
53
- self.param_space = self._flatten_params(sweep_cfg)
59
+ # Refactored to use DictUtils, passing our custom leaf logic
60
+ flat_sweep = DictUtils.flatten(sweep_cfg, is_leaf_predicate=self._is_sweep_leaf)
61
+
62
+ # Expand values (ranges, strings) which DictUtils leaves as-is
63
+ self.param_space = {k: self._expand_val(v) for k, v in flat_sweep.items()}
64
+
54
65
  self.keys = list(self.param_space.keys())
55
66
  self.values = list(self.param_space.values())
56
67
 
@@ -66,13 +77,16 @@ class ParamGen:
66
77
 
67
78
  # 2. Deep copy base and update with current params
68
79
  new_cfg = copy.deepcopy(self.base_cfg)
69
- new_cfg = self._apply_updates(new_cfg, flat_params)
80
+
81
+ # Refactored: Unflatten the specific params, then deep merge
82
+ update_structure = DictUtils.unflatten(flat_params)
83
+ DictUtils.deep_update(new_cfg, update_structure)
70
84
 
71
85
  # 3. Store metadata (Optional)
72
86
  # if "_meta" not in new_cfg:
73
87
  # new_cfg["_meta"] = {}
74
88
  # We unflatten the sweep params here so the log is readable
75
- # new_cfg["_meta"]["sweep_params"] = self._unflatten(flat_params)
89
+ # new_cfg["_meta"]["sweep_params"] = DictUtils.unflatten(flat_params)
76
90
 
77
91
  yield new_cfg
78
92
 
@@ -124,27 +138,8 @@ class ParamGen:
124
138
  combinations.append(flat_dict)
125
139
  return combinations
126
140
 
127
- def _unflatten(self, flat_dict: Dict[str, Any]) -> Dict[str, Any]:
128
- """Converts {'a.b': 1} back to {'a': {'b': 1}}."""
129
- nested = {}
130
- self._apply_updates(nested, flat_dict)
131
- return nested
132
-
133
- def _flatten_params(
134
- self, cfg: Dict[str, Any], parent_key: str = ""
135
- ) -> Dict[str, List[Any]]:
136
- """Recursively converts nested dicts into flat dot-notation keys."""
137
- flat = {}
138
- for key, val in cfg.items():
139
- current_key = f"{parent_key}.{key}" if parent_key else key
140
-
141
- if self._is_sweep_leaf(val):
142
- flat[current_key] = self._expand_val(val)
143
- elif isinstance(val, dict):
144
- flat.update(self._flatten_params(val, current_key))
145
- else:
146
- flat[current_key] = [val]
147
- return flat
141
+ # Note: _unflatten, _flatten_params, and _apply_updates have been removed
142
+ # as they are replaced by DictUtils methods.
148
143
 
149
144
  def _is_sweep_leaf(self, val: Any) -> bool:
150
145
  if isinstance(val, list):
@@ -173,17 +168,3 @@ class ParamGen:
173
168
  return np.arange(val["start"], val["stop"], step).tolist()
174
169
 
175
170
  return [val]
176
-
177
- def _apply_updates(
178
- self, cfg: Dict[str, Any], updates: Dict[str, Any]
179
- ) -> Dict[str, Any]:
180
- """Deep merges dot-notation updates into cfg."""
181
- for key, val in updates.items():
182
- parts = key.split(".")
183
- target = cfg
184
- for part in parts[:-1]:
185
- if part not in target:
186
- target[part] = {}
187
- target = target[part]
188
- target[parts[-1]] = val
189
- return cfg
@@ -210,7 +210,37 @@ class PerfCalc(ABC): # Abstract base class for performance calculation
210
210
  **kwargs,
211
211
  ) -> Tuple[Union[List[OrderedDict], pd.DataFrame], Optional[str]]:
212
212
  """
213
- Standard use case: Calculate metrics AND save to CSV.
213
+ Orchestrates the calculation of performance metrics and saves the results to a CSV.
214
+
215
+ This method acts as a pipeline:
216
+ 1. Calculates metrics (Accuracy, F1, etc.) using `raw_metrics_data`.
217
+ 2. Merges in `extra_data` (metadata) to the results.
218
+ 3. Saves the combined data to a CSV file.
219
+
220
+ Args:
221
+ raw_metrics_data (Union[List[dict], dict]):
222
+ The input data required for metric calculations.
223
+ Structure: A dictionary where keys are metric names (e.g., 'accuracy')
224
+ and values are inputs (e.g., {'preds': ..., 'target': ...}).
225
+
226
+ extra_data (Optional[Union[List[dict], dict]]):
227
+ Static metadata or context to attach to the result rows.
228
+ These fields are NOT used for calculation but are passed through
229
+ to the final output (DataFrame/CSV).
230
+
231
+ Example:
232
+ If extra_data={'model_version': 'v1.0', 'epoch': 5},
233
+ the output CSV will include columns 'model_version' and 'epoch'.
234
+
235
+ *args, **kwargs:
236
+ Additional arguments passed to `calc_exp_perf_metrics` or `save_results_to_csv`
237
+ (e.g., `outfile`, `return_df`).
238
+
239
+ Returns:
240
+ Tuple[Union[List[OrderedDict], pd.DataFrame], Optional[str]]:
241
+ A tuple containing:
242
+ 1. The results as a DataFrame (if return_df=True) or a list of dicts.
243
+ 2. The path to the saved output file (or None if save failed).
214
244
  """
215
245
  metric_names = self.get_metric_backend().metric_names
216
246
 
halib/utils/dict.py CHANGED
@@ -1,9 +1,203 @@
1
- def flatten_dict(d, parent_key="", sep="."):
2
- items = {}
3
- for k, v in d.items():
4
- key = f"{parent_key}{sep}{k}" if parent_key else k
5
- if isinstance(v, dict):
6
- items.update(flatten_dict(v, key, sep=sep))
7
- else:
8
- items[key] = v
9
- return items
1
+ from pygments.token import Other
2
+ from typing import Dict, Any, Callable, Optional
3
+ from rich.pretty import pprint
4
+ import json
5
+ import hashlib
6
+
7
+
8
+ class DictUtils:
9
+ """
10
+ General-purpose dictionary manipulation utilities.
11
+ """
12
+
13
+ @staticmethod
14
+ def flatten(
15
+ d: Dict[str, Any],
16
+ parent_key: str = "",
17
+ sep: str = ".",
18
+ is_leaf_predicate: Optional[Callable[[Any], bool]] = None,
19
+ ) -> Dict[str, Any]:
20
+ """
21
+ Recursively flattens a nested dictionary.
22
+
23
+ Args:
24
+ d: The dictionary to flatten.
25
+ parent_key: Prefix for keys (used during recursion).
26
+ sep: Separator for dot-notation keys.
27
+ is_leaf_predicate: Optional function that returns True if a value should
28
+ be treated as a leaf (value) rather than a branch to recurse.
29
+ Useful if you have dicts you don't want flattened.
30
+ """
31
+ items = []
32
+ for k, v in d.items():
33
+ new_key = f"{parent_key}{sep}{k}" if parent_key else k
34
+
35
+ # Check if we should treat this as a leaf (custom logic)
36
+ if is_leaf_predicate and is_leaf_predicate(v):
37
+ items.append((new_key, v))
38
+ # Standard recursion
39
+ elif isinstance(v, dict):
40
+ items.extend(
41
+ DictUtils.flatten(
42
+ v, new_key, sep=sep, is_leaf_predicate=is_leaf_predicate
43
+ ).items()
44
+ )
45
+ else:
46
+ items.append((new_key, v))
47
+ return dict(items)
48
+
49
+ @staticmethod
50
+ def unflatten(flat_dict: Dict[str, Any], sep: str = ".") -> Dict[str, Any]:
51
+ """
52
+ Converts flat dot-notation keys back to nested dictionaries.
53
+ e.g., {'a.b': 1} -> {'a': {'b': 1}}
54
+ """
55
+ nested = {}
56
+ for key, value in flat_dict.items():
57
+ DictUtils.deep_set(nested, key, value, sep=sep)
58
+ return nested
59
+
60
+ @staticmethod
61
+ def deep_update(base: Dict[str, Any], update: Dict[str, Any]) -> Dict[str, Any]:
62
+ """
63
+ Recursively merges 'update' dict into 'base' dict.
64
+
65
+ Unlike the standard `dict.update()`, which replaces nested dictionaries entirely,
66
+ this method enters nested dictionaries and updates them key-by-key. This preserves
67
+ existing keys in 'base' that are not present in 'update'.
68
+
69
+ Args:
70
+ base: The original dictionary to modify.
71
+ update: The dictionary containing new values.
72
+
73
+ Returns:
74
+ The modified 'base' dictionary.
75
+
76
+ Example:
77
+ >>> base = {'model': {'name': 'v1', 'dropout': 0.5}}
78
+ >>> new_vals = {'model': {'name': 'v2'}}
79
+ >>> # Standard update would delete 'dropout'. deep_update keeps it:
80
+ >>> DictUtils.deep_update(base, new_vals)
81
+ {'model': {'name': 'v2', 'dropout': 0.5}}
82
+ """
83
+ for k, v in update.items():
84
+ if isinstance(v, dict) and k in base and isinstance(base[k], dict):
85
+ DictUtils.deep_update(base[k], v)
86
+ else:
87
+ base[k] = v
88
+ return base
89
+
90
+ @staticmethod
91
+ def deep_set(d: Dict[str, Any], dot_key: str, value: Any, sep: str = ".") -> None:
92
+ """
93
+ Sets a value in a nested dictionary using a dot-notation key path.
94
+ Automatically creates any missing intermediate dictionaries.
95
+
96
+ Args:
97
+ d: The dictionary to modify.
98
+ dot_key: The path to the value (e.g., "model.backbone.layers").
99
+ value: The value to set.
100
+ sep: The separator used in the key (default is ".").
101
+
102
+ Example:
103
+ >>> cfg = {}
104
+ >>> DictUtils.deep_set(cfg, "a.b.c", 10)
105
+ >>> print(cfg)
106
+ {'a': {'b': {'c': 10}}}
107
+ """
108
+ parts = dot_key.split(sep)
109
+ target = d
110
+ for part in parts[:-1]:
111
+ if part not in target:
112
+ target[part] = {}
113
+ target = target[part]
114
+ if not isinstance(target, dict):
115
+ # Handle conflict if a path was previously a value (e.g. overwriting a leaf)
116
+ target = {}
117
+ target[parts[-1]] = value
118
+
119
+ @staticmethod
120
+ def get_unique_hash(input_dict, length=12):
121
+ """
122
+ Returns a unique hash string for a dictionary.
123
+
124
+ :param input_dict: The dictionary params
125
+ :param length: The desired length of the hash string (default 12)
126
+ """
127
+ assert length >= 12, "Hash length must be at least 12 to ensure uniqueness."
128
+ # 1. Sort keys to ensure {a:1, b:2} == {b:2, a:1}
129
+ config_str = json.dumps(input_dict, sort_keys=True)
130
+
131
+ # 2. Generate full SHA-256 hash (64 chars long)
132
+ full_hash = hashlib.sha256(config_str.encode("utf-8")).hexdigest()
133
+
134
+ # 3. Truncate to desired length
135
+ return full_hash[:length]
136
+
137
+
138
+ def test_update():
139
+ # --- Setup ---
140
+ base_config = {
141
+ "model": {
142
+ "name": "ResNet50",
143
+ "layers": 50,
144
+ "details": {
145
+ "activation": "relu",
146
+ "dropout": 0.5, # <--- We want to keep this
147
+ },
148
+ },
149
+ "epochs": 10,
150
+ }
151
+
152
+ new_settings = {
153
+ "model": {"details": {"activation": "gelu"}} # <--- We only want to change this
154
+ }
155
+
156
+ b1 = base_config.copy()
157
+ b2 = base_config.copy()
158
+ n1 = new_settings.copy()
159
+ n2 = new_settings.copy()
160
+
161
+ pprint("Base Config:")
162
+ pprint(base_config)
163
+ pprint("New Settings:")
164
+ pprint(new_settings)
165
+ print("*" * 40)
166
+ pprint(
167
+ "Task: Update base_config with new_settings, preserving unspecified nested keys."
168
+ )
169
+ print("*" * 40)
170
+
171
+ # --- Standard Update (The Problem) ---
172
+ pprint("Normal Update Result:")
173
+ b1.update(n1)
174
+ pprint(b1) # type: ignore[return-value]
175
+
176
+ # --- Deep Update (The Solution) ---
177
+ pprint("Deep Update Result:")
178
+ pprint(DictUtils.deep_update(b2, n2))
179
+
180
+
181
+ def test_hash():
182
+ # --- Usage ---
183
+ cfg1 = {"learning_rate": 0.01, "batch_size": 32, "optimizer": "adam"}
184
+ cfg1_shuffle = {
185
+ "batch_size": 32,
186
+ "optimizer": "adam",
187
+ "learning_rate": 0.01,
188
+ }
189
+ cfg2 = {"learning_rate": 0.02, "batch_size": 32, "optimizer": "adam"}
190
+ hash1 = DictUtils.get_unique_hash(cfg1)
191
+ hash2 = DictUtils.get_unique_hash(cfg1_shuffle)
192
+ hash3 = DictUtils.get_unique_hash(cfg2)
193
+ pprint(f"Config 1 Hash: {hash1}")
194
+ pprint(f"Config 1_shuffle Hash: {hash2}")
195
+ pprint(f"Config 2 Hash: {hash3}")
196
+
197
+ assert hash1 == hash2, "Hashes should match for identical dicts."
198
+ assert hash1 != hash3, "Hashes should differ for different dicts."
199
+
200
+
201
+ if __name__ == "__main__":
202
+ test_update()
203
+ test_hash()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: halib
3
- Version: 0.2.21
3
+ Version: 0.2.27
4
4
  Summary: Small library for common tasks
5
5
  Author: Hoang Van Ha
6
6
  Author-email: hoangvanhauit@gmail.com
@@ -56,7 +56,9 @@ Dynamic: summary
56
56
 
57
57
  ## v0.2.x (Experiment & Core Updates)
58
58
 
59
- ### **v0.2.21**
59
+ ### **v0.2.27**
60
+ - ✨ **New Feature:** Added `utils.dict.DictUtils` for advanced dictionary manipulations (merging, filtering, transforming).
61
+
60
62
  - ✨ **New Feature:** Added `common.common.pprint_stack_trace` to print stack traces with optional custom messages and force stop capability.
61
63
 
62
64
  - 🚀 **Improvement:** `exp.perf.profiler` - allow to export *report dict* as csv files for further analysis
@@ -23,7 +23,7 @@ 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
25
  halib/exp/core/base_exp.py,sha256=fknJVmW6ubbapOggbkrbNWgc1ZXcUz_FE3wMyuIGX7M,5180
26
- halib/exp/core/param_gen.py,sha256=_JjakBOr0UuJOxR11ZC-mrX7ye5kdc1SfGLZuYFmG1o,7385
26
+ halib/exp/core/param_gen.py,sha256=y4elw6bmoBMKRzS7KDLXp0xqC3x27gg0r6wrcTBfidc,6725
27
27
  halib/exp/core/wandb_op.py,sha256=powL2QyLBqF-6PUGAOqd60s1npHLLKJxPns3S4hKeNo,4160
28
28
  halib/exp/data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
29
  halib/exp/data/dataclass_util.py,sha256=OPZzqmug0be4JEq0hJ68pKjnyl0PRYQMVJGhKw1kvyk,1382
@@ -32,7 +32,7 @@ halib/exp/data/torchloader.py,sha256=oWUplXlGd1IB6CqdRd-mGe-DfMjjZxz9hQ7SWONb-0s
32
32
  halib/exp/perf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  halib/exp/perf/flop_calc.py,sha256=Kb3Gwqc7QtGALZzfyYXBA_9SioReJpTJdUX84kqj-Aw,6907
34
34
  halib/exp/perf/gpu_mon.py,sha256=vD41_ZnmPLKguuq9X44SB_vwd9JrblO4BDzHLXZhhFY,2233
35
- halib/exp/perf/perfcalc.py,sha256=p7rhVShiie7DT_s50lbvbGftVCkrWE0tQGFLUEmTXi0,18326
35
+ halib/exp/perf/perfcalc.py,sha256=zb0eGt24kPVC2HTq9M095wP6y8TqOicWy52BxAigap0,19834
36
36
  halib/exp/perf/perfmetrics.py,sha256=qRiNiCKGUSTLY7gPMVMuVHGAAyeosfGWup2eM4490aw,5485
37
37
  halib/exp/perf/perftb.py,sha256=IWElg3OB5dmhfxnY8pMZvkL2y_EnvLmEx3gJlpUR1Fs,31066
38
38
  halib/exp/perf/profiler.py,sha256=Nx-Y1V3pCbaEOisY1sDT8s0yGFs3J6TUmutZslseoNI,19201
@@ -95,15 +95,15 @@ halib/system/filesys.py,sha256=102J2fkQhmH1_-HQVy2FQ4NOU8LTjMWV3hToT_APtq8,4401
95
95
  halib/system/path.py,sha256=k_pveq41uXEzKPU2KTIdqjUSb4MVM-hCFXHGeO-6x6Q,3694
96
96
  halib/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
97
  halib/utils/dataclass_util.py,sha256=rj2IMLlUzbm2OlF5_B2dRTk9njZOaF7tTjYkOsq8uLY,1416
98
- halib/utils/dict.py,sha256=wYE6Iw-_CnCWdMg9tpJ2Y2-e2ESkW9FxmdBkZkbUh80,299
98
+ halib/utils/dict.py,sha256=tUmwKNbTLOyGLl5nKYdT8QIoS4pBFfdsZHCJ6NQCgp4,6922
99
99
  halib/utils/dict_op.py,sha256=wYE6Iw-_CnCWdMg9tpJ2Y2-e2ESkW9FxmdBkZkbUh80,299
100
100
  halib/utils/gpu_mon.py,sha256=vD41_ZnmPLKguuq9X44SB_vwd9JrblO4BDzHLXZhhFY,2233
101
101
  halib/utils/list.py,sha256=BM-8sRhYyqF7bh4p7TQtV7P_gnFruUCA6DTUOombaZg,337
102
102
  halib/utils/listop.py,sha256=Vpa8_2fI0wySpB2-8sfTBkyi_A4FhoFVVvFiuvW8N64,339
103
103
  halib/utils/tele_noti.py,sha256=-4WXZelCA4W9BroapkRyIdUu9cUVrcJJhegnMs_WpGU,5928
104
104
  halib/utils/video.py,sha256=zLoj5EHk4SmP9OnoHjO8mLbzPdtq6gQPzTQisOEDdO8,3261
105
- halib-0.2.21.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
106
- halib-0.2.21.dist-info/METADATA,sha256=cADukwK5Nxve5oqMTadeSYQUBOuEHUhkePS3i8dtqrs,7590
107
- halib-0.2.21.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
- halib-0.2.21.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
109
- halib-0.2.21.dist-info/RECORD,,
105
+ halib-0.2.27.dist-info/licenses/LICENSE.txt,sha256=qZssdna4aETiR8znYsShUjidu-U4jUT9Q-EWNlZ9yBQ,1100
106
+ halib-0.2.27.dist-info/METADATA,sha256=Or9W2uqrbMh_PQd3KCda2KZRuuH4Wi7I4aicUPq2Ks0,7719
107
+ halib-0.2.27.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
108
+ halib-0.2.27.dist-info/top_level.txt,sha256=7AD6PLaQTreE0Fn44mdZsoHBe_Zdd7GUmjsWPyQ7I-k,6
109
+ halib-0.2.27.dist-info/RECORD,,
File without changes