hydraflow 0.1.5__tar.gz → 0.2.1__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. {hydraflow-0.1.5 → hydraflow-0.2.1}/PKG-INFO +1 -1
  2. {hydraflow-0.1.5 → hydraflow-0.2.1}/pyproject.toml +1 -1
  3. {hydraflow-0.1.5 → hydraflow-0.2.1}/src/hydraflow/__init__.py +0 -10
  4. hydraflow-0.2.1/src/hydraflow/config.py +66 -0
  5. {hydraflow-0.1.5 → hydraflow-0.2.1}/src/hydraflow/context.py +31 -19
  6. {hydraflow-0.1.5 → hydraflow-0.2.1}/src/hydraflow/mlflow.py +23 -0
  7. hydraflow-0.2.1/src/hydraflow/runs.py +422 -0
  8. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/scripts/log_run.py +2 -2
  9. hydraflow-0.2.1/tests/test_config.py +168 -0
  10. hydraflow-0.2.1/tests/test_context.py +36 -0
  11. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/test_log_run.py +26 -8
  12. hydraflow-0.2.1/tests/test_mlflow.py +35 -0
  13. hydraflow-0.2.1/tests/test_runs.py +277 -0
  14. hydraflow-0.1.5/src/hydraflow/config.py +0 -54
  15. hydraflow-0.1.5/src/hydraflow/runs.py +0 -512
  16. hydraflow-0.1.5/src/hydraflow/util.py +0 -24
  17. hydraflow-0.1.5/tests/test_config.py +0 -62
  18. hydraflow-0.1.5/tests/test_runs.py +0 -260
  19. {hydraflow-0.1.5 → hydraflow-0.2.1}/.devcontainer/devcontainer.json +0 -0
  20. {hydraflow-0.1.5 → hydraflow-0.2.1}/.devcontainer/postCreate.sh +0 -0
  21. {hydraflow-0.1.5 → hydraflow-0.2.1}/.devcontainer/starship.toml +0 -0
  22. {hydraflow-0.1.5 → hydraflow-0.2.1}/.gitattributes +0 -0
  23. {hydraflow-0.1.5 → hydraflow-0.2.1}/.gitignore +0 -0
  24. {hydraflow-0.1.5 → hydraflow-0.2.1}/LICENSE +0 -0
  25. {hydraflow-0.1.5 → hydraflow-0.2.1}/README.md +0 -0
  26. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/scripts/__init__.py +0 -0
  27. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/scripts/watch.py +0 -0
  28. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/test_version.py +0 -0
  29. {hydraflow-0.1.5 → hydraflow-0.2.1}/tests/test_watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: hydraflow
3
- Version: 0.1.5
3
+ Version: 0.2.1
4
4
  Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
5
5
  Project-URL: Documentation, https://github.com/daizutabi/hydraflow
6
6
  Project-URL: Source, https://github.com/daizutabi/hydraflow
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hydraflow"
7
- version = "0.1.5"
7
+ version = "0.2.1"
8
8
  description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -3,15 +3,10 @@ from .mlflow import set_experiment
3
3
  from .runs import (
4
4
  Run,
5
5
  Runs,
6
- drop_unique_params,
7
6
  filter_runs,
8
- get_artifact_dir,
9
- get_artifact_path,
10
- get_artifact_uri,
11
7
  get_param_dict,
12
8
  get_param_names,
13
9
  get_run,
14
- get_run_id,
15
10
  load_config,
16
11
  )
17
12
 
@@ -20,15 +15,10 @@ __all__ = [
20
15
  "Run",
21
16
  "Runs",
22
17
  "chdir_artifact",
23
- "drop_unique_params",
24
18
  "filter_runs",
25
- "get_artifact_dir",
26
- "get_artifact_path",
27
- "get_artifact_uri",
28
19
  "get_param_dict",
29
20
  "get_param_names",
30
21
  "get_run",
31
- "get_run_id",
32
22
  "load_config",
33
23
  "log_run",
34
24
  "set_experiment",
@@ -0,0 +1,66 @@
1
+ """
2
+ This module provides functionality for working with configuration
3
+ objects using the OmegaConf library.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import TYPE_CHECKING
9
+
10
+ from omegaconf import DictConfig, ListConfig, OmegaConf
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Iterator
14
+ from typing import Any
15
+
16
+
17
+ def iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
18
+ """
19
+ Recursively iterate over the parameters in the given configuration object.
20
+
21
+ This function traverses the configuration object and yields key-value pairs
22
+ representing the parameters. The keys are prefixed with the provided prefix.
23
+
24
+ Args:
25
+ config: The configuration object to iterate over. This can be a dictionary,
26
+ list, DictConfig, or ListConfig.
27
+ prefix: The prefix to prepend to the parameter keys.
28
+ Defaults to an empty string.
29
+
30
+ Yields:
31
+ Key-value pairs representing the parameters in the configuration object.
32
+ """
33
+ if not isinstance(config, (DictConfig, ListConfig)):
34
+ config = OmegaConf.create(config) # type: ignore
35
+
36
+ yield from _iter_params(config, prefix)
37
+
38
+
39
+ def _iter_params(config: object, prefix: str = "") -> Iterator[tuple[str, Any]]:
40
+ if isinstance(config, DictConfig):
41
+ for key, value in config.items():
42
+ if _is_param(value):
43
+ yield f"{prefix}{key}", value
44
+
45
+ else:
46
+ yield from _iter_params(value, f"{prefix}{key}.")
47
+
48
+ elif isinstance(config, ListConfig):
49
+ for index, value in enumerate(config):
50
+ if _is_param(value):
51
+ yield f"{prefix}{index}", value
52
+
53
+ else:
54
+ yield from _iter_params(value, f"{prefix}{index}.")
55
+
56
+
57
+ def _is_param(value: object) -> bool:
58
+ """Check if the given value is a parameter."""
59
+ if isinstance(value, DictConfig):
60
+ return False
61
+
62
+ if isinstance(value, ListConfig):
63
+ if any(isinstance(v, (DictConfig, ListConfig)) for v in value):
64
+ return False
65
+
66
+ return True
@@ -5,6 +5,7 @@ run context.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ import logging
8
9
  import os
9
10
  import time
10
11
  from contextlib import contextmanager
@@ -17,15 +18,14 @@ from hydra.core.hydra_config import HydraConfig
17
18
  from watchdog.events import FileModifiedEvent, FileSystemEventHandler
18
19
  from watchdog.observers import Observer
19
20
 
20
- from hydraflow.mlflow import log_params
21
- from hydraflow.runs import get_artifact_path
22
- from hydraflow.util import uri_to_path
21
+ from hydraflow.mlflow import get_artifact_dir, log_params
23
22
 
24
23
  if TYPE_CHECKING:
25
24
  from collections.abc import Callable, Iterator
26
25
 
27
26
  from mlflow.entities.run import Run
28
- from pandas import Series
27
+
28
+ log = logging.getLogger(__name__)
29
29
 
30
30
 
31
31
  @dataclass
@@ -66,8 +66,7 @@ def log_run(
66
66
 
67
67
  hc = HydraConfig.get()
68
68
  output_dir = Path(hc.runtime.output_dir)
69
- uri = mlflow.get_artifact_uri()
70
- info = Info(output_dir, uri_to_path(uri))
69
+ info = Info(output_dir, get_artifact_dir())
71
70
 
72
71
  # Save '.hydra' config directory first.
73
72
  output_subdir = output_dir / (hc.output_subdir or "")
@@ -81,13 +80,21 @@ def log_run(
81
80
  with watch(log_artifact, output_dir):
82
81
  yield info
83
82
 
83
+ except Exception as e:
84
+ log.error(f"Error during log_run: {e}")
85
+ raise
86
+
84
87
  finally:
85
88
  # Save output_dir including '.hydra' config directory.
86
89
  mlflow.log_artifacts(output_dir.as_posix())
87
90
 
88
91
 
89
92
  @contextmanager
90
- def watch(func: Callable[[Path], None], dir: Path | str = "", timeout: int = 60) -> Iterator[None]:
93
+ def watch(
94
+ func: Callable[[Path], None],
95
+ dir: Path | str = "",
96
+ timeout: int = 60,
97
+ ) -> Iterator[None]:
91
98
  """
92
99
  Watch the given directory for changes and call the provided function
93
100
  when a change is detected.
@@ -98,25 +105,23 @@ def watch(func: Callable[[Path], None], dir: Path | str = "", timeout: int = 60)
98
105
  period or until the context is exited.
99
106
 
100
107
  Args:
101
- func (Callable[[Path], None]): The function to call when a change is
108
+ func: The function to call when a change is
102
109
  detected. It should accept a single argument of type `Path`,
103
110
  which is the path of the modified file.
104
- dir (Path | str, optional): The directory to watch. If not specified,
111
+ dir: The directory to watch. If not specified,
105
112
  the current MLflow artifact URI is used. Defaults to "".
106
- timeout (int, optional): The timeout period in seconds for the watcher
113
+ timeout: The timeout period in seconds for the watcher
107
114
  to run after the context is exited. Defaults to 60.
108
115
 
109
116
  Yields:
110
- None: This context manager does not return any value.
117
+ None
111
118
 
112
119
  Example:
113
120
  with watch(log_artifact, "/path/to/dir"):
114
121
  # Perform operations while watching the directory for changes
115
122
  pass
116
123
  """
117
- if not dir:
118
- uri = mlflow.get_artifact_uri()
119
- dir = uri_to_path(uri)
124
+ dir = dir or get_artifact_dir()
120
125
 
121
126
  handler = Handler(func)
122
127
  observer = Observer()
@@ -126,6 +131,10 @@ def watch(func: Callable[[Path], None], dir: Path | str = "", timeout: int = 60)
126
131
  try:
127
132
  yield
128
133
 
134
+ except Exception as e:
135
+ log.error(f"Error during watch: {e}")
136
+ raise
137
+
129
138
  finally:
130
139
  elapsed = 0
131
140
  while not observer.event_queue.empty():
@@ -150,7 +159,7 @@ class Handler(FileSystemEventHandler):
150
159
 
151
160
  @contextmanager
152
161
  def chdir_artifact(
153
- run: Run | Series | str,
162
+ run: Run,
154
163
  artifact_path: str | None = None,
155
164
  ) -> Iterator[Path]:
156
165
  """
@@ -166,11 +175,14 @@ def chdir_artifact(
166
175
  artifact_path: The artifact path.
167
176
  """
168
177
  curdir = Path.cwd()
178
+ path = mlflow.artifacts.download_artifacts(
179
+ run_id=run.info.run_id,
180
+ artifact_path=artifact_path,
181
+ )
169
182
 
170
- artifact_dir = get_artifact_path(run, artifact_path)
171
-
172
- os.chdir(artifact_dir)
183
+ os.chdir(path)
173
184
  try:
174
- yield artifact_dir
185
+ yield Path(path)
186
+
175
187
  finally:
176
188
  os.chdir(curdir)
@@ -5,6 +5,8 @@ configuration objects and set up experiments using MLflow.
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
+ from pathlib import Path
9
+
8
10
  import mlflow
9
11
  from hydra.core.hydra_config import HydraConfig
10
12
 
@@ -47,3 +49,24 @@ def log_params(config: object, *, synchronous: bool | None = None) -> None:
47
49
  """
48
50
  for key, value in iter_params(config):
49
51
  mlflow.log_param(key, value, synchronous=synchronous)
52
+
53
+
54
+ def get_artifact_dir(artifact_path: str | None = None) -> Path:
55
+ """
56
+ Get the artifact directory for the given artifact path.
57
+
58
+ This function retrieves the artifact URI for the specified artifact path
59
+ using MLflow, downloads the artifacts to a local directory, and returns
60
+ the path to that directory.
61
+
62
+ Args:
63
+ artifact_path: The artifact path for which to get the directory.
64
+ Defaults to None.
65
+
66
+ Returns:
67
+ The local path to the directory where the artifacts are downloaded.
68
+ """
69
+ uri = mlflow.get_artifact_uri(artifact_path)
70
+ dir = mlflow.artifacts.download_artifacts(artifact_uri=uri)
71
+
72
+ return Path(dir)
@@ -0,0 +1,422 @@
1
+ """
2
+ This module provides functionality for managing and interacting with MLflow runs.
3
+ It includes the `Runs` class and various methods to filter runs, retrieve run information,
4
+ log artifacts, and load configurations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from functools import cache
11
+ from itertools import chain
12
+ from typing import TYPE_CHECKING, Any
13
+
14
+ import mlflow
15
+ from mlflow.entities import ViewType
16
+ from mlflow.entities.run import Run
17
+ from mlflow.tracking.fluent import SEARCH_MAX_RESULTS_PANDAS
18
+ from omegaconf import DictConfig, OmegaConf
19
+
20
+ from hydraflow.config import iter_params
21
+
22
+ if TYPE_CHECKING:
23
+ from typing import Any
24
+
25
+
26
+ def search_runs(
27
+ experiment_ids: list[str] | None = None,
28
+ filter_string: str = "",
29
+ run_view_type: int = ViewType.ACTIVE_ONLY,
30
+ max_results: int = SEARCH_MAX_RESULTS_PANDAS,
31
+ order_by: list[str] | None = None,
32
+ search_all_experiments: bool = False,
33
+ experiment_names: list[str] | None = None,
34
+ ) -> Runs:
35
+ """
36
+ Search for Runs that fit the specified criteria.
37
+
38
+ This function wraps the `mlflow.search_runs` function and returns the results
39
+ as a `Runs` object. It allows for flexible searching of MLflow runs based on
40
+ various criteria.
41
+
42
+ Args:
43
+ experiment_ids: List of experiment IDs. Search can work with experiment IDs or
44
+ experiment names, but not both in the same call. Values other than
45
+ ``None`` or ``[]`` will result in error if ``experiment_names`` is
46
+ also not ``None`` or ``[]``. ``None`` will default to the active
47
+ experiment if ``experiment_names`` is ``None`` or ``[]``.
48
+ filter_string: Filter query string, defaults to searching all runs.
49
+ run_view_type: one of enum values ``ACTIVE_ONLY``, ``DELETED_ONLY``, or ``ALL`` runs
50
+ defined in :py:class:`mlflow.entities.ViewType`.
51
+ max_results: The maximum number of runs to put in the dataframe. Default is 100,000
52
+ to avoid causing out-of-memory issues on the user's machine.
53
+ order_by: List of columns to order by (e.g., "metrics.rmse"). The ``order_by`` column
54
+ can contain an optional ``DESC`` or ``ASC`` value. The default is ``ASC``.
55
+ The default ordering is to sort by ``start_time DESC``, then ``run_id``.
56
+ output_format: The output format to be returned. If ``pandas``, a ``pandas.DataFrame``
57
+ is returned and, if ``list``, a list of :py:class:`mlflow.entities.Run`
58
+ is returned.
59
+ search_all_experiments: Boolean specifying whether all experiments should be searched.
60
+ Only honored if ``experiment_ids`` is ``[]`` or ``None``.
61
+ experiment_names: List of experiment names. Search can work with experiment IDs or
62
+ experiment names, but not both in the same call. Values other
63
+ than ``None`` or ``[]`` will result in error if ``experiment_ids``
64
+ is also not ``None`` or ``[]``. ``None`` will default to the active
65
+ experiment if ``experiment_ids`` is ``None`` or ``[]``.
66
+
67
+ Returns:
68
+ A `Runs` object containing the search results.
69
+ """
70
+ runs = mlflow.search_runs(
71
+ experiment_ids=experiment_ids,
72
+ filter_string=filter_string,
73
+ run_view_type=run_view_type,
74
+ max_results=max_results,
75
+ order_by=order_by,
76
+ output_format="list",
77
+ search_all_experiments=search_all_experiments,
78
+ experiment_names=experiment_names,
79
+ )
80
+ return Runs(runs) # type: ignore
81
+
82
+
83
+ @dataclass
84
+ class Runs:
85
+ """
86
+ A class to represent a collection of MLflow runs.
87
+
88
+ This class provides methods to interact with the runs, such as filtering,
89
+ retrieving specific runs, and accessing run information.
90
+ """
91
+
92
+ runs: list[Run]
93
+
94
+ def __repr__(self) -> str:
95
+ return f"{self.__class__.__name__}({len(self)})"
96
+
97
+ def __len__(self) -> int:
98
+ return len(self.runs)
99
+
100
+ def filter(self, config: object) -> Runs:
101
+ """
102
+ Filter the runs based on the provided configuration.
103
+
104
+ This method filters the runs in the collection according to the
105
+ specified configuration object. The configuration object should
106
+ contain key-value pairs that correspond to the parameters of the
107
+ runs. Only the runs that match all the specified parameters will
108
+ be included in the returned `Runs` object.
109
+
110
+ Args:
111
+ config: The configuration object to filter the runs.
112
+
113
+ Returns:
114
+ A new `Runs` object containing the filtered runs.
115
+ """
116
+ return Runs(filter_runs(self.runs, config))
117
+
118
+ def get(self, config: object) -> Run | None:
119
+ """
120
+ Retrieve a specific run based on the provided configuration.
121
+
122
+ This method filters the runs in the collection according to the
123
+ specified configuration object and returns the run that matches
124
+ the provided parameters. If more than one run matches the criteria,
125
+ a `ValueError` is raised.
126
+
127
+ Args:
128
+ config: The configuration object to identify the run.
129
+
130
+ Returns:
131
+ Run: The run object that matches the provided configuration.
132
+ None, if the runs are not in a DataFrame format.
133
+
134
+ Raises:
135
+ ValueError: If the number of filtered runs is not exactly one.
136
+ """
137
+ return get_run(self.runs, config)
138
+
139
+ def get_earliest_run(self, config: object | None = None, **kwargs) -> Run | None:
140
+ """
141
+ Get the earliest run from the list of runs based on the start time.
142
+
143
+ This method filters the runs based on the configuration if provided
144
+ and returns the run with the earliest start time.
145
+
146
+ Args:
147
+ config: The configuration object to filter the runs.
148
+ If None, no filtering is applied.
149
+ **kwargs: Additional key-value pairs to filter the runs.
150
+
151
+ Returns:
152
+ The run with the earliest start time, or None if no runs match the criteria.
153
+ """
154
+ return get_earliest_run(self.runs, config, **kwargs)
155
+
156
+ def get_latest_run(self, config: object | None = None, **kwargs) -> Run | None:
157
+ """
158
+ Get the latest run from the list of runs based on the start time.
159
+
160
+ Args:
161
+ config: The configuration object to filter the runs.
162
+ If None, no filtering is applied.
163
+ **kwargs: Additional key-value pairs to filter the runs.
164
+
165
+ Returns:
166
+ The run with the latest start time, or None if no runs match the criteria.
167
+ """
168
+ return get_latest_run(self.runs, config, **kwargs)
169
+
170
+ def get_param_names(self) -> list[str]:
171
+ """
172
+ Get the parameter names from the runs.
173
+
174
+ This method extracts the unique parameter names from the provided list of runs.
175
+ It iterates through each run and collects the parameter names into a set to
176
+ ensure uniqueness.
177
+
178
+ Returns:
179
+ A list of unique parameter names.
180
+ """
181
+ return get_param_names(self.runs)
182
+
183
+ def get_param_dict(self) -> dict[str, list[str]]:
184
+ """
185
+ Get the parameter dictionary from the list of runs.
186
+
187
+ This method extracts the parameter names and their corresponding values
188
+ from the provided list of runs. It iterates through each run and collects
189
+ the parameter values into a dictionary where the keys are parameter names
190
+ and the values are lists of parameter values.
191
+
192
+ Returns:
193
+ A dictionary where the keys are parameter names and the values are lists
194
+ of parameter values.
195
+ """
196
+ return get_param_dict(self.runs)
197
+
198
+
199
+ def filter_runs(runs: list[Run], config: object, **kwargs) -> list[Run]:
200
+ """
201
+ Filter the runs based on the provided configuration.
202
+
203
+ This method filters the runs in the collection according to the
204
+ specified configuration object. The configuration object should
205
+ contain key-value pairs that correspond to the parameters of the
206
+ runs. Only the runs that match all the specified parameters will
207
+ be included in the returned list of runs.
208
+
209
+ Args:
210
+ runs: The runs to filter.
211
+ config: The configuration object to filter the runs.
212
+ **kwargs: Additional key-value pairs to filter the runs.
213
+
214
+ Returns:
215
+ A filtered list of runs.
216
+ """
217
+ for key, value in chain(iter_params(config), kwargs.items()):
218
+ runs = [run for run in runs if _is_equal(run, key, value)]
219
+
220
+ if len(runs) == 0:
221
+ return []
222
+
223
+ return runs
224
+
225
+
226
+ def _is_equal(run: Run, key: str, value: Any) -> bool:
227
+ param = run.data.params.get(key, value)
228
+
229
+ if param is None:
230
+ return False
231
+
232
+ return type(value)(param) == value
233
+
234
+
235
+ def get_run(runs: list[Run], config: object, **kwargs) -> Run | None:
236
+ """
237
+ Retrieve a specific run based on the provided configuration.
238
+
239
+ This method filters the runs in the collection according to the
240
+ specified configuration object and returns the run that matches
241
+ the provided parameters. If more than one run matches the criteria,
242
+ a `ValueError` is raised.
243
+
244
+ Args:
245
+ runs: The runs to filter.
246
+ config: The configuration object to identify the run.
247
+ **kwargs: Additional key-value pairs to filter the runs.
248
+
249
+ Returns:
250
+ The run object that matches the provided configuration, or None
251
+ if no runs match the criteria.
252
+
253
+ Raises:
254
+ ValueError: If more than one run matches the criteria.
255
+ """
256
+ runs = filter_runs(runs, config, **kwargs)
257
+
258
+ if len(runs) == 0:
259
+ return None
260
+
261
+ if len(runs) == 1:
262
+ return runs[0]
263
+
264
+ msg = f"Multiple runs were filtered. Expected number of runs is 1, but found {len(runs)} runs."
265
+ raise ValueError(msg)
266
+
267
+
268
+ def get_earliest_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
269
+ """
270
+ Get the earliest run from the list of runs based on the start time.
271
+
272
+ This method filters the runs based on the configuration if provided
273
+ and returns the run with the earliest start time.
274
+
275
+ Args:
276
+ runs: The list of runs.
277
+ config: The configuration object to filter the runs.
278
+ If None, no filtering is applied.
279
+ **kwargs: Additional key-value pairs to filter the runs.
280
+
281
+ Returns:
282
+ The run with the earliest start time, or None if no runs match the criteria.
283
+ """
284
+ if config is not None or kwargs:
285
+ runs = filter_runs(runs, config or {}, **kwargs)
286
+
287
+ return min(runs, key=lambda run: run.info.start_time, default=None)
288
+
289
+
290
+ def get_latest_run(runs: list[Run], config: object | None = None, **kwargs) -> Run | None:
291
+ """
292
+ Get the latest run from the list of runs based on the start time.
293
+
294
+ This method filters the runs based on the configuration if provided
295
+ and returns the run with the latest start time.
296
+
297
+ Args:
298
+ runs: The list of runs.
299
+ config: The configuration object to filter the runs.
300
+ If None, no filtering is applied.
301
+ **kwargs: Additional key-value pairs to filter the runs.
302
+
303
+ Returns:
304
+ The run with the latest start time, or None if no runs match the criteria.
305
+ """
306
+ if config is not None or kwargs:
307
+ runs = filter_runs(runs, config or {}, **kwargs)
308
+
309
+ return max(runs, key=lambda run: run.info.start_time, default=None)
310
+
311
+
312
+ def get_param_names(runs: list[Run]) -> list[str]:
313
+ """
314
+ Get the parameter names from the runs.
315
+
316
+ This method extracts the unique parameter names from the provided list of runs.
317
+ It iterates through each run and collects the parameter names into a set to
318
+ ensure uniqueness.
319
+
320
+ Args:
321
+ runs: The list of runs from which to extract parameter names.
322
+
323
+ Returns:
324
+ A list of unique parameter names.
325
+ """
326
+ param_names = set()
327
+
328
+ for run in runs:
329
+ for param in run.data.params.keys():
330
+ param_names.add(param)
331
+
332
+ return list(param_names)
333
+
334
+
335
+ def get_param_dict(runs: list[Run]) -> dict[str, list[str]]:
336
+ """
337
+ Get the parameter dictionary from the list of runs.
338
+
339
+ This method extracts the parameter names and their corresponding values
340
+ from the provided list of runs. It iterates through each run and collects
341
+ the parameter values into a dictionary where the keys are parameter names
342
+ and the values are lists of parameter values.
343
+
344
+ Args:
345
+ runs: The list of runs from which to extract parameter names and values.
346
+
347
+ Returns:
348
+ A dictionary where the keys are parameter names and the values are lists
349
+ of parameter values.
350
+ """
351
+ params = {}
352
+
353
+ for name in get_param_names(runs):
354
+ it = (run.data.params[name] for run in runs if name in run.data.params)
355
+ params[name] = sorted(set(it))
356
+
357
+ return params
358
+
359
+
360
+ def load_config(run: Run) -> DictConfig:
361
+ """
362
+ Load the configuration for a given run.
363
+
364
+ This function loads the configuration for the provided Run instance
365
+ by downloading the configuration file from the MLflow artifacts and
366
+ loading it using OmegaConf.
367
+
368
+ Args:
369
+ run: The Run instance to load the configuration for.
370
+
371
+ Returns:
372
+ The loaded configuration.
373
+ """
374
+ run_id = run.info.run_id
375
+ return _load_config(run_id)
376
+
377
+
378
+ @cache
379
+ def _load_config(run_id: str) -> DictConfig:
380
+ try:
381
+ path = mlflow.artifacts.download_artifacts(
382
+ run_id=run_id,
383
+ artifact_path=".hydra/config.yaml",
384
+ )
385
+ except OSError:
386
+ return DictConfig({})
387
+
388
+ return OmegaConf.load(path) # type: ignore
389
+
390
+
391
+ # def get_hydra_output_dir(run: Run_ | Series | str) -> Path:
392
+ # """
393
+ # Get the Hydra output directory.
394
+
395
+ # Args:
396
+ # run: The run object.
397
+
398
+ # Returns:
399
+ # Path: The Hydra output directory.
400
+ # """
401
+ # path = get_artifact_dir(run) / ".hydra/hydra.yaml"
402
+
403
+ # if path.exists():
404
+ # hc = OmegaConf.load(path)
405
+ # return Path(hc.hydra.runtime.output_dir)
406
+
407
+ # raise FileNotFoundError
408
+
409
+
410
+ # def log_hydra_output_dir(run: Run_ | Series | str) -> None:
411
+ # """
412
+ # Log the Hydra output directory.
413
+
414
+ # Args:
415
+ # run: The run object.
416
+
417
+ # Returns:
418
+ # None
419
+ # """
420
+ # output_dir = get_hydra_output_dir(run)
421
+ # run_id = run if isinstance(run, str) else run.info.run_id
422
+ # mlflow.log_artifacts(output_dir.as_posix(), run_id=run_id)