hydraflow 0.12.0__tar.gz → 0.12.2__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.12.0 → hydraflow-0.12.2}/PKG-INFO +1 -1
- {hydraflow-0.12.0 → hydraflow-0.12.2}/apps/quickstart.py +10 -10
- {hydraflow-0.12.0 → hydraflow-0.12.2}/docs/usage/quickstart.md +41 -14
- {hydraflow-0.12.0 → hydraflow-0.12.2}/mkdocs.yaml +1 -1
- {hydraflow-0.12.0 → hydraflow-0.12.2}/pyproject.toml +5 -2
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/main.py +2 -1
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/parser.py +153 -53
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_parser.py +56 -14
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.cursorrules +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/devcontainer.json +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/postCreate.sh +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.devcontainer/starship.toml +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.gitattributes +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.github/workflows/ci.yaml +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.github/workflows/docs.yaml +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/.gitignore +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/LICENSE +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/README.md +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/docs/index.md +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/cli.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/config.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/context.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/io.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/mlflow.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/core/param.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_collection.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_data.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/entities/run_info.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/conf.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/io.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/executor/job.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/src/hydraflow/py.typed +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/app.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/conftest.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/hydraflow.yaml +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_setup.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_show.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/cli/test_version.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/conftest.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/test_config.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/config/test_params.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/chdir.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/log_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/start_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_chdir.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_log_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/context/test_start_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/hydra_dir.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_hydra_dir.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_iter_dirs.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/io/test_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/default.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/force_new_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/match_overrides.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/rerun_finished.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/skip_finished.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_default.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_force_new_run.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_match_overrides.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_rerun_finished.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/main/test_skip_finished.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/params.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/test_param.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/param/test_params.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/core/test_mlflow.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/filter.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_collection.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_data.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_filter.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_info.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/test_values.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/entities/values.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/__init__.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/conftest.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/echo.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_args.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_conf.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_io.py +0 -0
- {hydraflow-0.12.0 → hydraflow-0.12.2}/tests/executor/test_job.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hydraflow
|
3
|
-
Version: 0.12.
|
3
|
+
Version: 0.12.2
|
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.12.
|
7
|
+
version = "0.12.2"
|
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,7 +96,7 @@ 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"]
|
@@ -104,3 +104,6 @@ ignore = [
|
|
104
104
|
|
105
105
|
[tool.pyright]
|
106
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
|
@@ -48,7 +48,7 @@ def to_number(x: str) -> int | float:
|
|
48
48
|
return int(x)
|
49
49
|
|
50
50
|
|
51
|
-
def
|
51
|
+
def count_decimal_digits(x: str) -> int:
|
52
52
|
"""Count decimal places in a string.
|
53
53
|
|
54
54
|
Examine a string representing a number and returns the count
|
@@ -62,13 +62,13 @@ def count_decimal_places(x: str) -> int:
|
|
62
62
|
int: The number of decimal places.
|
63
63
|
|
64
64
|
Examples:
|
65
|
-
>>>
|
65
|
+
>>> count_decimal_digits("1")
|
66
66
|
0
|
67
|
-
>>>
|
67
|
+
>>> count_decimal_digits("-1.2")
|
68
68
|
1
|
69
|
-
>>>
|
69
|
+
>>> count_decimal_digits("1.234")
|
70
70
|
3
|
71
|
-
>>>
|
71
|
+
>>> count_decimal_digits("-1.234e-10")
|
72
72
|
3
|
73
73
|
|
74
74
|
"""
|
@@ -82,8 +82,52 @@ def count_decimal_places(x: str) -> int:
|
|
82
82
|
return len(decimal_part)
|
83
83
|
|
84
84
|
|
85
|
+
def count_integer_digits(num_str: str) -> int:
|
86
|
+
"""Count the number of digits in the integer part of a number.
|
87
|
+
|
88
|
+
Consider only the integer part of a number, even if it is in
|
89
|
+
scientific notation.
|
90
|
+
|
91
|
+
Args:
|
92
|
+
num_str (str): The string representing a number.
|
93
|
+
|
94
|
+
Returns:
|
95
|
+
int: The number of digits in the integer part of a number.
|
96
|
+
(excluding the sign)
|
97
|
+
|
98
|
+
Examples:
|
99
|
+
>>> count_integer_digits("123")
|
100
|
+
3
|
101
|
+
>>> count_integer_digits("-123.45")
|
102
|
+
3
|
103
|
+
>>> count_integer_digits("+0.00123")
|
104
|
+
1
|
105
|
+
>>> count_integer_digits("-1.200")
|
106
|
+
1
|
107
|
+
>>> count_integer_digits("+1.20e3")
|
108
|
+
1
|
109
|
+
>>> count_integer_digits("-0.120e-3")
|
110
|
+
1
|
111
|
+
>>> count_integer_digits(".123")
|
112
|
+
0
|
113
|
+
|
114
|
+
"""
|
115
|
+
if num_str.startswith(("+", "-")):
|
116
|
+
num_str = num_str[1:]
|
117
|
+
|
118
|
+
if "e" in num_str.lower():
|
119
|
+
num_str = num_str.lower().split("e")[0]
|
120
|
+
|
121
|
+
if "." in num_str:
|
122
|
+
int_part = num_str.split(".")[0]
|
123
|
+
if not int_part:
|
124
|
+
return 0
|
125
|
+
return len(int_part)
|
126
|
+
return len(num_str)
|
127
|
+
|
128
|
+
|
85
129
|
def is_number(x: str) -> bool:
|
86
|
-
"""Check if a string
|
130
|
+
"""Check if a string represents a valid number.
|
87
131
|
|
88
132
|
Args:
|
89
133
|
x (str): The string to check.
|
@@ -140,18 +184,46 @@ def _get_range(arg: str) -> tuple[float, float, float]:
|
|
140
184
|
|
141
185
|
|
142
186
|
def _arange(start: float, step: float, stop: float) -> list[float]:
|
187
|
+
"""Generate a range of floating point numbers.
|
188
|
+
|
189
|
+
This function generates a range of floating point numbers
|
190
|
+
with protection against rounding errors.
|
191
|
+
|
192
|
+
Args:
|
193
|
+
start (float): The starting value.
|
194
|
+
step (float): The step size.
|
195
|
+
stop (float): The end value (inclusive).
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
list[float]: A list of floating point numbers from start to stop
|
199
|
+
(inclusive) with the given step.
|
200
|
+
|
201
|
+
"""
|
202
|
+
if step == 0:
|
203
|
+
raise ValueError("Step cannot be zero")
|
204
|
+
|
205
|
+
epsilon = min(abs(start), abs(stop)) * 1e-5
|
206
|
+
|
143
207
|
result = []
|
144
208
|
current = start
|
145
209
|
|
146
|
-
|
147
|
-
|
148
|
-
|
210
|
+
if step > 0:
|
211
|
+
while current <= stop + epsilon:
|
212
|
+
result.append(current)
|
213
|
+
current += step
|
214
|
+
else:
|
215
|
+
while current >= stop - epsilon:
|
216
|
+
result.append(current)
|
217
|
+
current += step
|
149
218
|
|
150
219
|
return result
|
151
220
|
|
152
221
|
|
153
222
|
def split_suffix(arg: str) -> tuple[str, str]:
|
154
|
-
"""Split a string into prefix and suffix.
|
223
|
+
"""Split a string into the prefix and suffix.
|
224
|
+
|
225
|
+
The suffix is the part of the string that starts with a colon (:).
|
226
|
+
The prefix is the part of the string that precedes the suffix.
|
155
227
|
|
156
228
|
Args:
|
157
229
|
arg (str): The string to split.
|
@@ -194,6 +266,16 @@ def add_exponent(value: str, exponent: str) -> str:
|
|
194
266
|
Returns:
|
195
267
|
str: The value with the exponent added.
|
196
268
|
|
269
|
+
Examples:
|
270
|
+
>>> add_exponent("1", "e3")
|
271
|
+
'1e3'
|
272
|
+
>>> add_exponent("1", "")
|
273
|
+
'1'
|
274
|
+
>>> add_exponent("0", "e-3")
|
275
|
+
'0'
|
276
|
+
>>> add_exponent("0.0", "e-3")
|
277
|
+
'0.0'
|
278
|
+
|
197
279
|
"""
|
198
280
|
if value in ["0", "0.", "0.0"] or not exponent:
|
199
281
|
return value
|
@@ -201,43 +283,6 @@ def add_exponent(value: str, exponent: str) -> str:
|
|
201
283
|
return f"{value}{exponent}"
|
202
284
|
|
203
285
|
|
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
286
|
def split_parentheses(arg: str) -> Iterator[str]:
|
242
287
|
"""Split a string with parentheses into a list of strings.
|
243
288
|
|
@@ -290,8 +335,60 @@ def collect_parentheses(arg: str) -> list[str]:
|
|
290
335
|
return ["".join(x[::-1]) for x in product(*it[::-1])]
|
291
336
|
|
292
337
|
|
338
|
+
def collect_values(arg: str) -> list[str]:
|
339
|
+
"""Collect a list of values from a range argument.
|
340
|
+
|
341
|
+
Collect all individual values within a numeric range
|
342
|
+
represented by a string (e.g., `1:4`) and return them
|
343
|
+
as a list of strings.
|
344
|
+
Support both integer and floating-point ranges.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
arg (str): The argument to collect.
|
348
|
+
|
349
|
+
Returns:
|
350
|
+
list[str]: A list of the collected values.
|
351
|
+
|
352
|
+
Examples:
|
353
|
+
>>> collect_values("1:4")
|
354
|
+
['1', '2', '3', '4']
|
355
|
+
>>> collect_values("1.2:0.1:1.4:k")
|
356
|
+
['1.2e3', '1.3e3', '1.4e3']
|
357
|
+
>>> collect_values("0.1")
|
358
|
+
['0.1']
|
359
|
+
>>> collect_values("4:M")
|
360
|
+
['4e6']
|
361
|
+
>>> collect_values("(1:3,5:7)M")
|
362
|
+
['1e6', '2e6', '3e6', '5e6', '6e6', '7e6']
|
363
|
+
>>> collect_values("(1,5)e-(1:3)")
|
364
|
+
['1e-1', '5e-1', '1e-2', '5e-2', '1e-3', '5e-3']
|
365
|
+
|
366
|
+
"""
|
367
|
+
if "(" in arg:
|
368
|
+
return collect_parentheses(arg)
|
369
|
+
|
370
|
+
if ":" not in arg:
|
371
|
+
return [arg]
|
372
|
+
|
373
|
+
arg, exponent = split_suffix(arg)
|
374
|
+
|
375
|
+
if ":" not in arg:
|
376
|
+
return [f"{arg}{exponent}"]
|
377
|
+
|
378
|
+
rng = _get_range(arg)
|
379
|
+
|
380
|
+
if all(isinstance(x, int) for x in rng):
|
381
|
+
values = [str(x) for x in _arange(*rng)]
|
382
|
+
else:
|
383
|
+
n = max(*(count_integer_digits(x) for x in arg.split(":")))
|
384
|
+
m = max(*(count_decimal_digits(x) for x in arg.split(":")))
|
385
|
+
values = [f"{x:.{n + m}g}" for x in _arange(*rng)]
|
386
|
+
|
387
|
+
return [add_exponent(x, exponent) for x in values]
|
388
|
+
|
389
|
+
|
293
390
|
def split(arg: str) -> list[str]:
|
294
|
-
|
391
|
+
"""Split a string by top-level commas.
|
295
392
|
|
296
393
|
Splits a string by commas while respecting nested structures.
|
297
394
|
Commas inside brackets and quotes are ignored, only splitting
|
@@ -355,8 +452,9 @@ def split(arg: str) -> list[str]:
|
|
355
452
|
def expand_values(arg: str, suffix: str = "") -> Iterator[str]:
|
356
453
|
"""Expand a string argument into a list of values.
|
357
454
|
|
358
|
-
Take a string containing comma-separated values or ranges
|
359
|
-
of all individual values.
|
455
|
+
Take a string containing comma-separated values or ranges
|
456
|
+
and return a list of all individual values.
|
457
|
+
Handle numeric ranges and special characters.
|
360
458
|
|
361
459
|
Args:
|
362
460
|
arg (str): The argument to expand.
|
@@ -379,7 +477,8 @@ def split_arg(arg: str) -> tuple[str, str, str]:
|
|
379
477
|
arg (str): The argument to split.
|
380
478
|
|
381
479
|
Returns:
|
382
|
-
tuple[str, str, str]: A tuple containing the key,
|
480
|
+
tuple[str, str, str]: A tuple containing the key,
|
481
|
+
suffix, and value.
|
383
482
|
|
384
483
|
"""
|
385
484
|
if "=" not in arg:
|
@@ -398,8 +497,9 @@ def split_arg(arg: str) -> tuple[str, str, str]:
|
|
398
497
|
def collect_arg(arg: str) -> str:
|
399
498
|
"""Collect a string of expanded key-value pairs.
|
400
499
|
|
401
|
-
Take a key-value pair argument and concatenates all expanded
|
402
|
-
returning a single string suitable for
|
500
|
+
Take a key-value pair argument and concatenates all expanded
|
501
|
+
values with commas, returning a single string suitable for
|
502
|
+
command-line usage.
|
403
503
|
|
404
504
|
Args:
|
405
505
|
arg (str): The argument to collect.
|
@@ -1,3 +1,4 @@
|
|
1
|
+
import numpy as np
|
1
2
|
import pytest
|
2
3
|
|
3
4
|
|
@@ -12,10 +13,20 @@ def test_to_number(s, x):
|
|
12
13
|
("s", "x"),
|
13
14
|
[("1", 0), ("1.2", 1), ("1.234", 3), ("123.", 0), ("", 0), ("1.234e-10", 3)],
|
14
15
|
)
|
15
|
-
def
|
16
|
-
from hydraflow.executor.parser import
|
16
|
+
def test_count_decimal_digits(s, x):
|
17
|
+
from hydraflow.executor.parser import count_decimal_digits
|
17
18
|
|
18
|
-
assert
|
19
|
+
assert count_decimal_digits(s) == x
|
20
|
+
|
21
|
+
|
22
|
+
@pytest.mark.parametrize(
|
23
|
+
("s", "x"),
|
24
|
+
[("1", 1), ("1.2", 1), ("1.234", 1), ("123.", 3), ("", 0), ("1.234e-10", 1)],
|
25
|
+
)
|
26
|
+
def test_count_integer_digits(s, x):
|
27
|
+
from hydraflow.executor.parser import count_integer_digits
|
28
|
+
|
29
|
+
assert count_integer_digits(s) == x
|
19
30
|
|
20
31
|
|
21
32
|
@pytest.mark.parametrize(
|
@@ -24,6 +35,7 @@ def test_count_decimal_places(s, x):
|
|
24
35
|
("1:2", (1, 1, 2)),
|
25
36
|
(":2", (0, 1, 2)),
|
26
37
|
("0.1:0.1:0.4", (0.1, 0.1, 0.4)),
|
38
|
+
("1.2:0.1:1.4", (1.2, 0.1, 1.4)),
|
27
39
|
],
|
28
40
|
)
|
29
41
|
def test_get_range(s, x):
|
@@ -49,6 +61,29 @@ def test_get_range_errors(arg, expected_exception, expected_message):
|
|
49
61
|
assert str(excinfo.value) == expected_message
|
50
62
|
|
51
63
|
|
64
|
+
@pytest.mark.parametrize(
|
65
|
+
("start", "step", "stop", "expected"),
|
66
|
+
[
|
67
|
+
(1.0, 2.0, 5.0, [1.0, 3.0, 5.0]),
|
68
|
+
(1.2, 0.1, 1.4, [1.2, 1.3, 1.4]),
|
69
|
+
(1.2e-12, 0.1e-12, 1.4e-12, [1.2e-12, 1.3e-12, 1.4e-12]),
|
70
|
+
(1.4, -0.1, 1.2, [1.4, 1.3, 1.2]),
|
71
|
+
(1.02e-3, 0.01e-3, 1.04e-3, [0.00102, 0.00103, 0.00104]),
|
72
|
+
],
|
73
|
+
)
|
74
|
+
def test_arange(start, step, stop, expected):
|
75
|
+
from hydraflow.executor.parser import _arange
|
76
|
+
|
77
|
+
np.testing.assert_allclose(_arange(start, step, stop), expected)
|
78
|
+
|
79
|
+
|
80
|
+
def test_arange_error():
|
81
|
+
from hydraflow.executor.parser import _arange
|
82
|
+
|
83
|
+
with pytest.raises(ValueError):
|
84
|
+
_arange(1.0, 0.0, 1.0)
|
85
|
+
|
86
|
+
|
52
87
|
@pytest.mark.parametrize(
|
53
88
|
("s", "x"),
|
54
89
|
[
|
@@ -86,20 +121,27 @@ def test_split_suffix(s, x):
|
|
86
121
|
("1:M", ["1e6"]),
|
87
122
|
("0.234p", ["0.234p"]),
|
88
123
|
("1:3", ["1", "2", "3"]),
|
89
|
-
("0:0.25:1", ["0", "0.25", "0.5", "0.75", "1
|
124
|
+
("0:0.25:1", ["0", "0.25", "0.5", "0.75", "1"]),
|
125
|
+
("10:0.25:11", ["10", "10.25", "10.5", "10.75", "11"]),
|
90
126
|
(":3", ["0", "1", "2", "3"]),
|
91
127
|
("5:7", ["5", "6", "7"]),
|
92
128
|
("-1:1", ["-1", "0", "1"]),
|
93
|
-
("1:0.5:2", ["1", "1.5", "2
|
94
|
-
("1.:0.5:2", ["1
|
95
|
-
("2:0.5:3", ["2", "2.5", "3
|
96
|
-
("-1:0.5:1", ["-1", "-0.5", "0
|
129
|
+
("1:0.5:2", ["1", "1.5", "2"]),
|
130
|
+
("1.:0.5:2", ["1", "1.5", "2"]),
|
131
|
+
("2:0.5:3", ["2", "2.5", "3"]),
|
132
|
+
("-1:0.5:1", ["-1", "-0.5", "0", "0.5", "1"]),
|
97
133
|
("4:-1:2", ["4", "3", "2"]),
|
98
|
-
("4.5:-1.5:2", ["4.5", "3
|
99
|
-
("4.5:-1.5:1.5", ["4.5", "3
|
100
|
-
("4.5:-1.5:-4.5", ["4.5", "3
|
134
|
+
("4.5:-1.5:2", ["4.5", "3"]),
|
135
|
+
("4.5:-1.5:1.5", ["4.5", "3", "1.5"]),
|
136
|
+
("4.5:-1.5:-4.5", ["4.5", "3", "1.5", "0", "-1.5", "-3", "-4.5"]),
|
101
137
|
("1:2:u", ["1e-6", "2e-6"]),
|
102
|
-
("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "
|
138
|
+
("1:.25:2:n", ["1e-9", "1.25e-9", "1.5e-9", "1.75e-9", "2e-9"]),
|
139
|
+
("1.2:0.1:1.4:k", ["1.2e3", "1.3e3", "1.4e3"]),
|
140
|
+
("1.2e-3:0.1e-3:1.4e-3", ["0.0012", "0.0013", "0.0014"]),
|
141
|
+
("1.2e-12:0.1e-12:1.4e-12", ["1.2e-12", "1.3e-12", "1.4e-12"]),
|
142
|
+
("1.22e-6:0.01e-6:1.24e-6", ["1.22e-06", "1.23e-06", "1.24e-06"]),
|
143
|
+
("1.2e3:0.1e3:1.4e3", ["1.2e+03", "1.3e+03", "1.4e+03"]),
|
144
|
+
("1.2e6:0.1e6:1.4e6", ["1.2e+06", "1.3e+06", "1.4e+06"]),
|
103
145
|
("1:2:e2", ["1e2", "2e2"]),
|
104
146
|
(":2:e2", ["0", "1e2", "2e2"]),
|
105
147
|
("-2:2:k", ["-2e3", "-1e3", "0", "1e3", "2e3"]),
|
@@ -117,7 +159,7 @@ def test_collect_value(s, x):
|
|
117
159
|
[
|
118
160
|
("1,2,3", ["1", "2", "3"]),
|
119
161
|
("1:3,5:6", ["1", "2", "3", "5", "6"]),
|
120
|
-
("0:0.25:1,2.0", ["0", "0.25", "0.5", "0.75", "1
|
162
|
+
("0:0.25:1,2.0", ["0", "0.25", "0.5", "0.75", "1", "2.0"]),
|
121
163
|
("3", ["3"]),
|
122
164
|
("3:k", ["3e3"]),
|
123
165
|
("1:3:k,3:2:7:M", ["1e3", "2e3", "3e3", "3e6", "5e6", "7e6"]),
|
@@ -140,7 +182,7 @@ def test_expand_value(s, x):
|
|
140
182
|
[
|
141
183
|
("1,2,3", ["1e3", "2e3", "3e3"]),
|
142
184
|
("1:3,5:6", ["1e3", "2e3", "3e3", "5e3", "6e3"]),
|
143
|
-
("0:0.25:1,2.0", ["0e3", "0.25e3", "0.5e3", "0.75e3", "
|
185
|
+
("0:0.25:1,2.0", ["0e3", "0.25e3", "0.5e3", "0.75e3", "1e3", "2.0e3"]),
|
144
186
|
("3", ["3e3"]),
|
145
187
|
],
|
146
188
|
)
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|