hydraflow 0.11.1__tar.gz → 0.12.1__tar.gz
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.
- {hydraflow-0.11.1 → hydraflow-0.12.1}/PKG-INFO +1 -1
- {hydraflow-0.11.1 → hydraflow-0.12.1}/apps/quickstart.py +10 -10
- {hydraflow-0.11.1 → hydraflow-0.12.1}/docs/usage/quickstart.md +41 -14
- {hydraflow-0.11.1 → hydraflow-0.12.1}/mkdocs.yaml +1 -1
- {hydraflow-0.11.1 → hydraflow-0.12.1}/pyproject.toml +8 -2
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/main.py +2 -1
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/conf.py +2 -2
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/io.py +21 -2
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/job.py +2 -2
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/parser.py +104 -44
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/hydraflow.yaml +2 -2
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_conf.py +4 -3
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_io.py +13 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_parser.py +24 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.cursorrules +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.gitattributes +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/.gitignore +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/LICENSE +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/README.md +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/docs/index.md +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/cli.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/io.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/app.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/conftest.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_show.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/cli/test_version.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/conftest.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_iter_dirs.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/default.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/params.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/core/test_mlflow.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/filter.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_data.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_info.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/test_values.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/entities/values.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/__init__.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/conftest.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/echo.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_args.py +0 -0
- {hydraflow-0.11.1 → hydraflow-0.12.1}/tests/executor/test_job.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.12.1
|
4
4
|
Summary: Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments.
|
5
5
|
Project-URL: Documentation, https://daizutabi.github.io/hydraflow/
|
6
6
|
Project-URL: Source, https://github.com/daizutabi/hydraflow
|
@@ -1,13 +1,16 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
1
3
|
import logging
|
2
4
|
from dataclasses import dataclass
|
5
|
+
from typing import TYPE_CHECKING
|
3
6
|
|
4
|
-
import hydra
|
5
|
-
import mlflow
|
6
7
|
from hydra.core.config_store import ConfigStore
|
7
|
-
from hydra.core.hydra_config import HydraConfig
|
8
8
|
|
9
9
|
import hydraflow
|
10
10
|
|
11
|
+
if TYPE_CHECKING:
|
12
|
+
from mlflow.entities import Run
|
13
|
+
|
11
14
|
log = logging.getLogger(__name__)
|
12
15
|
|
13
16
|
|
@@ -21,13 +24,10 @@ cs = ConfigStore.instance()
|
|
21
24
|
cs.store(name="config", node=Config)
|
22
25
|
|
23
26
|
|
24
|
-
@
|
25
|
-
def app(cfg: Config) -> None:
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
with hydraflow.start_run(cfg):
|
30
|
-
log.info(f"{cfg.width=}, {cfg.height=}")
|
27
|
+
@hydraflow.main(Config)
|
28
|
+
def app(run: Run, cfg: Config) -> None:
|
29
|
+
log.info(run.info.run_id)
|
30
|
+
log.info(cfg)
|
31
31
|
|
32
32
|
|
33
33
|
if __name__ == "__main__":
|
@@ -2,24 +2,22 @@
|
|
2
2
|
|
3
3
|
## Hydra application
|
4
4
|
|
5
|
-
The following example demonstrates how to use
|
6
|
-
There are two main steps to using Hydraflow:
|
5
|
+
The following example demonstrates how to use a Hydraflow application.
|
7
6
|
|
8
|
-
|
9
|
-
2. Start a new MLflow run that logs the Hydra configuration.
|
10
|
-
|
11
|
-
```python title="apps/quickstart.py" linenums="1" hl_lines="24 26"
|
7
|
+
```python title="apps/quickstart.py" linenums="1"
|
12
8
|
--8<-- "apps/quickstart.py"
|
13
9
|
```
|
14
10
|
|
15
|
-
###
|
11
|
+
### Hydraflow's `main` decorator
|
16
12
|
|
17
|
-
[`hydraflow.
|
18
|
-
|
19
|
-
|
13
|
+
[`hydraflow.main`][] starts a new MLflow run that logs the Hydra configuration.
|
14
|
+
The decorated function must have two arguments: `Run` and `Config`.
|
15
|
+
The `Run` argument is the current MLflow run.
|
16
|
+
The `Config` argument is the Hydra configuration.
|
20
17
|
|
21
18
|
```python
|
22
|
-
|
19
|
+
@hydraflow.main(Config)
|
20
|
+
def app(run: Run, cfg: Config) -> None:
|
23
21
|
pass
|
24
22
|
```
|
25
23
|
|
@@ -31,7 +29,7 @@ rm -rf mlruns outputs multirun
|
|
31
29
|
|
32
30
|
### Single-run
|
33
31
|
|
34
|
-
Run the
|
32
|
+
Run the Hydraflow application as a normal Python script.
|
35
33
|
|
36
34
|
```console exec="1" source="console"
|
37
35
|
$ python apps/quickstart.py
|
@@ -43,8 +41,12 @@ Check the MLflow CLI to view the experiment.
|
|
43
41
|
$ mlflow experiments search
|
44
42
|
```
|
45
43
|
|
44
|
+
The experiment name is the name of the Hydra job.
|
45
|
+
|
46
46
|
### Multi-run
|
47
47
|
|
48
|
+
Run the Hydraflow application with multiple configurations.
|
49
|
+
|
48
50
|
```console exec="1" source="console"
|
49
51
|
$ python apps/quickstart.py -m width=400,600 height=100,200,300
|
50
52
|
```
|
@@ -53,6 +55,8 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
53
55
|
|
54
56
|
### Run collection
|
55
57
|
|
58
|
+
The `RunCollection` object is a collection of runs.
|
59
|
+
|
56
60
|
```pycon exec="1" source="console" session="quickstart"
|
57
61
|
>>> import hydraflow
|
58
62
|
>>> rc = hydraflow.list_runs("quickstart")
|
@@ -61,35 +65,47 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
61
65
|
|
62
66
|
### Retrieve a run
|
63
67
|
|
68
|
+
The `RunCollection` object has a `first` and `last` method that
|
69
|
+
returns the first and last run in the collection.
|
70
|
+
|
64
71
|
```pycon exec="1" source="console" session="quickstart"
|
65
72
|
>>> run = rc.first()
|
66
73
|
>>> print(type(run))
|
67
74
|
```
|
68
75
|
|
69
76
|
```pycon exec="1" source="console" session="quickstart"
|
77
|
+
>>> run = rc.last()
|
70
78
|
>>> cfg = hydraflow.load_config(run)
|
71
|
-
>>> print(type(cfg))
|
72
79
|
>>> print(cfg)
|
73
80
|
```
|
74
81
|
|
82
|
+
The `load_config` function loads the Hydra configuration from the run.
|
83
|
+
|
75
84
|
```pycon exec="1" source="console" session="quickstart"
|
76
|
-
>>> run = rc.last()
|
77
85
|
>>> cfg = hydraflow.load_config(run)
|
86
|
+
>>> print(type(cfg))
|
78
87
|
>>> print(cfg)
|
79
88
|
```
|
80
89
|
|
81
90
|
### Filter runs
|
82
91
|
|
92
|
+
The `filter` method filters the runs by the given key-value pairs.
|
93
|
+
|
83
94
|
```pycon exec="1" source="console" session="quickstart"
|
84
95
|
>>> filtered = rc.filter(width=400)
|
85
96
|
>>> print(filtered)
|
86
97
|
```
|
87
98
|
|
99
|
+
If the value is a list, the run will be included if the value is in the list.
|
100
|
+
|
88
101
|
```pycon exec="1" source="console" session="quickstart"
|
89
102
|
>>> filtered = rc.filter(height=[100, 300])
|
90
103
|
>>> print(filtered)
|
91
104
|
```
|
92
105
|
|
106
|
+
If the value is a tuple, the run will be included if the value is between the tuple.
|
107
|
+
The start and end of the tuple are inclusive.
|
108
|
+
|
93
109
|
```pycon exec="1" source="console" session="quickstart"
|
94
110
|
>>> filtered = rc.filter(height=(100, 300))
|
95
111
|
>>> print(filtered)
|
@@ -97,12 +113,16 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
97
113
|
|
98
114
|
### Group runs
|
99
115
|
|
116
|
+
The `groupby` method groups the runs by the given key.
|
117
|
+
|
100
118
|
```pycon exec="1" source="console" session="quickstart"
|
101
119
|
>>> grouped = rc.groupby("width")
|
102
120
|
>>> for key, group in grouped.items():
|
103
121
|
... print(key, group)
|
104
122
|
```
|
105
123
|
|
124
|
+
The `groupby` method can also take a list of keys.
|
125
|
+
|
106
126
|
```pycon exec="1" source="console" session="quickstart"
|
107
127
|
>>> grouped = rc.groupby(["height"])
|
108
128
|
>>> for key, group in grouped.items():
|
@@ -111,6 +131,13 @@ $ python apps/quickstart.py -m width=400,600 height=100,200,300
|
|
111
131
|
|
112
132
|
### Config dataframe
|
113
133
|
|
134
|
+
The `data.config` attribute returns a pandas DataFrame
|
135
|
+
of the Hydra configuration.
|
136
|
+
|
114
137
|
```pycon exec="1" source="console" session="quickstart"
|
115
138
|
>>> print(rc.data.config)
|
116
139
|
```
|
140
|
+
|
141
|
+
```bash exec="on"
|
142
|
+
rm -rf mlruns outputs multirun
|
143
|
+
```
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
4
4
|
|
5
5
|
[project]
|
6
6
|
name = "hydraflow"
|
7
|
-
version = "0.
|
7
|
+
version = "0.12.1"
|
8
8
|
description = "Hydraflow integrates Hydra and MLflow to manage and track machine learning experiments."
|
9
9
|
readme = "README.md"
|
10
10
|
license = { file = "LICENSE" }
|
@@ -96,8 +96,14 @@ ignore = [
|
|
96
96
|
]
|
97
97
|
|
98
98
|
[tool.ruff.lint.per-file-ignores]
|
99
|
-
"apps/*.py" = ["D", "G", "INP"]
|
99
|
+
"apps/*.py" = ["D", "G", "INP", "T"]
|
100
100
|
"src/hydraflow/cli.py" = ["ANN", "D"]
|
101
101
|
"src/hydraflow/core/main.py" = ["ANN201", "D401"]
|
102
102
|
"src/hydraflow/executor/conf.py" = ["ANN", "D"]
|
103
103
|
"tests/*" = ["A001", "ANN", "ARG", "D", "FBT", "PD", "PLR", "PT", "S", "SLF"]
|
104
|
+
|
105
|
+
[tool.pyright]
|
106
|
+
include = ["src", "tests"]
|
107
|
+
strictDictionaryInference = true
|
108
|
+
strictListInference = true
|
109
|
+
strictSetInference = true
|
@@ -16,6 +16,7 @@ used to wrap experiment entry points. This decorator handles:
|
|
16
16
|
|
17
17
|
Example:
|
18
18
|
```python
|
19
|
+
import hydraflow
|
19
20
|
from dataclasses import dataclass
|
20
21
|
from mlflow.entities import Run
|
21
22
|
|
@@ -24,7 +25,7 @@ Example:
|
|
24
25
|
learning_rate: float
|
25
26
|
batch_size: int
|
26
27
|
|
27
|
-
@main(Config)
|
28
|
+
@hydraflow.main(Config)
|
28
29
|
def train(run: Run, config: Config):
|
29
30
|
# Your training code here
|
30
31
|
pass
|
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
|
|
7
7
|
class Step:
|
8
8
|
batch: str = ""
|
9
9
|
args: str = ""
|
10
|
-
|
10
|
+
with_: str = ""
|
11
11
|
|
12
12
|
|
13
13
|
@dataclass
|
@@ -15,7 +15,7 @@ class Job:
|
|
15
15
|
name: str = ""
|
16
16
|
run: str = ""
|
17
17
|
call: str = ""
|
18
|
-
|
18
|
+
with_: str = ""
|
19
19
|
steps: list[Step] = field(default_factory=list)
|
20
20
|
|
21
21
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
5
5
|
from pathlib import Path
|
6
6
|
from typing import TYPE_CHECKING
|
7
7
|
|
8
|
-
from omegaconf import OmegaConf
|
8
|
+
from omegaconf import DictConfig, ListConfig, OmegaConf
|
9
9
|
|
10
10
|
from .conf import HydraflowConf
|
11
11
|
|
@@ -35,7 +35,26 @@ def load_config() -> HydraflowConf:
|
|
35
35
|
|
36
36
|
cfg = OmegaConf.load(path)
|
37
37
|
|
38
|
-
|
38
|
+
if not isinstance(cfg, DictConfig):
|
39
|
+
return schema
|
40
|
+
|
41
|
+
rename_with(cfg)
|
42
|
+
|
43
|
+
return OmegaConf.merge(schema, cfg) # type: ignore[return-value]
|
44
|
+
|
45
|
+
|
46
|
+
def rename_with(cfg: DictConfig) -> None:
|
47
|
+
"""Rename the `with` field to `with_`."""
|
48
|
+
if "with" in cfg:
|
49
|
+
cfg["with_"] = cfg.pop("with")
|
50
|
+
|
51
|
+
for key in list(cfg.keys()):
|
52
|
+
if isinstance(cfg[key], DictConfig):
|
53
|
+
rename_with(cfg[key])
|
54
|
+
elif isinstance(cfg[key], ListConfig):
|
55
|
+
for item in cfg[key]:
|
56
|
+
if isinstance(item, DictConfig):
|
57
|
+
rename_with(item)
|
39
58
|
|
40
59
|
|
41
60
|
def get_job(name: str) -> Job:
|
@@ -69,10 +69,10 @@ def iter_batches(job: Job) -> Iterator[list[str]]:
|
|
69
69
|
|
70
70
|
"""
|
71
71
|
job_name = f"hydra.job.name={job.name}"
|
72
|
-
job_configs = shlex.split(job.
|
72
|
+
job_configs = shlex.split(job.with_)
|
73
73
|
|
74
74
|
for step in job.steps:
|
75
|
-
configs = shlex.split(step.
|
75
|
+
configs = shlex.split(step.with_) or job_configs
|
76
76
|
|
77
77
|
for args in iter_args(step.batch, step.args):
|
78
78
|
sweep_dir = f"hydra.sweep.dir=multirun/{ulid.ULID()}"
|
@@ -83,7 +83,7 @@ def count_decimal_places(x: str) -> int:
|
|
83
83
|
|
84
84
|
|
85
85
|
def is_number(x: str) -> bool:
|
86
|
-
"""Check if a string
|
86
|
+
"""Check if a string represents a valid number.
|
87
87
|
|
88
88
|
Args:
|
89
89
|
x (str): The string to check.
|
@@ -140,18 +140,54 @@ def _get_range(arg: str) -> tuple[float, float, float]:
|
|
140
140
|
|
141
141
|
|
142
142
|
def _arange(start: float, step: float, stop: float) -> list[float]:
|
143
|
+
"""Generate a range of floating point numbers.
|
144
|
+
|
145
|
+
This function generates a range of floating point numbers
|
146
|
+
with protection against rounding errors.
|
147
|
+
|
148
|
+
Args:
|
149
|
+
start (float): The starting value.
|
150
|
+
step (float): The step size.
|
151
|
+
stop (float): The end value (inclusive).
|
152
|
+
|
153
|
+
Returns:
|
154
|
+
list[float]: A list of floating point numbers from start to stop
|
155
|
+
(inclusive) with the given step.
|
156
|
+
|
157
|
+
"""
|
158
|
+
if step == 0:
|
159
|
+
raise ValueError("Step cannot be zero")
|
160
|
+
|
161
|
+
epsilon = 1e-10
|
162
|
+
|
163
|
+
decimal_places = max(
|
164
|
+
count_decimal_places(str(start)),
|
165
|
+
count_decimal_places(str(step)),
|
166
|
+
count_decimal_places(str(stop)),
|
167
|
+
)
|
168
|
+
|
143
169
|
result = []
|
144
170
|
current = start
|
145
171
|
|
146
|
-
|
147
|
-
|
148
|
-
|
172
|
+
if step > 0:
|
173
|
+
while current <= stop + epsilon:
|
174
|
+
rounded = round(current, decimal_places)
|
175
|
+
result.append(rounded)
|
176
|
+
current += step
|
177
|
+
else:
|
178
|
+
while current >= stop - epsilon:
|
179
|
+
rounded = round(current, decimal_places)
|
180
|
+
result.append(rounded)
|
181
|
+
current += step
|
149
182
|
|
150
183
|
return result
|
151
184
|
|
152
185
|
|
153
186
|
def split_suffix(arg: str) -> tuple[str, str]:
|
154
|
-
"""Split a string into prefix and suffix.
|
187
|
+
"""Split a string into the prefix and suffix.
|
188
|
+
|
189
|
+
The suffix is the part of the string that starts with a colon (:).
|
190
|
+
The prefix is the part of the string that precedes the suffix.
|
155
191
|
|
156
192
|
Args:
|
157
193
|
arg (str): The string to split.
|
@@ -194,6 +230,16 @@ def add_exponent(value: str, exponent: str) -> str:
|
|
194
230
|
Returns:
|
195
231
|
str: The value with the exponent added.
|
196
232
|
|
233
|
+
Examples:
|
234
|
+
>>> add_exponent("1", "e3")
|
235
|
+
'1e3'
|
236
|
+
>>> add_exponent("1", "")
|
237
|
+
'1'
|
238
|
+
>>> add_exponent("0", "e-3")
|
239
|
+
'0'
|
240
|
+
>>> add_exponent("0.0", "e-3")
|
241
|
+
'0.0'
|
242
|
+
|
197
243
|
"""
|
198
244
|
if value in ["0", "0.", "0.0"] or not exponent:
|
199
245
|
return value
|
@@ -201,43 +247,6 @@ def add_exponent(value: str, exponent: str) -> str:
|
|
201
247
|
return f"{value}{exponent}"
|
202
248
|
|
203
249
|
|
204
|
-
def collect_values(arg: str) -> list[str]:
|
205
|
-
"""Collect a list of values from a range argument.
|
206
|
-
|
207
|
-
Collect all individual values within a numeric range
|
208
|
-
represented by a string (e.g., `1:4`) and return them
|
209
|
-
as a list of strings.
|
210
|
-
Support both integer and floating-point ranges.
|
211
|
-
|
212
|
-
Args:
|
213
|
-
arg (str): The argument to collect.
|
214
|
-
|
215
|
-
Returns:
|
216
|
-
list[str]: A list of the collected values.
|
217
|
-
|
218
|
-
"""
|
219
|
-
if "(" in arg:
|
220
|
-
return collect_parentheses(arg)
|
221
|
-
|
222
|
-
if ":" not in arg:
|
223
|
-
return [arg]
|
224
|
-
|
225
|
-
arg, exponent = split_suffix(arg)
|
226
|
-
|
227
|
-
if ":" not in arg:
|
228
|
-
return [f"{arg}{exponent}"]
|
229
|
-
|
230
|
-
rng = _get_range(arg)
|
231
|
-
|
232
|
-
if all(isinstance(x, int) for x in rng):
|
233
|
-
values = [str(x) for x in _arange(*rng)]
|
234
|
-
else:
|
235
|
-
n = max(*(count_decimal_places(x) for x in arg.split(":")))
|
236
|
-
values = [str(round(x, n)) for x in _arange(*rng)]
|
237
|
-
|
238
|
-
return [add_exponent(x, exponent) for x in values]
|
239
|
-
|
240
|
-
|
241
250
|
def split_parentheses(arg: str) -> Iterator[str]:
|
242
251
|
"""Split a string with parentheses into a list of strings.
|
243
252
|
|
@@ -290,8 +299,59 @@ def collect_parentheses(arg: str) -> list[str]:
|
|
290
299
|
return ["".join(x[::-1]) for x in product(*it[::-1])]
|
291
300
|
|
292
301
|
|
302
|
+
def collect_values(arg: str) -> list[str]:
|
303
|
+
"""Collect a list of values from a range argument.
|
304
|
+
|
305
|
+
Collect all individual values within a numeric range
|
306
|
+
represented by a string (e.g., `1:4`) and return them
|
307
|
+
as a list of strings.
|
308
|
+
Support both integer and floating-point ranges.
|
309
|
+
|
310
|
+
Args:
|
311
|
+
arg (str): The argument to collect.
|
312
|
+
|
313
|
+
Returns:
|
314
|
+
list[str]: A list of the collected values.
|
315
|
+
|
316
|
+
Examples:
|
317
|
+
>>> collect_values("1:4")
|
318
|
+
['1', '2', '3', '4']
|
319
|
+
>>> collect_values("1.2:0.1:1.4:k")
|
320
|
+
['1.2e3', '1.3e3', '1.4e3']
|
321
|
+
>>> collect_values("0.1")
|
322
|
+
['0.1']
|
323
|
+
>>> collect_values("4:M")
|
324
|
+
['4e6']
|
325
|
+
>>> collect_values("(1:3,5:7)M")
|
326
|
+
['1e6', '2e6', '3e6', '5e6', '6e6', '7e6']
|
327
|
+
>>> collect_values("(1,5)e-(1:3)")
|
328
|
+
['1e-1', '5e-1', '1e-2', '5e-2', '1e-3', '5e-3']
|
329
|
+
|
330
|
+
"""
|
331
|
+
if "(" in arg:
|
332
|
+
return collect_parentheses(arg)
|
333
|
+
|
334
|
+
if ":" not in arg:
|
335
|
+
return [arg]
|
336
|
+
|
337
|
+
arg, exponent = split_suffix(arg)
|
338
|
+
|
339
|
+
if ":" not in arg:
|
340
|
+
return [f"{arg}{exponent}"]
|
341
|
+
|
342
|
+
rng = _get_range(arg)
|
343
|
+
|
344
|
+
if all(isinstance(x, int) for x in rng):
|
345
|
+
values = [str(x) for x in _arange(*rng)]
|
346
|
+
else:
|
347
|
+
n = max(*(count_decimal_places(x) for x in arg.split(":")))
|
348
|
+
values = [str(round(x, n)) for x in _arange(*rng)]
|
349
|
+
|
350
|
+
return [add_exponent(x, exponent) for x in values]
|
351
|
+
|
352
|
+
|
293
353
|
def split(arg: str) -> list[str]:
|
294
|
-
|
354
|
+
"""Split a string by top-level commas.
|
295
355
|
|
296
356
|
Splits a string by commas while respecting nested structures.
|
297
357
|
Commas inside brackets and quotes are ignored, only splitting
|
@@ -389,7 +449,7 @@ def split_arg(arg: str) -> tuple[str, str, str]:
|
|
389
449
|
key, value = arg.split("=")
|
390
450
|
|
391
451
|
if "/" in key:
|
392
|
-
key, suffix = key.split("/"
|
452
|
+
key, suffix = key.split("/")
|
393
453
|
return key, suffix, value
|
394
454
|
|
395
455
|
return key, "", value
|
@@ -13,10 +13,10 @@ jobs:
|
|
13
13
|
args: count=100
|
14
14
|
parallel:
|
15
15
|
run: python app.py
|
16
|
-
|
16
|
+
with: hydra/launcher=joblib hydra.launcher.n_jobs=2
|
17
17
|
steps:
|
18
18
|
- batch: name=a
|
19
19
|
args: count=1:4
|
20
20
|
- batch: name=b
|
21
21
|
args: count=11:14
|
22
|
-
|
22
|
+
with: hydra/launcher=joblib hydra.launcher.n_jobs=4
|
@@ -25,10 +25,11 @@ def test_none():
|
|
25
25
|
|
26
26
|
|
27
27
|
def test_job(config):
|
28
|
-
cfg = config("jobs:\n a:\n run: a.test\n")
|
28
|
+
cfg = config("jobs:\n a:\n run: a.test\n with: --opt1 --opt2\n")
|
29
29
|
assert cfg.jobs["a"].run == "a.test"
|
30
|
+
assert cfg.jobs["a"].with_ == "--opt1 --opt2"
|
30
31
|
|
31
32
|
|
32
33
|
def test_step(config):
|
33
|
-
cfg = config("jobs:\n a:\n steps:\n -
|
34
|
-
assert cfg.jobs["a"].steps[0].
|
34
|
+
cfg = config("jobs:\n a:\n steps:\n - with: --opt1 --opt2\n")
|
35
|
+
assert cfg.jobs["a"].steps[0].with_ == "--opt1 --opt2"
|
@@ -1,6 +1,7 @@
|
|
1
1
|
from pathlib import Path
|
2
2
|
|
3
3
|
import pytest
|
4
|
+
from omegaconf import DictConfig
|
4
5
|
|
5
6
|
|
6
7
|
@pytest.mark.parametrize("file", ["hydraflow.yaml", "hydraflow.yml"])
|
@@ -16,3 +17,15 @@ def test_find_config_none(chdir):
|
|
16
17
|
from hydraflow.executor.io import find_config_file
|
17
18
|
|
18
19
|
assert find_config_file() is None
|
20
|
+
|
21
|
+
|
22
|
+
def test_load_config_list(chdir):
|
23
|
+
from hydraflow.executor.io import load_config
|
24
|
+
|
25
|
+
Path("hydraflow.yaml").write_text("- a\n- b\n")
|
26
|
+
|
27
|
+
cfg = load_config()
|
28
|
+
assert isinstance(cfg, DictConfig)
|
29
|
+
assert cfg.jobs == {}
|
30
|
+
|
31
|
+
Path("hydraflow.yaml").unlink()
|
@@ -24,6 +24,7 @@ def test_count_decimal_places(s, x):
|
|
24
24
|
("1:2", (1, 1, 2)),
|
25
25
|
(":2", (0, 1, 2)),
|
26
26
|
("0.1:0.1:0.4", (0.1, 0.1, 0.4)),
|
27
|
+
("1.2:0.1:1.4", (1.2, 0.1, 1.4)),
|
27
28
|
],
|
28
29
|
)
|
29
30
|
def test_get_range(s, x):
|
@@ -49,6 +50,28 @@ def test_get_range_errors(arg, expected_exception, expected_message):
|
|
49
50
|
assert str(excinfo.value) == expected_message
|
50
51
|
|
51
52
|
|
53
|
+
@pytest.mark.parametrize(
|
54
|
+
("start", "step", "stop", "expected"),
|
55
|
+
[
|
56
|
+
(1.0, 2.0, 5.0, [1.0, 3.0, 5.0]),
|
57
|
+
(1.2, 0.1, 1.4, [1.2, 1.3, 1.4]),
|
58
|
+
(1.4, -0.1, 1.2, [1.4, 1.3, 1.2]),
|
59
|
+
(1.02e-3, 0.01e-3, 1.04e-3, [0.00102, 0.00103, 0.00104]),
|
60
|
+
],
|
61
|
+
)
|
62
|
+
def test_arange(start, step, stop, expected):
|
63
|
+
from hydraflow.executor.parser import _arange
|
64
|
+
|
65
|
+
assert _arange(start, step, stop) == expected
|
66
|
+
|
67
|
+
|
68
|
+
def test_arange_error():
|
69
|
+
from hydraflow.executor.parser import _arange
|
70
|
+
|
71
|
+
with pytest.raises(ValueError):
|
72
|
+
_arange(1.0, 0.0, 1.0)
|
73
|
+
|
74
|
+
|
52
75
|
@pytest.mark.parametrize(
|
53
76
|
("s", "x"),
|
54
77
|
[
|
@@ -100,6 +123,7 @@ def test_split_suffix(s, x):
|
|
100
123
|
("4.5:-1.5:-4.5", ["4.5", "3.0", "1.5", "0.0", "-1.5", "-3.0", "-4.5"]),
|
101
124
|
("1:2:u", ["1e-6", "2e-6"]),
|
102
125
|
("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "2.0e-9"]),
|
126
|
+
("1.2:0.1:1.4:k", ["1.2e3", "1.3e3", "1.4e3"]),
|
103
127
|
("1:2:e2", ["1e2", "2e2"]),
|
104
128
|
(":2:e2", ["0", "1e2", "2e2"]),
|
105
129
|
("-2:2:k", ["-2e3", "-1e3", "0", "1e3", "2e3"]),
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|