diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c9c8997..40efc6aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,40 +15,29 @@ jobs: uses: actions/setup-python@v5 with: python-version: 3.11 - cache: pip - cache-dependency-path: | - requirements.txt - requirements-dev.txt + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + enable-cache: true + - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt -r requirements-dev.txt + run: uv sync --group dev - name: Check imports sort order - uses: isort/isort-action@v1 - with: - sort-paths: src, tests - configuration: --profile black --check-only --diff - requirements-files: "requirements.txt requirements-dev.txt" + run: uv run isort --profile black --check-only --diff src tests - name: Check formatting - uses: psf/black@stable - with: - # Version of Black should match the versions set in `requirements-dev.txt` - version: "~=23.7.0" - options: --check --diff + run: uv run black --check --diff src tests - name: Check typing - run: | - python -m mypy + run: uv run mypy - name: Test - run: | - pytest --cov antarest --cov-report xml + run: uv run pytest --cov gems --cov-report xml - name: Archive code coverage results uses: actions/upload-artifact@v4 with: name: python-code-coverage-report path: coverage.xml - diff --git a/.gitignore b/.gitignore index 59318f35..aae0da66 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,4 @@ outputs build src/gems.egg-info/ src/gemspy.egg-info/ +build_time_scalability.csv diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4206974..00f6cbb7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -3,7 +3,7 @@ repos: hooks: - id: mypy name: Run mypy - entry: mypy + entry: uv run mypy language: system files: ^src/ types: [python] @@ -12,7 +12,7 @@ repos: hooks: - id: black name: Run black - entry: black + entry: uv run black args: ["--config", "pyproject.toml"] language: system types: [python] @@ -21,7 +21,7 @@ repos: hooks: - id: isort name: Run isort - entry: isort - args: ["--profile", "black", "--filter-files"] + entry: uv run isort + args: ["--profile", "black", "--filter-files", "src", "tests"] language: system types: [python] diff --git a/.readthedocs.yml b/.readthedocs.yml index d0135160..f1e034ff 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -13,7 +13,10 @@ build: mkdocs: configuration: mkdocs.yml -# Optionally declare the Python requirements required to build your docs +# Install the package with doc optional dependencies python: install: - - requirements: requirements-doc.txt \ No newline at end of file + - method: pip + path: . + extra_requirements: + - doc diff --git a/AGENTS.md b/AGENTS.md index 8dae8b76..f1e49f43 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,7 @@ pip install -r requirements.txt -r requirements-dev.txt ```bash pytest # run all tests pytest tests/path/to/test_file.py::test_name # run a single test -pytest --cov gems --cov-report xml # with coverage +pytest --cov gems --cov-report xml # with coverage ``` **Lint & Format:** @@ -37,51 +37,65 @@ gemspy \ --duration 8760 \ --scenarios 1 -# Python API (minimal example) -from gems.main.main import build_problem +# Python API — directory-based study +from gems.study.folder import load_study, run_study +from gems.simulation import TimeBlock + +study = load_study(Path("path/to/study_dir")) # reads input/, model-libraries/, data-series/ +problem = run_study(Path("path/to/study_dir"), scenarios=1, time_block=TimeBlock(1, list(range(8760)))) + +# Python API — programmatic study +from gems.study import Study +from gems.simulation import build_problem, TimeBlock + +study = Study(system=system, database=database) +problem = build_problem(study, TimeBlock(1, list(range(8760))), scenarios=1) +problem.solve(solver_name="highs") ``` ## Architecture -The pipeline flows: **YAML input → parsing → model resolution → network building → optimization problem → OR-Tools solver → results** +The pipeline flows: **YAML input → parsing → model resolution → system instantiation → optimization problem → HiGHS solver (via linopy) → results** + +An optional `optim-config.yml` activates decomposition: variables and constraints are split across a master problem and subproblems, with either sequential resolution or full Benders decomposition. ### Core Modules (`src/gems/`) **`model/`** — Immutable model templates. - `Model`: defines component behavior (parameters, variables, constraints, ports) - `Library`: a collection of models, loaded from YAML -- Models are never instantiated directly — they are referenced by components **`expression/`** — Mathematical expression language and AST. - `ExpressionNode`: base frozen dataclass for all expression tree nodes -- Node types cover: arithmetic (`+`, `-`, `*`, `/`), comparisons (`<=`, `>=`, `==`), time/scenario operators (`time_sum()`), and functions (`max()`, `min()`, `ceil()`, `floor()`) -- Grammar is defined in `/grammar/` and parsed via ANTLR4 (generated files live in `expression/parsing/antlr/`) +- Grammar is defined in `grammar/Expr.g4` and parsed via ANTLR4 (generated files live in `expression/parsing/antlr/` — do not edit directly) - `ExpressionVisitor` is the dominant pattern for traversing and transforming expression trees (evaluation, linearization, printing, degree analysis) -- Expressions support operator overloading: `var('x') + 5 * param('p')` -**`study/`** — Study definition and network instantiation. -- `System`: top-level structure parsed from YAML (before instantiation) -- `Network`: instantiated graph of `Node`s, `Component`s, and connections -- `Component`: an instance of a `Model` with concrete parameter values -- `DataBase`: manages time series and scenario data +**`study/`** — Study definition and instantiation. +- `System` (`system.py`): resolved topology — graph of `Component`s, `PortRef`s, and `PortsConnection`s after library references are substituted +- `Study` (`study.py`): dataclass pairing a `System` with a `DataBase`; validates that the database supplies every parameter required by the system +- `DataBase` (`data.py`): manages time-series and scenario data; +- `load_study` / `run_study` (`folder.py`): convenience functions for directory-based studies (`input/system.yml`, `input/model-libraries/`, `input/data-series/`) **`simulation/`** — Optimization problem construction and solving. -- `OptimizationProblem`: main interface; translates network + database into OR-Tools constraints -- `LinearExpression`: the linearized form of model constraints used by the solver -- `BendersDecomposedProblem`: temporal decomposition strategy for large problems -- `TimeBlock`: structure for defining temporal decomposition -- `OutputValues`: result extraction and formatting +- `OptimizationProblem` (`optimization.py`): main interface; translates a `Study` into a linopy model solved by HiGHS +- `DecomposedProblems` (`optimization.py`): holds the master problem and subproblem produced by temporal decomposition +- `VectorizedLinearExprBuilder` (`linearize.py`): `ExpressionVisitor` subclass that converts an expression AST into a `VectorizedExpr` +- `VectorizedBuilderBase` (`vectorized_builder.py`): shared base for all vectorized visitors (used by both `linearize.py` and `extra_output.py`) +- `TimeBlock` (`time_block.py`): defines the temporal window for one solve +- `SimulationTableBuilder` / `SimulationTableWriter` (`simulation_table.py`): result extraction as a flat pandas `DataFrame` -### Key Design Patterns +**`optim_config/`** — Optional decomposition configuration. +- `OptimConfig` (`parsing.py`): top-level config loaded from `optim-config.yml` +- `ResolutionMode` (`parsing.py`): `SEQUENTIAL_SUBPROBLEMS` (default) or `BENDERS_DECOMPOSITION` +- `ModelDecompositionConfig` (`parsing.py`): per-model assignment of variables/constraints/objective contributions to master or subproblems -- **Frozen dataclasses** throughout for immutability (models, expressions, constraints) -- **Visitor pattern** for all expression tree operations (`ExpressionVisitor` subclasses) -- **Indexing dimensions**: parameters and variables carry time and scenario indices explicitly; expressions track these automatically -- **`ValueType`** enum (`INTEGER`, `CONTINUOUS`, `BOOLEAN`) for variable typing +**`libs/`** — Resolves the path to bundled YAML model libraries shipped with the package. -### Type Checking +### Key Design Patterns -Strict mypy is enforced (`disallow_untyped_defs`, `disallow_untyped_calls`). All new code must be fully typed. Configuration is in `mypy.ini`. +- **Visitor pattern** for all expression tree operations (`ExpressionVisitor` subclasses). Use `ExpressionVisitorOperations` as a base when the return type supports `+, -, *, /` — it provides those four method implementations for free. +- **Template-method via single abstract method**: `VectorizedBuilderBase` implements all 18+ visitor methods once with `xr.DataArray`-compatible semantics; concrete subclasses only override `variable()` (and optionally a few linopy-specific methods). +- **Indexing dimensions**: parameters and variables carry time and scenario indices explicitly via `IndexingStructure`; expressions track these automatically. ## Further Reading diff --git a/docs/agents/python-convention.md b/docs/agents/python-convention.md index a929da99..23919335 100644 --- a/docs/agents/python-convention.md +++ b/docs/agents/python-convention.md @@ -2,18 +2,19 @@ ## Code Style & Conventions -- **Formatter**: Black, line-length 88. Never adjust line breaks manually—let Black decide. -- **Import order**: isort with `profile = "black"`. One `import` block, no manual blank lines between - groups. -- **Type annotations**: All functions and methods **must** have full type annotations. mypy is run in - strict mode (`disallow_untyped_defs`, `disallow_untyped_calls`). +- **Formatter**: Black `23.7.0`, line-length 88. Never adjust line breaks manually—let Black decide. +- **Import order**: isort `5.12.0` with `profile = "black"`. One `import` block, no manual blank + lines between groups. +- **Type annotations**: All functions and methods **must** have full type annotations. mypy `1.7.1` + is run with `disallow_untyped_defs = true` and `disallow_untyped_calls = true` (see `mypy.ini`). - **Dataclasses**: Prefer `@dataclass(frozen=True)` for value objects (expression nodes, model definitions). Mutability must be justified explicitly. - **Pydantic**: Use `ConfigDict(alias_generator=to_camel)` or kebab-case alias generation for YAML round-tripping; use Pydantic v2 APIs only. - **Naming**: - Classes: `PascalCase` - - Functions / variables: `snake_case` + - Functions / variables: `snake_case` — use descriptive names; avoid single-letter or two-letter + names except for well-established loop indices (`i`, `j`) or type parameters (`T`). - Constants / type-level aliases: `UPPER_SNAKE_CASE` - YAML keys: `kebab-case` - **Commit messages**: Follow Conventional Commits — `(): `, e.g. diff --git a/docs/agents/testing.md b/docs/agents/testing.md index 246591fb..d65ece6b 100644 --- a/docs/agents/testing.md +++ b/docs/agents/testing.md @@ -3,13 +3,19 @@ ## Philosophy Tests live alongside the module they exercise. Fixtures (YAML snippets, small networks) are kept -in `tests/` sub-directories. No mocking of the solver—tests use real OR-Tools calls. +in `tests/` sub-directories. No mocking of the solver—tests use real HiGHS calls via linopy +(`problem.solve(solver_name="highs")`). ## Layers | Layer | Location | Description | |---|---|---| -| Unit | `tests/unittests/` | One file per module area; covers AST visitors, parsing, model loading, linearisation | -| Integration | `tests/unittests/simulation/` | Full problem build + solve on tiny networks | -| End-to-end | `tests/e2e/` | CLI-level tests reading real YAML fixtures | -| Converter | `tests/input_converter/`, `tests/pypsa_converter/`, `tests/antares_historic/` | Format-specific conversion tests | +| Unit — expressions | `tests/unittests/expressions/` | AST visitors and expression parsing (parsing/, visitor/) | +| Unit — libraries | `tests/unittests/lib_parsing/` | Model library YAML parsing | +| Unit — system | `tests/unittests/system/` | Model, network, and port object behaviour | +| Unit — system parsing | `tests/unittests/system_parsing/` | System YAML parsing | +| Unit — scenario builder | `tests/unittests/scenario_builder/` | Scenario and time-series builder | +| Integration | `tests/unittests/simulation/` | Full problem build + solve on small networks | +| End-to-end — functional | `tests/e2e/functional/` | Cross-cutting tests: library/system combinations, stochastic, investment, scenario builder | +| End-to-end — models | `tests/e2e/models/` | Model-level tests (andromede-v1 models, operator tests, proof-of-concept models) | +| End-to-end — studies | `tests/e2e/studies/` | Study-level tests reading full YAML study fixtures | diff --git a/docs/getting-started.md b/docs/getting-started.md index a3773070..8daa12ae 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -174,7 +174,7 @@ with open("system.yml") as lib_file: input_libraries = [parse_yaml_library(lib_file)] result_lib = resolve_library(input_libraries) -components_input = resolve_system(input_system, result_lib) +system_input = resolve_system(input_system, result_lib) database = build_data_base(input_system, Path(series_dir)) ~~~ @@ -182,11 +182,10 @@ database = build_data_base(input_system, Path(series_dir)) ~~~ python -network = build_network(components_input) +network = build_network(system_input) problem = build_problem( - network, - database, + Study(network, database), TimeBlock(1, [i for i in range(0, timespan)]), scenarios, ) diff --git a/docs/user-guide/inputs.md b/docs/user-guide/inputs.md index 7bec662a..4a9e8784 100644 --- a/docs/user-guide/inputs.md +++ b/docs/user-guide/inputs.md @@ -13,7 +13,7 @@ with open("simple_library.yml") as lib_file: input_libraries = [parse_yaml_library(lib_file)] result_lib = resolve_library(input_libraries) -components_input = resolve_system(input_system, result_lib) +system_input = resolve_system(input_system, result_lib) database = build_data_base(input_system, Path(series_dir)) ~~~ diff --git a/docs/user-guide/optimisation.md b/docs/user-guide/optimisation.md index 7ce0a94e..8cf76347 100644 --- a/docs/user-guide/optimisation.md +++ b/docs/user-guide/optimisation.md @@ -7,11 +7,10 @@ ~~~ python -network = build_network(components_input) +network = build_network(system_input) problem = build_problem( - network, - database, + Study(network, database), TimeBlock(1, [i for i in range(0, timespan)]), scenarios, ) diff --git a/docs/user-guide/outputs.md b/docs/user-guide/outputs.md index 40b4ead8..33efac68 100644 --- a/docs/user-guide/outputs.md +++ b/docs/user-guide/outputs.md @@ -4,13 +4,25 @@ Once the optimisation problem was built and solved, one can retrieve the results as follows: ~~~ python -problem.solver.Solve() -results = OutputValues(problem) +from gems.simulation.simulation_table import SimulationTableBuilder + +df = SimulationTableBuilder().build(problem) ~~~ +The result is a `pandas.DataFrame` with columns: `block`, `component`, `output`, +`absolute-time-index`, `block-time-index`, `scenario-index`, `value`, `basis-status`. + +Reading the value of the optimisation variable `var_id` of component `component_id` +for a single time step and scenario reads: + +~~~ python +value = df[(df["component"] == component_id) & (df["output"] == var_id)]["value"].iloc[0] +~~~ -Reading the timeseries of the optimisation variable ''var_id'' of component "component_id" reads: +For multi-time or multi-scenario results, filter additionally by `block-time-index` +and `scenario-index`: ~~~ python -var_timeseries = results.component(component_id).var(var_id).value -~~~ \ No newline at end of file +sub = df[(df["component"] == component_id) & (df["output"] == var_id)] +value_s0_t1 = sub[(sub["scenario-index"] == 0) & (sub["block-time-index"] == 1)]["value"].iloc[0] +~~~ diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index b0affba5..00000000 --- a/mypy.ini +++ /dev/null @@ -1,17 +0,0 @@ -[mypy] -mypy_path = src -packages = gems -disallow_untyped_defs = true -disallow_untyped_calls = true - -[mypy-ortools.*] -ignore_missing_imports = true - -[mypy-anytree.*] -ignore_missing_imports = true - -[mypy-gems.expression.parsing.antlr.*] -ignore_errors = True - -[mypy-antlr4.*] -ignore_missing_imports = true diff --git a/pyproject.toml b/pyproject.toml index 5ccba8d1..87676309 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,14 +30,16 @@ authors = [ ] requires-python = ">=3.10" dependencies = [ - "numpy", - "ortools", - "scipy>=1.10", - "antlr4-python3-runtime>=4.13", - "PyYAML>=6.0", + "numpy>=1.26", + "antlr4-python3-runtime>=4.13.2", + "PyYAML>=6.0.2", "pydantic>=2.0", - "antares_craft>=0.3", - "anytree>=2.12", + "anytree>=2.13", + "linopy>=0.6", + "xarray>=2025.3", + "highspy>=1.14", + "pandas>=2.2", + "attrs>=26.0", ] classifiers = [ # Classifiers here: https://pypi.org/classifiers/ @@ -49,6 +51,13 @@ classifiers = [ "Programming Language :: Python :: 3.12", ] +[project.optional-dependencies] +doc = [ + "mkdocs", + "mkdocs-material", + "mkdocs-material-extensions", + "mkdocstrings-python", +] [project.scripts] gemspy = "gems.main.main:main_cli" @@ -61,14 +70,47 @@ Repository = "https://github.com/AntaresSimulatorTeam/GemsPy" # All the following settings are optional: where = ["src"] +[dependency-groups] +dev = [ + "pyarrow", + "pytest>=9.0.3", + "mypy>=1.7", + "black>=24.0", + "isort>=5.12", + "pytest-cov", + "pre-commit>=3.5", + "types-PyYAML>=6.0.12", + "antlr4-tools>=0.2.1", + "pandas-stubs", +] + [tool.black] line-length = 88 -include = '\.pyi?$' -exclude = ''' +target-version = ["py311"] +extend-exclude = ''' /( - ^(?!(src)).* + \.venv )/ ''' [tool.isort] profile = "black" + +[tool.pytest.ini_options] +pythonpath = [".", "src"] +testpaths = ["tests"] +log_cli = true + +[tool.mypy] +mypy_path = "src" +packages = ["gems"] +disallow_untyped_defs = true +disallow_untyped_calls = true + +[[tool.mypy.overrides]] +module = ["anytree.*", "linopy.*", "xarray.*", "antlr4.*", "pytest.*", "pydantic.*"] +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = ["gems.expression.parsing.antlr.*"] +ignore_errors = true diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index fc243958..00000000 --- a/pytest.ini +++ /dev/null @@ -1,4 +0,0 @@ -[pytest] -pythonpath = src -testpaths = tests -log_cli = true diff --git a/requirements-dev.in b/requirements-dev.in deleted file mode 100644 index 73c4b932..00000000 --- a/requirements-dev.in +++ /dev/null @@ -1,11 +0,0 @@ --r requirements.txt -pytest~=7.0.0 -mypy~=1.7.1 -black~=23.7.0 -isort~=5.12.0 -pytest-cov -pre-commit~=3.5.0 -types-PyYAML~=6.0.12.12 -antlr4-tools~=0.2.1 -pandas -pandas-stubs \ No newline at end of file diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index d256bdef..00000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,357 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --output-file=requirements-dev.txt requirements-dev.in -# -absl-py==2.3.1 - # via - # -r requirements.txt - # ortools -annotated-types==0.7.0 - # via - # -r requirements.txt - # pydantic -antares-craft==0.3.0 - # via -r requirements.txt -antares-study-version==1.0.15 - # via - # -r requirements.txt - # antares-craft -antares-timeseries-generation==0.1.7 - # via - # -r requirements.txt - # antares-craft -antlr4-python3-runtime==4.13.1 - # via -r requirements.txt -antlr4-tools==0.2.1 - # via -r requirements-dev.in -anytree==2.12.1 - # via -r requirements.txt -attrs==25.3.0 - # via pytest -black==23.7.0 - # via -r requirements-dev.in -bottleneck==1.4.2 - # via - # -r requirements.txt - # linopy -certifi==2025.1.31 - # via - # -r requirements.txt - # netcdf4 - # pyogrio - # pyproj - # requests -cfgv==3.4.0 - # via pre-commit -cftime==1.6.4.post1 - # via - # -r requirements.txt - # netcdf4 -charset-normalizer==3.4.1 - # via - # -r requirements.txt - # requests -click==8.1.8 - # via - # -r requirements.txt - # antares-study-version - # black - # dask -cloudpickle==3.1.1 - # via - # -r requirements.txt - # dask -contourpy==1.3.2 - # via - # -r requirements.txt - # matplotlib -coverage[toml]==7.8.0 - # via pytest-cov -cycler==0.12.1 - # via - # -r requirements.txt - # matplotlib -dask==2025.4.0 - # via - # -r requirements.txt - # linopy -deprecation==2.1.0 - # via - # -r requirements.txt - # linopy - # pypsa -distlib==0.3.9 - # via virtualenv -filelock==3.18.0 - # via virtualenv -fonttools==4.57.0 - # via - # -r requirements.txt - # matplotlib -fsspec==2025.3.2 - # via - # -r requirements.txt - # dask -geopandas==1.0.1 - # via - # -r requirements.txt - # pypsa -highspy==1.10.0 - # via - # -r requirements.txt - # pypsa -identify==2.6.10 - # via pre-commit -idna==3.10 - # via - # -r requirements.txt - # requests -immutabledict==4.2.1 - # via - # -r requirements.txt - # ortools -importlib-metadata==8.6.1 - # via - # -r requirements.txt - # dask -iniconfig==2.1.0 - # via pytest -install-jdk==1.1.0 - # via antlr4-tools -isort==5.12.0 - # via -r requirements-dev.in -kiwisolver==1.4.8 - # via - # -r requirements.txt - # matplotlib -linopy==0.5.3 - # via - # -r requirements.txt - # pypsa -locket==1.0.0 - # via - # -r requirements.txt - # partd -matplotlib==3.10.1 - # via - # -r requirements.txt - # pypsa - # seaborn -mypy==1.7.1 - # via -r requirements-dev.in -mypy-extensions==1.1.0 - # via - # black - # mypy -netcdf4==1.7.2 - # via - # -r requirements.txt - # pypsa -networkx==3.4.2 - # via - # -r requirements.txt - # pypsa -nodeenv==1.9.1 - # via pre-commit -numexpr==2.10.2 - # via - # -r requirements.txt - # linopy -numpy==1.26.4 - # via - # -r requirements.txt - # antares-craft - # antares-timeseries-generation - # bottleneck - # cftime - # contourpy - # geopandas - # highspy - # linopy - # matplotlib - # netcdf4 - # numexpr - # ortools - # pandas - # pandas-stubs - # pyogrio - # pypsa - # scipy - # seaborn - # shapely - # xarray -ortools==9.9.3963 - # via -r requirements.txt -packaging==25.0 - # via - # -r requirements.txt - # black - # dask - # deprecation - # geopandas - # matplotlib - # pyogrio - # pytest - # xarray -pandas==2.2.3 - # via - # -r requirements-dev.in - # -r requirements.txt - # antares-craft - # antares-study-version - # antares-timeseries-generation - # geopandas - # ortools - # pypsa - # seaborn - # xarray -pandas-stubs==2.2.3.250308 - # via -r requirements-dev.in -partd==1.4.2 - # via - # -r requirements.txt - # dask -pathspec==0.12.1 - # via black -pillow==11.2.1 - # via - # -r requirements.txt - # matplotlib -platformdirs==4.3.7 - # via - # black - # virtualenv -pluggy==1.5.0 - # via pytest -polars==1.27.1 - # via - # -r requirements.txt - # linopy -pre-commit==3.5.0 - # via -r requirements-dev.in -protobuf==6.32.0 - # via - # -r requirements.txt - # ortools -psutil==7.0.0 - # via - # -r requirements.txt - # antares-craft -py==1.11.0 - # via pytest -pydantic==2.11.7 - # via - # -r requirements.txt - # antares-craft -pydantic-core==2.33.2 - # via - # -r requirements.txt - # pydantic -pyogrio==0.10.0 - # via - # -r requirements.txt - # geopandas -pyparsing==3.2.3 - # via - # -r requirements.txt - # matplotlib -pyproj==3.7.1 - # via - # -r requirements.txt - # geopandas -pytest==7.0.1 - # via - # -r requirements-dev.in - # pytest-cov -pytest-cov==6.1.1 - # via -r requirements-dev.in -python-dateutil==2.9.0.post0 - # via - # -r requirements.txt - # matplotlib - # pandas -pytz==2025.2 - # via - # -r requirements.txt - # pandas -pyyaml==6.0.2 - # via - # -r requirements.txt - # antares-timeseries-generation - # dask - # pre-commit -requests==2.32.3 - # via - # -r requirements.txt - # antares-craft -scipy==1.10.1 - # via - # -r requirements.txt - # linopy - # pypsa -seaborn==0.13.2 - # via - # -r requirements.txt - # pypsa -shapely==2.0.7 - # via - # -r requirements.txt - # geopandas - # pypsa -six==1.17.0 - # via - # -r requirements.txt - # anytree - # python-dateutil -tomli==2.2.1 - # via pytest -toolz==1.0.0 - # via - # -r requirements.txt - # dask - # linopy - # partd -tqdm==4.67.1 - # via - # -r requirements.txt - # linopy -types-pytz==2025.2.0.20250326 - # via pandas-stubs -types-pyyaml==6.0.12.20250402 - # via -r requirements-dev.in -typing-extensions==4.13.2 - # via - # -r requirements.txt - # mypy - # pydantic - # pydantic-core - # typing-inspection -typing-inspection==0.4.1 - # via - # -r requirements.txt - # pydantic -tzdata==2025.2 - # via - # -r requirements.txt - # pandas -urllib3==2.4.0 - # via - # -r requirements.txt - # requests -validators==0.34.0 - # via - # -r requirements.txt - # pypsa -virtualenv==20.30.0 - # via pre-commit -xarray==2025.3.1 - # via - # -r requirements.txt - # linopy - # pypsa -zipp==3.21.0 - # via - # -r requirements.txt - # importlib-metadata diff --git a/requirements-doc.txt b/requirements-doc.txt deleted file mode 100644 index 18546371..00000000 --- a/requirements-doc.txt +++ /dev/null @@ -1,4 +0,0 @@ -mkdocs -mkdocs-material -mkdocs-material-extensions -mkdocstrings-python \ No newline at end of file diff --git a/requirements.in b/requirements.in deleted file mode 100644 index db8b30ab..00000000 --- a/requirements.in +++ /dev/null @@ -1,8 +0,0 @@ -numpy -ortools==9.9.3963 -scipy==1.10.1 -antlr4-python3-runtime==4.13.1 -PyYAML~=6.0.1 -pydantic -antares_craft>=0.3 -anytree==2.12.1 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 8fcd7bbf..00000000 --- a/requirements.txt +++ /dev/null @@ -1,196 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile --output-file=requirements.txt requirements.in -# -absl-py==2.3.1 - # via ortools -annotated-types==0.7.0 - # via pydantic -antares-craft==0.3.0 - # via -r requirements.in -antares-study-version==1.0.15 - # via antares-craft -antares-timeseries-generation==0.1.7 - # via antares-craft -antlr4-python3-runtime==4.13.1 - # via -r requirements.in -anytree==2.12.1 - # via -r requirements.in -bottleneck==1.4.2 - # via linopy -certifi==2025.1.31 - # via - # netcdf4 - # pyogrio - # pyproj - # requests -cftime==1.6.4.post1 - # via netcdf4 -charset-normalizer==3.4.1 - # via requests -click==8.1.8 - # via - # antares-study-version - # dask -cloudpickle==3.1.1 - # via dask -contourpy==1.3.2 - # via matplotlib -cycler==0.12.1 - # via matplotlib -dask==2025.4.0 - # via linopy -deprecation==2.1.0 - # via - # linopy - # pypsa -fonttools==4.57.0 - # via matplotlib -fsspec==2025.3.2 - # via dask -geopandas==1.0.1 - # via pypsa -highspy==1.10.0 - # via pypsa -idna==3.10 - # via requests -immutabledict==4.2.1 - # via ortools -importlib-metadata==8.6.1 - # via dask -kiwisolver==1.4.8 - # via matplotlib -linopy==0.5.3 - # via pypsa -locket==1.0.0 - # via partd -matplotlib==3.10.1 - # via - # pypsa - # seaborn -netcdf4==1.7.2 - # via pypsa -networkx==3.4.2 - # via pypsa -numexpr==2.10.2 - # via linopy -numpy==1.26.4 - # via - # -r requirements.in - # antares-craft - # antares-timeseries-generation - # bottleneck - # cftime - # contourpy - # geopandas - # highspy - # linopy - # matplotlib - # netcdf4 - # numexpr - # ortools - # pandas - # pyogrio - # pypsa - # scipy - # seaborn - # shapely - # xarray -ortools==9.9.3963 - # via -r requirements.in -packaging==25.0 - # via - # dask - # deprecation - # geopandas - # matplotlib - # pyogrio - # xarray -pandas==2.2.3 - # via - # antares-craft - # antares-study-version - # antares-timeseries-generation - # geopandas - # ortools - # pypsa - # seaborn - # xarray -partd==1.4.2 - # via dask -pillow==11.2.1 - # via matplotlib -polars==1.27.1 - # via linopy -protobuf==6.32.0 - # via ortools -psutil==7.0.0 - # via antares-craft -pydantic==2.11.7 - # via - # -r requirements.in - # antares-craft -pydantic-core==2.33.2 - # via pydantic -pyogrio==0.10.0 - # via geopandas -pyparsing==3.2.3 - # via matplotlib -pyproj==3.7.1 - # via geopandas -python-dateutil==2.9.0.post0 - # via - # matplotlib - # pandas -pytz==2025.2 - # via pandas -pyyaml==6.0.2 - # via - # -r requirements.in - # antares-timeseries-generation - # dask -requests==2.32.3 - # via antares-craft -scipy==1.10.1 - # via - # -r requirements.in - # linopy - # pypsa -seaborn==0.13.2 - # via pypsa -shapely==2.0.7 - # via - # geopandas - # pypsa -six==1.17.0 - # via - # anytree - # python-dateutil -toolz==1.0.0 - # via - # dask - # linopy - # partd -tqdm==4.67.1 - # via linopy -typing-extensions==4.13.2 - # via - # pydantic - # pydantic-core - # typing-inspection -typing-inspection==0.4.1 - # via pydantic -tzdata==2025.2 - # via pandas -urllib3==2.4.0 - # via requests -validators==0.34.0 - # via pypsa -xarray==2025.3.1 - # via - # linopy - # pypsa -zipp==3.21.0 - # via importlib-metadata diff --git a/src/gems/expression/__init__.py b/src/gems/expression/__init__.py index 1eafc790..2e0c0752 100644 --- a/src/gems/expression/__init__.py +++ b/src/gems/expression/__init__.py @@ -13,11 +13,6 @@ from .copy import CopyVisitor, copy_expression from .degree import ExpressionDegreeVisitor, compute_degree from .evaluate import EvaluationContext, EvaluationVisitor, ValueProvider, evaluate -from .evaluate_parameters import ( - ParameterResolver, - ParameterValueProvider, - resolve_parameters, -) from .expression import ( AdditionNode, CeilNode, diff --git a/src/gems/expression/context_adder.py b/src/gems/expression/context_adder.py deleted file mode 100644 index 07ee97fd..00000000 --- a/src/gems/expression/context_adder.py +++ /dev/null @@ -1,65 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from dataclasses import dataclass - -from . import CopyVisitor -from .expression import ( - ComponentParameterNode, - ComponentVariableNode, - ExpressionNode, - ParameterNode, - ProblemParameterNode, - ProblemVariableNode, - VariableNode, -) -from .visitor import visit - - -@dataclass(frozen=True) -class ContextAdder(CopyVisitor): - """ - Simply copies the whole AST but associates all variables and parameters - to the provided component ID. - """ - - component_id: str - - def variable(self, node: VariableNode) -> ExpressionNode: - return ComponentVariableNode(self.component_id, node.name) - - def parameter(self, node: ParameterNode) -> ExpressionNode: - return ComponentParameterNode(self.component_id, node.name) - - def comp_variable(self, node: ComponentVariableNode) -> ExpressionNode: - raise ValueError( - "This expression has already been associated to another component." - ) - - def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: - raise ValueError( - "This expression has already been associated to another component." - ) - - def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: - raise ValueError( - "This expression has already been associated to another component." - ) - - def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: - raise ValueError( - "This expression has already been associated to another component." - ) - - -def add_component_context(id: str, expression: ExpressionNode) -> ExpressionNode: - return visit(expression, ContextAdder(id)) diff --git a/src/gems/expression/copy.py b/src/gems/expression/copy.py index 37eae951..fe87cf04 100644 --- a/src/gems/expression/copy.py +++ b/src/gems/expression/copy.py @@ -18,8 +18,7 @@ AllTimeSumNode, CeilNode, ComparisonNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, ExpressionNode, FloorNode, LiteralNode, @@ -28,8 +27,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, ScenarioOperatorNode, TimeEvalNode, TimeShiftNode, @@ -62,22 +60,6 @@ def variable(self, node: VariableNode) -> ExpressionNode: def parameter(self, node: ParameterNode) -> ExpressionNode: return ParameterNode(node.name) - def comp_variable(self, node: ComponentVariableNode) -> ExpressionNode: - return ComponentVariableNode(node.component_id, node.name) - - def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: - return ComponentParameterNode(node.component_id, node.name) - - def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: - return ProblemVariableNode( - node.component_id, node.name, node.time_index, node.scenario_index - ) - - def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: - return ProblemParameterNode( - node.component_id, node.name, node.time_index, node.scenario_index - ) - def time_shift(self, node: TimeShiftNode) -> ExpressionNode: return TimeShiftNode(visit(node.operand, self), visit(node.time_shift, self)) @@ -115,6 +97,12 @@ def maximum(self, node: MaxNode) -> ExpressionNode: def minimum(self, node: MinNode) -> ExpressionNode: return MinNode([visit(op, self) for op in node.operands]) + def dual(self, node: DualNode) -> ExpressionNode: + return DualNode(node.constraint_id) + + def reduced_cost(self, node: ReducedCostNode) -> ExpressionNode: + return ReducedCostNode(node.variable_id) + def copy_expression(expression: ExpressionNode) -> ExpressionNode: return visit(expression, CopyVisitor()) diff --git a/src/gems/expression/degree.py b/src/gems/expression/degree.py index 220650c1..6bd6f4c4 100644 --- a/src/gems/expression/degree.py +++ b/src/gems/expression/degree.py @@ -14,24 +14,24 @@ import gems.expression.scenario_operator from gems.expression.expression import ( + AdditionNode, AllTimeSumNode, + BinaryOperatorNode, CeilNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, FloorNode, MaxNode, MinNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, TimeEvalNode, TimeShiftNode, TimeSumNode, + UnaryOperatorNode, ) from .expression import ( - AdditionNode, ComparisonNode, DivisionNode, ExpressionNode, @@ -79,18 +79,6 @@ def variable(self, node: VariableNode) -> int | float: def parameter(self, node: ParameterNode) -> int | float: return 0 - def comp_variable(self, node: ComponentVariableNode) -> int | float: - return 1 - - def comp_parameter(self, node: ComponentParameterNode) -> int | float: - return 0 - - def pb_variable(self, node: ProblemVariableNode) -> int | float: - return 1 - - def pb_parameter(self, node: ProblemParameterNode) -> int | float: - return 0 - def time_shift(self, node: TimeShiftNode) -> int | float: return visit(node.operand, self) @@ -128,6 +116,12 @@ def maximum(self, node: MaxNode) -> int | float: def minimum(self, node: MinNode) -> int | float: return 0 if all(visit(op, self) == 0 for op in node.operands) else math.inf + def dual(self, node: DualNode) -> int | float: + return math.inf + + def reduced_cost(self, node: ReducedCostNode) -> int | float: + return math.inf + def compute_degree(expression: ExpressionNode) -> int | float: return visit(expression, ExpressionDegreeVisitor()) @@ -145,3 +139,16 @@ def is_linear(expr: ExpressionNode) -> bool: True if the expression is linear with respect to variables. """ return compute_degree(expr) <= 1 + + +def contains_dual_or_reduced_cost(expr: ExpressionNode) -> bool: + """Return True if expr contains any DualNode or ReducedCostNode.""" + if isinstance(expr, (DualNode, ReducedCostNode)): + return True + if isinstance(expr, (AdditionNode, MaxNode, MinNode)): + return any(contains_dual_or_reduced_cost(o) for o in expr.operands) + if isinstance(expr, BinaryOperatorNode): + return contains_dual_or_reduced_cost(expr.left) or contains_dual_or_reduced_cost(expr.right) + if isinstance(expr, UnaryOperatorNode): + return contains_dual_or_reduced_cost(expr.operand) + return False diff --git a/src/gems/expression/equality.py b/src/gems/expression/equality.py index b96ddaa0..094f5810 100644 --- a/src/gems/expression/equality.py +++ b/src/gems/expression/equality.py @@ -29,15 +29,13 @@ AllTimeSumNode, BinaryOperatorNode, CeilNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, FloorNode, MaxNode, MinNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, ScenarioOperatorNode, TimeEvalNode, TimeShiftNode, @@ -81,22 +79,6 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool: return self.variable(left, right) if isinstance(left, ParameterNode) and isinstance(right, ParameterNode): return self.parameter(left, right) - if isinstance(left, ComponentVariableNode) and isinstance( - right, ComponentVariableNode - ): - return self.comp_variable(left, right) - if isinstance(left, ComponentParameterNode) and isinstance( - right, ComponentParameterNode - ): - return self.comp_parameter(left, right) - if isinstance(left, ProblemVariableNode) and isinstance( - right, ProblemVariableNode - ): - return self.problem_variable(left, right) - if isinstance(left, ProblemParameterNode) and isinstance( - right, ProblemParameterNode - ): - return self.problem_parameter(left, right) if isinstance(left, TimeShiftNode) and isinstance(right, TimeShiftNode): return self.time_shift(left, right) if isinstance(left, TimeEvalNode) and isinstance(right, TimeEvalNode): @@ -123,6 +105,10 @@ def visit(self, left: ExpressionNode, right: ExpressionNode) -> bool: return self.maximum(left, right) if isinstance(left, MinNode) and isinstance(right, MinNode): return self.minimum(left, right) + if isinstance(left, DualNode) and isinstance(right, DualNode): + return self.dual(left, right) + if isinstance(left, ReducedCostNode) and isinstance(right, ReducedCostNode): + return self.reduced_cost(left, right) raise NotImplementedError(f"Equality not implemented for {left.__class__}") def literal(self, left: LiteralNode, right: LiteralNode) -> bool: @@ -162,36 +148,6 @@ def variable(self, left: VariableNode, right: VariableNode) -> bool: def parameter(self, left: ParameterNode, right: ParameterNode) -> bool: return left.name == right.name - def comp_variable( - self, left: ComponentVariableNode, right: ComponentVariableNode - ) -> bool: - return left.name == right.name and left.component_id == right.component_id - - def comp_parameter( - self, left: ComponentParameterNode, right: ComponentParameterNode - ) -> bool: - return left.name == right.name and left.component_id == right.component_id - - def problem_variable( - self, left: ProblemVariableNode, right: ProblemVariableNode - ) -> bool: - return ( - left.name == right.name - and left.component_id == right.component_id - and left.time_index == right.time_index - and left.scenario_index == right.scenario_index - ) - - def problem_parameter( - self, left: ProblemParameterNode, right: ProblemParameterNode - ) -> bool: - return ( - left.name == right.name - and left.component_id == right.component_id - and left.time_index == right.time_index - and left.scenario_index == right.scenario_index - ) - def time_shift(self, left: TimeShiftNode, right: TimeShiftNode) -> bool: return self.visit(left.time_shift, right.time_shift) and self.visit( left.operand, right.operand @@ -243,6 +199,12 @@ def minimum(self, left: MinNode, right: MinNode) -> bool: self.visit(l, r) for l, r in zip(left.operands, right.operands) ) + def dual(self, left: DualNode, right: DualNode) -> bool: + return left.constraint_id == right.constraint_id + + def reduced_cost(self, left: ReducedCostNode, right: ReducedCostNode) -> bool: + return left.variable_id == right.variable_id + def expressions_equal( left: ExpressionNode, right: ExpressionNode, abs_tol: float = 0, rel_tol: float = 0 diff --git a/src/gems/expression/evaluate.py b/src/gems/expression/evaluate.py index 2c850eff..fdc40a34 100644 --- a/src/gems/expression/evaluate.py +++ b/src/gems/expression/evaluate.py @@ -18,15 +18,13 @@ from gems.expression.expression import ( AllTimeSumNode, CeilNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, FloorNode, MaxNode, MinNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, TimeEvalNode, TimeShiftNode, TimeSumNode, @@ -51,20 +49,10 @@ class ValueProvider(ABC): """ @abstractmethod - def get_variable_value(self, name: str) -> float: - ... - - @abstractmethod - def get_parameter_value(self, name: str) -> float: - ... + def get_variable_value(self, name: str) -> float: ... @abstractmethod - def get_component_variable_value(self, component_id: str, name: str) -> float: - ... - - @abstractmethod - def get_component_parameter_value(self, component_id: str, name: str) -> float: - ... + def get_parameter_value(self, name: str) -> float: ... @dataclass(frozen=True) @@ -83,12 +71,6 @@ def get_variable_value(self, name: str) -> float: def get_parameter_value(self, name: str) -> float: return self.parameters[name] - def get_component_variable_value(self, component_id: str, name: str) -> float: - raise NotImplementedError() - - def get_component_parameter_value(self, component_id: str, name: str) -> float: - raise NotImplementedError() - @dataclass(frozen=True) class EvaluationVisitor(ExpressionVisitorOperations[float]): @@ -111,18 +93,6 @@ def variable(self, node: VariableNode) -> float: def parameter(self, node: ParameterNode) -> float: return self.context.get_parameter_value(node.name) - def comp_parameter(self, node: ComponentParameterNode) -> float: - return self.context.get_component_parameter_value(node.component_id, node.name) - - def comp_variable(self, node: ComponentVariableNode) -> float: - return self.context.get_component_variable_value(node.component_id, node.name) - - def pb_parameter(self, node: ProblemParameterNode) -> float: - raise ValueError("Should not reach here.") - - def pb_variable(self, node: ProblemVariableNode) -> float: - raise ValueError("Should not reach here.") - def time_shift(self, node: TimeShiftNode) -> float: raise NotImplementedError() @@ -156,6 +126,12 @@ def maximum(self, node: MaxNode) -> float: def minimum(self, node: MinNode) -> float: return min(visit(op, self) for op in node.operands) + def dual(self, node: DualNode) -> float: + raise NotImplementedError("dual() is not statically evaluable.") + + def reduced_cost(self, node: ReducedCostNode) -> float: + raise NotImplementedError("reduced_cost() is not statically evaluable.") + def evaluate(expression: ExpressionNode, value_provider: ValueProvider) -> float: return visit(expression, EvaluationVisitor(value_provider)) diff --git a/src/gems/expression/evaluate_parameters.py b/src/gems/expression/evaluate_parameters.py deleted file mode 100644 index b605af54..00000000 --- a/src/gems/expression/evaluate_parameters.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from abc import ABC, abstractmethod -from dataclasses import dataclass - -from gems.expression.evaluate import ValueProvider - -from .copy import CopyVisitor -from .expression import ( - ComponentParameterNode, - ExpressionNode, - LiteralNode, - ParameterNode, -) -from .visitor import visit - - -class ParameterValueProvider(ABC): - @abstractmethod - def get_parameter_value(self, name: str) -> float: - ... - - @abstractmethod - def get_component_parameter_value(self, component_id: str, name: str) -> float: - ... - - -@dataclass(frozen=True) -class ParameterResolver(CopyVisitor): - """ - Duplicates the AST with replacement of parameter nodes by literal nodes. - """ - - context: ParameterValueProvider - - def parameter(self, node: ParameterNode) -> ExpressionNode: - value: float = self.context.get_parameter_value(node.name) - return LiteralNode(value) - - def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: - value: float = self.context.get_component_parameter_value( - node.component_id, node.name - ) - return LiteralNode(value) - - -def resolve_parameters( - expression: ExpressionNode, parameter_provider: ParameterValueProvider -) -> ExpressionNode: - return visit(expression, ParameterResolver(parameter_provider)) diff --git a/src/gems/expression/expression.py b/src/gems/expression/expression.py index 68dcee3d..5dae3244 100644 --- a/src/gems/expression/expression.py +++ b/src/gems/expression/expression.py @@ -180,156 +180,6 @@ def param(name: str) -> ParameterNode: return ParameterNode(name) -@dataclass(frozen=True, eq=False) -class ComponentParameterNode(ExpressionNode): - """ - Represents one parameter of one component. - - When building actual equations for a system, - we need to associated each parameter to its - actual component, at some point. - """ - - component_id: str - name: str - - -def comp_param(component_id: str, name: str) -> ComponentParameterNode: - return ComponentParameterNode(component_id, name) - - -@dataclass(frozen=True) -class TimeIndex: - pass - - -@dataclass(frozen=True) -class NoTimeIndex(TimeIndex): - """ - Some values do not depend on the timestep, this index should be used for them - (think one variable for all timesteps). - """ - - pass - - -@dataclass(frozen=True) -class TimeShift(TimeIndex): - """ - Represents the current timestep + a shift. - - This should only be used for nodes that actually depend on the timestep, - never for time-independent nodes (constant parameters ...). - """ - - timeshift: int - - -@dataclass(frozen=True) -class TimeStep(TimeIndex): - """ - Represents a given timestep, independently of the current timestep. - - This should only be used for nodes that actually depend on the timestep, - never for time-independent nodes (constant parameters ...). - """ - - timestep: int - - -@dataclass(frozen=True) -class ScenarioIndex: - pass - - -@dataclass(frozen=True) -class NoScenarioIndex(ScenarioIndex): - """ - Some values do not depend on the scenario, this index should be used for them - (think one variable for all timesteps). - """ - - pass - - -@dataclass(frozen=True) -class CurrentScenarioIndex(ScenarioIndex): - """ - Represents the current scenario. - - This should only be used for nodes that actually depend on the scenario, - never for scenario-independent nodes (constant parameters ...). - """ - - pass - - -@dataclass(frozen=True) -class OneScenarioIndex(ScenarioIndex): - """ - Represents a given scenario out of all scenarios. - - This should only be used for nodes that actually depend on the scenario, - never for scenario-independent nodes (constant parameters ...). - """ - - scenario: int - - -@dataclass(frozen=True, eq=False) -class ProblemParameterNode(ExpressionNode): - """ - Represents one parameter of the optimization problem - """ - - component_id: str - name: str - time_index: TimeIndex - scenario_index: ScenarioIndex - - -def problem_param( - component_id: str, name: str, time_index: TimeIndex, scenario_index: ScenarioIndex -) -> ProblemParameterNode: - return ProblemParameterNode(component_id, name, time_index, scenario_index) - - -@dataclass(frozen=True, eq=False) -class ComponentVariableNode(ExpressionNode): - """ - Represents one variable of one component. - - When building actual equations for a system, - we need to associated each variable to its - actual component, at some point. - """ - - component_id: str - name: str - - -def comp_var(component_id: str, name: str) -> ComponentVariableNode: - return ComponentVariableNode(component_id, name) - - -@dataclass(frozen=True, eq=False) -class ProblemVariableNode(ExpressionNode): - """ - Represents one variable of the optimization problem - """ - - component_id: str - name: str - time_index: TimeIndex - scenario_index: ScenarioIndex - - -def problem_var( - component_id: str, name: str, time_index: TimeIndex, scenario_index: ScenarioIndex -) -> ProblemVariableNode: - return ProblemVariableNode(component_id, name, time_index, scenario_index) - - @dataclass(frozen=True, eq=False) class LiteralNode(ExpressionNode): value: float @@ -479,6 +329,16 @@ def __post_init__(self) -> None: ) +@dataclass(frozen=True, eq=False) +class DualNode(ExpressionNode): + constraint_id: str + + +@dataclass(frozen=True, eq=False) +class ReducedCostNode(ExpressionNode): + variable_id: str + + def sum_expressions(expressions: Sequence[ExpressionNode]) -> ExpressionNode: if len(expressions) == 0: return LiteralNode(0) diff --git a/src/gems/expression/indexing.py b/src/gems/expression/indexing.py index c7858204..0460e61c 100644 --- a/src/gems/expression/indexing.py +++ b/src/gems/expression/indexing.py @@ -21,9 +21,8 @@ AllTimeSumNode, CeilNode, ComparisonNode, - ComponentParameterNode, - ComponentVariableNode, DivisionNode, + DualNode, ExpressionNode, FloorNode, LiteralNode, @@ -34,8 +33,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, ScenarioOperatorNode, TimeEvalNode, TimeShiftNode, @@ -47,24 +45,13 @@ class IndexingStructureProvider(ABC): @abstractmethod - def get_parameter_structure(self, name: str) -> IndexingStructure: - ... + def get_parameter_structure(self, name: str) -> IndexingStructure: ... @abstractmethod - def get_variable_structure(self, name: str) -> IndexingStructure: - ... + def get_variable_structure(self, name: str) -> IndexingStructure: ... @abstractmethod - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - ... - - @abstractmethod - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - ... + def get_constraint_structure(self, name: str) -> IndexingStructure: ... @dataclass(frozen=True) @@ -118,26 +105,6 @@ def parameter(self, node: ParameterNode) -> IndexingStructure: scenario = self.context.get_parameter_structure(node.name).scenario == True return IndexingStructure(time, scenario) - def comp_variable(self, node: ComponentVariableNode) -> IndexingStructure: - return self.context.get_component_variable_structure( - node.component_id, node.name - ) - - def comp_parameter(self, node: ComponentParameterNode) -> IndexingStructure: - return self.context.get_component_parameter_structure( - node.component_id, node.name - ) - - def pb_variable(self, node: ProblemVariableNode) -> IndexingStructure: - raise ValueError( - "Not relevant to compute indexation on already instantiated problem variables." - ) - - def pb_parameter(self, node: ProblemParameterNode) -> IndexingStructure: - raise ValueError( - "Not relevant to compute indexation on already instantiated problem parameters." - ) - def time_shift(self, node: TimeShiftNode) -> IndexingStructure: return visit(node.operand, self) @@ -175,6 +142,12 @@ def maximum(self, node: MaxNode) -> IndexingStructure: def minimum(self, node: MinNode) -> IndexingStructure: return self._combine(node.operands) + def dual(self, node: DualNode) -> IndexingStructure: + return self.context.get_constraint_structure(node.constraint_id) + + def reduced_cost(self, node: ReducedCostNode) -> IndexingStructure: + return self.context.get_variable_structure(node.variable_id) + def compute_indexation( expression: ExpressionNode, provider: IndexingStructureProvider diff --git a/src/gems/expression/operators_expansion.py b/src/gems/expression/operators_expansion.py deleted file mode 100644 index fa4538ba..00000000 --- a/src/gems/expression/operators_expansion.py +++ /dev/null @@ -1,240 +0,0 @@ -import dataclasses -from dataclasses import dataclass -from typing import Callable, TypeVar, Union - -from gems.expression import CopyVisitor, ExpressionNode, sum_expressions, visit -from gems.expression.expression import ( - AllTimeSumNode, - ComponentParameterNode, - ComponentVariableNode, - CurrentScenarioIndex, - NoScenarioIndex, - NoTimeIndex, - OneScenarioIndex, - ProblemParameterNode, - ProblemVariableNode, - ScenarioOperatorNode, - TimeEvalNode, - TimeShift, - TimeShiftNode, - TimeStep, - TimeSumNode, - problem_param, - problem_var, -) -from gems.expression.indexing import IndexingStructureProvider - -ExpressionEvaluator = Callable[[ExpressionNode], int] - - -@dataclass(frozen=True) -class ProblemDimensions: - """ - Dimensions for the simulation window - """ - - timesteps_count: int - scenarios_count: int - - -@dataclass(frozen=True) -class ProblemIndex: - """ - Index of an object in the simulation window. - """ - - timestep: int - scenario: int - - -@dataclass(frozen=True) -class OperatorsExpansion(CopyVisitor): - """ - Replaces aggregators (time sum, expectations ...) by their - arithmetic expansion. - - This will allow to easily translate it to a plain linear expression later on, - without complex handling of operators. - - The obtained expression only contains `ProblemVariableNode` for variables - and `ProblemParameterNode` parameters. - """ - - timesteps_count: int - scenarios_count: int - evaluator: ExpressionEvaluator - structure_provider: IndexingStructureProvider - - def comp_variable(self, node: ComponentVariableNode) -> ExpressionNode: - structure = self.structure_provider.get_component_variable_structure( - node.component_id, node.name - ) - time_index = TimeShift(0) if structure.time else NoTimeIndex() - scenario_index = ( - CurrentScenarioIndex() if structure.scenario else NoScenarioIndex() - ) - return problem_var(node.component_id, node.name, time_index, scenario_index) - - def comp_parameter(self, node: ComponentParameterNode) -> ExpressionNode: - structure = self.structure_provider.get_component_parameter_structure( - node.component_id, node.name - ) - time_index = TimeShift(0) if structure.time else NoTimeIndex() - scenario_index = ( - CurrentScenarioIndex() if structure.scenario else NoScenarioIndex() - ) - return problem_param(node.component_id, node.name, time_index, scenario_index) - - def time_shift(self, node: TimeShiftNode) -> ExpressionNode: - shift = self.evaluator(node.time_shift) - operand = visit(node.operand, self) - return apply_timeshift(operand, shift) - - def time_eval(self, node: TimeEvalNode) -> ExpressionNode: - timestep = self.evaluator(node.eval_time) - operand = visit(node.operand, self) - return apply_timestep(operand, timestep) - - def time_sum(self, node: TimeSumNode) -> ExpressionNode: - from_shift = self.evaluator(node.from_time) - to_shift = self.evaluator(node.to_time) - operand = visit(node.operand, self) - nodes = [] - for t in range(from_shift, to_shift + 1): - nodes.append(apply_timeshift(operand, t)) - return sum_expressions(nodes) - - def all_time_sum(self, node: AllTimeSumNode) -> ExpressionNode: - nodes = [] - operand = visit(node.operand, self) - for t in range(self.timesteps_count): - # if we sum previously "evaluated" variables for example x[0], it's ok - nodes.append(apply_timestep(operand, t, allow_existing=True)) - return sum_expressions(nodes) - - def scenario_operator(self, node: ScenarioOperatorNode) -> ExpressionNode: - if node.name != "Expectation": - raise ValueError(f"Scenario operator not supported: {node.name}") - nodes = [] - operand = visit(node.operand, self) - for t in range(self.scenarios_count): - nodes.append(apply_scenario(operand, t)) - return sum_expressions(nodes) / self.scenarios_count - - def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: - raise ValueError("Should not reach") - - def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: - raise ValueError("Should not reach") - - -def expand_operators( - expression: ExpressionNode, - dimensions: ProblemDimensions, - evaluator: ExpressionEvaluator, - structure_provider: IndexingStructureProvider, -) -> ExpressionNode: - return visit( - expression, - OperatorsExpansion( - dimensions.timesteps_count, - dimensions.scenarios_count, - evaluator, - structure_provider, - ), - ) - - -TimeIndexedNode = TypeVar( - "TimeIndexedNode", bound=Union[ProblemParameterNode, ProblemVariableNode] -) - - -@dataclass(frozen=True) -class ApplyTimeShift(CopyVisitor): - """ - Shifts all underlying expressions. - """ - - timeshift: int - - def _apply_timeshift(self, node: TimeIndexedNode) -> TimeIndexedNode: - current_index = node.time_index - if isinstance(current_index, TimeShift): - return dataclasses.replace( - node, time_index=TimeShift(current_index.timeshift + self.timeshift) - ) - if isinstance(current_index, TimeStep): - return dataclasses.replace( - node, time_index=TimeStep(current_index.timestep + self.timeshift) - ) - if isinstance(current_index, NoTimeIndex): - return node - raise ValueError("Unknown time index type.") - - def pb_parameter(self, node: ProblemParameterNode) -> ProblemParameterNode: - return self._apply_timeshift(node) - - def pb_variable(self, node: ProblemVariableNode) -> ProblemVariableNode: - return self._apply_timeshift(node) - - -def apply_timeshift(expression: ExpressionNode, timeshift: int) -> ExpressionNode: - return visit(expression, ApplyTimeShift(timeshift)) - - -@dataclass(frozen=True) -class ApplyTimeStep(CopyVisitor): - """ - Applies timestep to all underlying expressions. - """ - - timestep: int - allow_existing: bool = False - - def _apply_timestep(self, node: TimeIndexedNode) -> TimeIndexedNode: - current_index = node.time_index - if isinstance(current_index, TimeShift): - return dataclasses.replace( - node, time_index=TimeStep(current_index.timeshift + self.timestep) - ) - if isinstance(current_index, TimeStep): - if not self.allow_existing: - raise ValueError( - "Cannot override a previously defined timestep (for example (x[0])[1])." - ) - return node - if isinstance(current_index, NoTimeIndex): - return node - raise ValueError("Unknown time index type.") - - def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: - return self._apply_timestep(node) - - def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: - return self._apply_timestep(node) - - -def apply_timestep( - expression: ExpressionNode, timestep: int, allow_existing: bool = False -) -> ExpressionNode: - return visit(expression, ApplyTimeStep(timestep, allow_existing)) - - -@dataclass(frozen=True) -class ApplyScenario(CopyVisitor): - scenario: int - - def pb_parameter(self, node: ProblemParameterNode) -> ExpressionNode: - if isinstance(node.scenario_index, NoScenarioIndex): - return node - return dataclasses.replace(node, scenario_index=OneScenarioIndex(self.scenario)) - - def pb_variable(self, node: ProblemVariableNode) -> ExpressionNode: - if isinstance(node.scenario_index, NoScenarioIndex): - return node - return dataclasses.replace(node, scenario_index=OneScenarioIndex(self.scenario)) - - -def apply_scenario(expression: ExpressionNode, scenario: int) -> ExpressionNode: - return visit(expression, ApplyScenario(scenario)) diff --git a/src/gems/expression/parsing/parse_expression.py b/src/gems/expression/parsing/parse_expression.py index 47316bf8..57a326a9 100644 --- a/src/gems/expression/parsing/parse_expression.py +++ b/src/gems/expression/parsing/parse_expression.py @@ -9,7 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import Set from antlr4 import CommonTokenStream, InputStream @@ -20,8 +20,10 @@ from gems.expression.expression import ( Comparator, ComparisonNode, + DualNode, PortFieldAggregatorNode, PortFieldNode, + ReducedCostNode, maximum, minimum, ) @@ -33,11 +35,12 @@ @dataclass(frozen=True) class ModelIdentifiers: """ - Allows to distinguish between parameters and variables. + Allows to distinguish between parameters, variables, and constraints. """ variables: Set[str] parameters: Set[str] + constraints: Set[str] = field(default_factory=set) def is_variable(self, identifier: str) -> bool: return identifier in self.variables @@ -175,12 +178,35 @@ def visitAllTimeSum(self, ctx: ExprParser.AllTimeSumContext) -> ExpressionNode: shifted_expr = ctx.expr().accept(self) # type: ignore return shifted_expr.time_sum() + def _visit_dual(self, arg_exprs: list) -> ExpressionNode: + if len(arg_exprs) != 1: + raise ValueError("dual() requires exactly 1 argument.") + cid = arg_exprs[0].getText() # type: ignore + if cid not in self.identifiers.constraints: + raise ValueError(f"'{cid}' is not a constraint of the model.") + return DualNode(cid) + + def _visit_reduced_cost(self, arg_exprs: list) -> ExpressionNode: + if len(arg_exprs) != 1: + raise ValueError("reduced_cost() requires exactly 1 argument.") + vid = arg_exprs[0].getText() # type: ignore + if vid not in self.identifiers.variables: + raise ValueError(f"'{vid}' is not a variable of the model.") + return ReducedCostNode(vid) + # Visit a parse tree produced by ExprParser#function. def visitFunction(self, ctx: ExprParser.FunctionContext) -> ExpressionNode: function_name: str = ctx.IDENTIFIER().getText() # type: ignore arg_list = ctx.argList() # type: ignore + arg_exprs = arg_list.expr() if arg_list is not None else [] # type: ignore + + if function_name == "dual": + return self._visit_dual(arg_exprs) + if function_name == "reduced_cost": + return self._visit_reduced_cost(arg_exprs) + args: list[ExpressionNode] = ( - [expr.accept(self) for expr in arg_list.expr()] # type: ignore + [expr.accept(self) for expr in arg_exprs] # type: ignore if arg_list is not None else [] ) diff --git a/src/gems/expression/port_operator.py b/src/gems/expression/port_operator.py index 875d4f32..16f5c7d4 100644 --- a/src/gems/expression/port_operator.py +++ b/src/gems/expression/port_operator.py @@ -14,15 +14,9 @@ Operators that allow port manipulation of expressions """ -from abc import ABC from dataclasses import dataclass -@dataclass(frozen=True) -class PortOperator(ABC): - pass - - @dataclass(frozen=True) class PortAggregator: pass diff --git a/src/gems/expression/print.py b/src/gems/expression/print.py index d819185c..a7ca3850 100644 --- a/src/gems/expression/print.py +++ b/src/gems/expression/print.py @@ -16,16 +16,14 @@ from gems.expression.expression import ( AllTimeSumNode, CeilNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, ExpressionNode, FloorNode, MaxNode, MinNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, TimeEvalNode, TimeShiftNode, TimeSumNode, @@ -99,20 +97,6 @@ def variable(self, node: VariableNode) -> str: def parameter(self, node: ParameterNode) -> str: return node.name - def comp_variable(self, node: ComponentVariableNode) -> str: - return f"{node.component_id}.{node.name}" - - def comp_parameter(self, node: ComponentParameterNode) -> str: - return f"{node.component_id}.{node.name}" - - def pb_variable(self, node: ProblemVariableNode) -> str: - # TODO - return f"{node.component_id}.{node.name}" - - def pb_parameter(self, node: ProblemParameterNode) -> str: - # TODO - return f"{node.component_id}.{node.name}" - def time_shift(self, node: TimeShiftNode) -> str: return f"({visit(node.operand, self)}.shift({visit(node.time_shift, self)}))" @@ -146,6 +130,12 @@ def maximum(self, node: MaxNode) -> str: def minimum(self, node: MinNode) -> str: return "min(" + ", ".join(visit(op, self) for op in node.operands) + ")" + def dual(self, node: DualNode) -> str: + return f"dual({node.constraint_id})" + + def reduced_cost(self, node: ReducedCostNode) -> str: + return f"reduced_cost({node.variable_id})" + def print_expr(expression: ExpressionNode) -> str: return visit(expression, PrinterVisitor()) diff --git a/src/gems/expression/visitor.py b/src/gems/expression/visitor.py index dc5ad42c..a2687ed8 100644 --- a/src/gems/expression/visitor.py +++ b/src/gems/expression/visitor.py @@ -23,9 +23,8 @@ AllTimeSumNode, CeilNode, ComparisonNode, - ComponentParameterNode, - ComponentVariableNode, DivisionNode, + DualNode, ExpressionNode, FloorNode, LiteralNode, @@ -36,8 +35,7 @@ ParameterNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, ScenarioOperatorNode, TimeEvalNode, TimeShiftNode, @@ -58,96 +56,67 @@ class ExpressionVisitor(ABC, Generic[T]): """ @abstractmethod - def literal(self, node: LiteralNode) -> T: - ... + def literal(self, node: LiteralNode) -> T: ... @abstractmethod - def negation(self, node: NegationNode) -> T: - ... + def negation(self, node: NegationNode) -> T: ... @abstractmethod - def addition(self, node: AdditionNode) -> T: - ... + def addition(self, node: AdditionNode) -> T: ... @abstractmethod - def multiplication(self, node: MultiplicationNode) -> T: - ... + def multiplication(self, node: MultiplicationNode) -> T: ... @abstractmethod - def division(self, node: DivisionNode) -> T: - ... + def division(self, node: DivisionNode) -> T: ... @abstractmethod - def comparison(self, node: ComparisonNode) -> T: - ... + def comparison(self, node: ComparisonNode) -> T: ... @abstractmethod - def variable(self, node: VariableNode) -> T: - ... + def variable(self, node: VariableNode) -> T: ... @abstractmethod - def parameter(self, node: ParameterNode) -> T: - ... + def parameter(self, node: ParameterNode) -> T: ... @abstractmethod - def comp_parameter(self, node: ComponentParameterNode) -> T: - ... + def time_shift(self, node: TimeShiftNode) -> T: ... @abstractmethod - def comp_variable(self, node: ComponentVariableNode) -> T: - ... + def time_eval(self, node: TimeEvalNode) -> T: ... @abstractmethod - def pb_parameter(self, node: ProblemParameterNode) -> T: - ... + def time_sum(self, node: TimeSumNode) -> T: ... @abstractmethod - def pb_variable(self, node: ProblemVariableNode) -> T: - ... + def all_time_sum(self, node: AllTimeSumNode) -> T: ... @abstractmethod - def time_shift(self, node: TimeShiftNode) -> T: - ... + def scenario_operator(self, node: ScenarioOperatorNode) -> T: ... @abstractmethod - def time_eval(self, node: TimeEvalNode) -> T: - ... + def port_field(self, node: PortFieldNode) -> T: ... @abstractmethod - def time_sum(self, node: TimeSumNode) -> T: - ... + def port_field_aggregator(self, node: PortFieldAggregatorNode) -> T: ... @abstractmethod - def all_time_sum(self, node: AllTimeSumNode) -> T: - ... + def floor(self, node: FloorNode) -> T: ... @abstractmethod - def scenario_operator(self, node: ScenarioOperatorNode) -> T: - ... + def ceil(self, node: CeilNode) -> T: ... @abstractmethod - def port_field(self, node: PortFieldNode) -> T: - ... + def maximum(self, node: MaxNode) -> T: ... @abstractmethod - def port_field_aggregator(self, node: PortFieldAggregatorNode) -> T: - ... + def minimum(self, node: MinNode) -> T: ... @abstractmethod - def floor(self, node: FloorNode) -> T: - ... + def dual(self, node: DualNode) -> T: ... @abstractmethod - def ceil(self, node: CeilNode) -> T: - ... - - @abstractmethod - def maximum(self, node: MaxNode) -> T: - ... - - @abstractmethod - def minimum(self, node: MinNode) -> T: - ... + def reduced_cost(self, node: ReducedCostNode) -> T: ... def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: @@ -162,14 +131,6 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: return visitor.variable(root) elif isinstance(root, ParameterNode): return visitor.parameter(root) - elif isinstance(root, ComponentParameterNode): - return visitor.comp_parameter(root) - elif isinstance(root, ComponentVariableNode): - return visitor.comp_variable(root) - elif isinstance(root, ProblemParameterNode): - return visitor.pb_parameter(root) - elif isinstance(root, ProblemVariableNode): - return visitor.pb_variable(root) elif isinstance(root, AdditionNode): return visitor.addition(root) elif isinstance(root, MultiplicationNode): @@ -200,6 +161,10 @@ def visit(root: ExpressionNode, visitor: ExpressionVisitor[T]) -> T: return visitor.maximum(root) elif isinstance(root, MinNode): return visitor.minimum(root) + elif isinstance(root, DualNode): + return visitor.dual(root) + elif isinstance(root, ReducedCostNode): + return visitor.reduced_cost(root) raise ValueError(f"Unknown expression node type {root.__class__}") diff --git a/src/gems/main/main.py b/src/gems/main/main.py index cbaa074e..59e21578 100644 --- a/src/gems/main/main.py +++ b/src/gems/main/main.py @@ -10,43 +10,32 @@ # # This file is part of the Antares project. - from pathlib import Path from typing import Dict, List, Optional from gems.model.library import Library from gems.model.parsing import parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.simulation import TimeBlock, build_problem -from gems.study import DataBase +from gems.optim_config.parsing import OptimConfig +from gems.simulation import DecomposedProblems, build_couplings, dump_couplings +from gems.study import Study +from gems.study.data import DataBase from gems.study.parsing import parse_cli, parse_yaml_components -from gems.study.resolve_components import ( - System, - build_data_base, - build_network, - consistency_check, - resolve_system, -) - - -class AntaresTimeSeriesImportError(Exception): - pass +from gems.study.resolve_components import build_data_base, resolve_system +from gems.study.runner import run_study +from gems.study.system import System -def input_libs(yaml_lib_paths: List[Path]) -> dict[str, Library]: +def input_libs(yaml_lib_paths: List[Path]) -> Dict[str, Library]: yaml_libraries = [] yaml_library_ids = set() - for path in yaml_lib_paths: with path.open("r") as file: yaml_lib = parse_yaml_library(file) - if yaml_lib.id in yaml_library_ids: raise ValueError(f"The identifier '{yaml_lib.id}' is defined twice") - yaml_libraries.append(yaml_lib) yaml_library_ids.add(yaml_lib.id) - return resolve_library(yaml_libraries) @@ -55,50 +44,30 @@ def input_database(study_path: Path, timeseries_path: Optional[Path]) -> DataBas return build_data_base(parse_yaml_components(comp), timeseries_path) -def input_study(study_path: Path, librairies: dict[str, Library]) -> System: +def input_system(study_path: Path, libraries: Dict[str, Library]) -> System: with study_path.open() as comp: - return resolve_system(parse_yaml_components(comp), librairies) - - -def main_cli() -> None: - parsed_args = parse_cli() - - lib_dict = input_libs(parsed_args.models_path) - study = input_study(parsed_args.components_path, lib_dict) - - models = {} - for lib in lib_dict.values(): - models.update(lib.models) - - consistency_check(study.components, models) + return resolve_system(parse_yaml_components(comp), libraries) - try: - database = input_database( - parsed_args.components_path, parsed_args.timeseries_path - ) - except UnboundLocalError: - raise AntaresTimeSeriesImportError( - f"An error occurred while importing time series." - ) +def _write_structure_txt( + decomposed: DecomposedProblems, + optim_config: OptimConfig, + output_dir: Path, +) -> None: + dump_couplings(build_couplings(decomposed, optim_config), output_dir) - network = build_network(study) - timeblock = TimeBlock(1, list(range(parsed_args.duration))) - scenario = parsed_args.nb_scenarios +# --------------------------------------------------------------------------- +# CLI entry point +# --------------------------------------------------------------------------- - try: - problem = build_problem(network, database, timeblock, scenario) - except IndexError as e: - raise IndexError( - f"{e}. Did parameters '--duration' and '--scenario' were correctly set?" - ) - - status = problem.solver.Solve() - print("status : ", status) - - print("final average cost : ", problem.solver.Objective().Value()) +def main_cli() -> None: + parsed_args = parse_cli() + run_study( + study_dir=parsed_args.study_dir, + optim_config_path=parsed_args.optim_config_path, + ) if __name__ == "__main__": diff --git a/src/gems/model/__init__.py b/src/gems/model/__init__.py index be48aeab..2546b888 100644 --- a/src/gems/model/__init__.py +++ b/src/gems/model/__init__.py @@ -10,7 +10,7 @@ # # This file is part of the Antares project. -from .common import ProblemContext, ValueType +from .common import ValueType from .constraint import Constraint from .model import Model, ModelPort, model from .parameter import Parameter, float_parameter, int_parameter diff --git a/src/gems/model/common.py b/src/gems/model/common.py index b4342735..d37855c0 100644 --- a/src/gems/model/common.py +++ b/src/gems/model/common.py @@ -20,10 +20,4 @@ class ValueType(Enum): CONTINUOUS = "CONTINUOUS" INTEGER = "INTEGER" - BOOLEAN = "BOOLEAN" - - -class ProblemContext(Enum): - OPERATIONAL = 0 - INVESTMENT = 1 - COUPLING = 2 + BINARY = "BINARY" diff --git a/src/gems/model/constraint.py b/src/gems/model/constraint.py index 5fac6507..889b227e 100644 --- a/src/gems/model/constraint.py +++ b/src/gems/model/constraint.py @@ -24,7 +24,6 @@ literal, ) from gems.expression.print import print_expr -from gems.model.common import ProblemContext @dataclass @@ -39,7 +38,6 @@ class Constraint: expression: ExpressionNode lower_bound: ExpressionNode = field(default=literal(-float("inf"))) upper_bound: ExpressionNode = field(default=literal(float("inf"))) - context: ProblemContext = field(default=ProblemContext.OPERATIONAL) def __post_init__( self, diff --git a/src/gems/model/model.py b/src/gems/model/model.py index bd5c8b5c..eb10cc29 100644 --- a/src/gems/model/model.py +++ b/src/gems/model/model.py @@ -17,6 +17,7 @@ """ import itertools +import warnings from dataclasses import dataclass, field, replace from typing import Any, Dict, Iterable, Optional @@ -31,38 +32,105 @@ # TODO: Introduce bool_variable ? -def _make_structure_provider(model: "Model") -> IndexingStructureProvider: +def _make_structure_provider( + parameters: Dict[str, Parameter], + variables: Dict[str, Variable], + constraints: Optional[Dict[str, Constraint]] = None, +) -> IndexingStructureProvider: + # Pre-compute constraint structures using a params/vars-only base provider. + # Constraint expressions cannot contain dual()/reduced_cost(), so the base + # provider's get_constraint_structure is never invoked during this step. + constraint_structures: Dict[str, IndexingStructure] = {} + if constraints: + class _BaseProvider(IndexingStructureProvider): + def get_parameter_structure(self, name: str) -> IndexingStructure: + return parameters[name].structure + + def get_variable_structure(self, name: str) -> IndexingStructure: + return variables[name].structure + + def get_constraint_structure(self, name: str) -> IndexingStructure: + raise NotImplementedError( + f"Constraint structure for '{name}' not available at this stage." + ) + + base = _BaseProvider() + for cname, c in constraints.items(): + try: + constraint_structures[cname] = compute_indexation(c.expression, base) + except ValueError: + # Constraints containing unresolved port fields (sum_connections) + # cannot be indexed before port resolution; fall back to the most + # general structure so callers can still proceed. + constraint_structures[cname] = IndexingStructure( + time=True, scenario=True + ) + class Provider(IndexingStructureProvider): def get_parameter_structure(self, name: str) -> IndexingStructure: - return model.parameters[name].structure + return parameters[name].structure def get_variable_structure(self, name: str) -> IndexingStructure: - return model.variables[name].structure + return variables[name].structure - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - raise NotImplementedError( - "Cannot have parameters associated to components in models." - ) - - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - raise NotImplementedError( - "Cannot have variables associated to components in models." - ) + def get_constraint_structure(self, name: str) -> IndexingStructure: + return constraint_structures[name] return Provider() +def _normalize_objective_contributions( + contributions: Dict[str, ExpressionNode], + parameters: Dict[str, Parameter], + variables: Dict[str, Variable], +) -> Dict[str, ExpressionNode]: + """ + Tolerate absence of expec() in objective contributions that carry a residual + scenario dimension (IndexingStructure(time=False, scenario=True)). + + Such contributions are automatically wrapped with expec(), applying + expectation (average-over-scenarios) semantics, and a UserWarning is emitted + so authors can add expec() explicitly at their convenience. + + Contributions that are already fully scalar, or already wrapped in expec(), + are returned unchanged with no warning. + + This implements the iso-format behaviour of Antares Simulator v10.0.0 (Issue #76). + + TODO: This auto-wrapping is a temporary compatibility shim. Once Antares Simulator + natively supports the expec() operator in objective contributions, this function + should be removed and authors should be required to write expec() explicitly. + """ + + provider = _make_structure_provider(parameters, variables) + result: Dict[str, ExpressionNode] = {} + for contrib_id, expr in contributions.items(): + structure = compute_indexation(expr, provider) + if structure == IndexingStructure(time=False, scenario=True): + warnings.warn( + f"Objective contribution '{contrib_id}' has a scenario dimension " + "but no explicit expec() operator. " + "Expectation semantics (average over scenarios) are applied " + "automatically. Add expec() explicitly to suppress this warning.", + UserWarning, + stacklevel=4, + ) + expr = expr.expec() + result[contrib_id] = expr + return result + + def _is_objective_contribution_valid( model: "Model", objective_contribution: ExpressionNode ) -> bool: if not is_linear(objective_contribution): raise ValueError("Objective contribution must be a linear expression.") - data_structure_provider = _make_structure_provider(model) + data_structure_provider = _make_structure_provider( + model.parameters, + model.variables, + {**model.constraints, **model.binding_constraints}, + ) objective_structure = compute_indexation( objective_contribution, data_structure_provider ) @@ -103,8 +171,6 @@ class Model: parameters: Dict[str, Parameter] = field(default_factory=dict) variables: Dict[str, Variable] = field(default_factory=dict) objective_contributions: Optional[Dict[str, ExpressionNode]] = None - objective_operational_contribution: Optional[ExpressionNode] = None - objective_investment_contribution: Optional[ExpressionNode] = None ports: Dict[str, ModelPort] = field(default_factory=dict) port_fields_definitions: Dict[PortFieldId, PortFieldDefinition] = field( default_factory=dict @@ -112,15 +178,6 @@ class Model: extra_outputs: Optional[Dict[str, ExpressionNode]] = None def __post_init__(self) -> None: - if self.objective_operational_contribution: - _is_objective_contribution_valid( - self, self.objective_operational_contribution - ) - - if self.objective_investment_contribution: - _is_objective_contribution_valid( - self, self.objective_investment_contribution - ) # Validate each contribution if present if self.objective_contributions: for expr in self.objective_contributions.values(): @@ -157,8 +214,6 @@ def model( parameters: Optional[Iterable[Parameter]] = None, variables: Optional[Iterable[Variable]] = None, objective_contributions: Optional[Dict[str, ExpressionNode]] = None, - objective_operational_contribution: Optional[ExpressionNode] = None, - objective_investment_contribution: Optional[ExpressionNode] = None, inter_block_dyn: bool = False, ports: Optional[Iterable[ModelPort]] = None, port_fields_definitions: Optional[Iterable[PortFieldDefinition]] = None, @@ -167,6 +222,17 @@ def model( """ Utility method to create Models from relaxed arguments """ + # Build dicts upfront so we can inspect indexing structure before Model construction. + params_dict = {p.name: p for p in parameters} if parameters else {} + vars_dict = {v.name: v for v in variables} if variables else {} + + # Auto-wrap any objective contribution that has a residual scenario dimension + # without an explicit expec() (Issue #76 / Antares Simulator v10.0.0 iso-format). + if objective_contributions: + objective_contributions = _normalize_objective_contributions( + objective_contributions, params_dict, vars_dict + ) + existing_port_names = {} if ports: for port in ports: @@ -183,11 +249,9 @@ def model( binding_constraints=( {c.name: c for c in binding_constraints} if binding_constraints else {} ), - parameters={p.name: p for p in parameters} if parameters else {}, - variables={v.name: v for v in variables} if variables else {}, + parameters=params_dict, + variables=vars_dict, objective_contributions=objective_contributions, - objective_investment_contribution=objective_investment_contribution, - objective_operational_contribution=objective_operational_contribution, inter_block_dyn=inter_block_dyn, ports=existing_port_names, port_fields_definitions=( diff --git a/src/gems/model/parsing.py b/src/gems/model/parsing.py index abb99428..aa75e21a 100644 --- a/src/gems/model/parsing.py +++ b/src/gems/model/parsing.py @@ -19,21 +19,21 @@ from gems.utils import ModifiedBaseModel -def parse_yaml_library(input: typing.TextIO) -> "InputLibrary": +def parse_yaml_library(input: typing.TextIO) -> "LibrarySchema": tree = safe_load(input) try: - return InputLibrary.model_validate(tree["library"]) + return LibrarySchema.model_validate(tree["library"]) except ValidationError as e: raise ValueError(f"An error occurred during parsing: {e}") -class InputParameter(ModifiedBaseModel): +class ParameterSchema(ModifiedBaseModel): id: str time_dependent: bool = False scenario_dependent: bool = False -class InputVariable(ModifiedBaseModel): +class VariableSchema(ModifiedBaseModel): id: str time_dependent: bool = True scenario_dependent: bool = True @@ -46,63 +46,65 @@ class InputVariable(ModifiedBaseModel): ) -class InputConstraint(ModifiedBaseModel): +class ConstraintSchema(ModifiedBaseModel): id: str expression: str lower_bound: Optional[str] = None upper_bound: Optional[str] = None -class InputField(ModifiedBaseModel): +class FieldSchema(ModifiedBaseModel): id: str -class InputPortType(ModifiedBaseModel): +class PortTypeSchema(ModifiedBaseModel): id: str - fields: List[InputField] = Field(default_factory=list) + fields: List[FieldSchema] = Field(default_factory=list) description: Optional[str] = None -class InputModelPort(ModifiedBaseModel): +class ModelPortSchema(ModifiedBaseModel): id: str type: str -class InputPortFieldDefinition(ModifiedBaseModel): +class PortFieldDefinitionSchema(ModifiedBaseModel): port: str field: str definition: str -class InputObjectiveContribution(ModifiedBaseModel): +class ObjectiveContributionSchema(ModifiedBaseModel): id: str expression: str @dataclass -class InputExtraOutput(ModifiedBaseModel): +class ExtraOutputSchema(ModifiedBaseModel): id: str expression: str -class InputModel(ModifiedBaseModel): +class ModelSchema(ModifiedBaseModel): id: str - parameters: List[InputParameter] = Field(default_factory=list) - variables: List[InputVariable] = Field(default_factory=list) - ports: List[InputModelPort] = Field(default_factory=list) - port_field_definitions: List[InputPortFieldDefinition] = Field(default_factory=list) - binding_constraints: List[InputConstraint] = Field(default_factory=list) - constraints: List[InputConstraint] = Field(default_factory=list) - objective_contributions: List[InputObjectiveContribution] = Field( + parameters: List[ParameterSchema] = Field(default_factory=list) + variables: List[VariableSchema] = Field(default_factory=list) + ports: List[ModelPortSchema] = Field(default_factory=list) + port_field_definitions: List[PortFieldDefinitionSchema] = Field( + default_factory=list + ) + binding_constraints: List[ConstraintSchema] = Field(default_factory=list) + constraints: List[ConstraintSchema] = Field(default_factory=list) + objective_contributions: List[ObjectiveContributionSchema] = Field( default_factory=list, alias="objective-contributions" ) description: Optional[str] = None - extra_outputs: Optional[List[InputExtraOutput]] = None + extra_outputs: Optional[List[ExtraOutputSchema]] = None -class InputLibrary(ModifiedBaseModel): +class LibrarySchema(ModifiedBaseModel): id: str dependencies: List[str] = Field(default_factory=list) - port_types: List[InputPortType] = Field(default_factory=list) - models: List[InputModel] = Field(default_factory=list) + port_types: List[PortTypeSchema] = Field(default_factory=list) + models: List[ModelSchema] = Field(default_factory=list) description: Optional[str] = None diff --git a/src/gems/model/port.py b/src/gems/model/port.py index a13bce35..718a105f 100644 --- a/src/gems/model/port.py +++ b/src/gems/model/port.py @@ -29,15 +29,13 @@ AllTimeSumNode, BinaryOperatorNode, CeilNode, - ComponentParameterNode, - ComponentVariableNode, + DualNode, FloorNode, MaxNode, MinNode, PortFieldAggregatorNode, PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, + ReducedCostNode, ScenarioOperatorNode, TimeEvalNode, TimeShiftNode, @@ -131,26 +129,6 @@ def variable(self, node: VariableNode) -> None: def parameter(self, node: ParameterNode) -> None: pass - def comp_parameter(self, node: ComponentParameterNode) -> None: - raise ValueError( - "Port definition must not contain a parameter associated to a component." - ) - - def comp_variable(self, node: ComponentVariableNode) -> None: - raise ValueError( - "Port definition must not contain a variable associated to a component." - ) - - def pb_parameter(self, node: ProblemParameterNode) -> None: - raise ValueError( - "Port definition must not contain a parameter associated to a component." - ) - - def pb_variable(self, node: ProblemVariableNode) -> None: - raise ValueError( - "Port definition must not contain a variable associated to a component." - ) - def time_shift(self, node: TimeShiftNode) -> None: visit(node.operand, self) @@ -186,6 +164,12 @@ def minimum(self, node: MinNode) -> None: def port_field_aggregator(self, node: PortFieldAggregatorNode) -> None: raise ValueError("Port definition cannot contain port field aggregation.") + def dual(self, node: DualNode) -> None: + pass # dual() is permitted in port-field definitions + + def reduced_cost(self, node: ReducedCostNode) -> None: + pass # reduced_cost() is permitted in port-field definitions + def _validate_port_field_expression(definition: PortFieldDefinition) -> None: visit(definition.definition, _PortFieldExpressionChecker()) diff --git a/src/gems/model/probability_law.py b/src/gems/model/probability_law.py deleted file mode 100644 index 998cf96a..00000000 --- a/src/gems/model/probability_law.py +++ /dev/null @@ -1,55 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -Describes probability distributions used in the models -""" - -from abc import ABC -from dataclasses import dataclass -from typing import List - -import numpy as np - -from gems.expression.expression import ExpressionNode - - -class AbstractProbabilityLaw(ABC): - def get_sample(self, size: int) -> List[float]: - return NotImplemented - - -@dataclass(frozen=True) -class Normal(AbstractProbabilityLaw): - mean: ExpressionNode - standard_deviation: ExpressionNode - - def get_sample(self, size: int) -> List[float]: - return NotImplemented - - -@dataclass(frozen=True) -class Uniform(AbstractProbabilityLaw): - lower_bound: ExpressionNode - upper_bound: ExpressionNode - - def get_sample(self, size: int) -> List[float]: - return NotImplemented - - -@dataclass(frozen=True) -class UniformIntegers(AbstractProbabilityLaw): - lower_bound: ExpressionNode - upper_bound: ExpressionNode - - def get_sample(self, size: int) -> List[float]: - return NotImplemented diff --git a/src/gems/model/resolve_library.py b/src/gems/model/resolve_library.py index a2c6983e..fc30475e 100644 --- a/src/gems/model/resolve_library.py +++ b/src/gems/model/resolve_library.py @@ -12,6 +12,7 @@ from typing import Dict, List, Optional, Set from gems.expression import ExpressionNode, literal +from gems.expression.degree import contains_dual_or_reduced_cost from gems.expression.indexing_structure import IndexingStructure from gems.expression.parsing.parse_expression import ModelIdentifiers, parse_expression from gems.model import ( @@ -21,7 +22,6 @@ Parameter, PortField, PortType, - ProblemContext, ValueType, Variable, model, @@ -29,21 +29,21 @@ from gems.model.library import Library from gems.model.model import model from gems.model.parsing import ( - InputConstraint, - InputField, - InputLibrary, - InputModel, - InputModelPort, - InputParameter, - InputPortFieldDefinition, - InputPortType, - InputVariable, + ConstraintSchema, + FieldSchema, + LibrarySchema, + ModelPortSchema, + ModelSchema, + ParameterSchema, + PortFieldDefinitionSchema, + PortTypeSchema, + VariableSchema, ) from gems.model.port import PortFieldDefinition, port_field_def def resolve_library( - input_libs: List[InputLibrary], preloaded_libs: Optional[List[Library]] = None + input_libs: List[LibrarySchema], preloaded_libs: Optional[List[Library]] = None ) -> Dict[str, Library]: """ Converts parsed data into an actually usable library of models. @@ -105,7 +105,7 @@ def _add_preloaded_port_types_to_current_lib( def _add_resolved_dependent_port_types_to_current_lib( output_lib_dict: Dict[str, Library], treated_lib_ids: Set[str], - cur_yaml_lib: InputLibrary, + cur_yaml_lib: LibrarySchema, current_lib: Library, ) -> None: done_dependencies = set(cur_yaml_lib.dependencies) & treated_lib_ids @@ -120,7 +120,7 @@ def _update_treated_libs_and_import_stack( def _resolve_lib( - current_lib: Library, cur_yaml_lib: InputLibrary, output_lib: Dict[str, Library] + current_lib: Library, cur_yaml_lib: LibrarySchema, output_lib: Dict[str, Library] ) -> None: port_types = [_convert_port_type(p) for p in cur_yaml_lib.port_types] port_types_dict = dict((p.id, p) for p in port_types) @@ -136,7 +136,10 @@ def _resolve_lib( if cur_yaml_lib_model_ids.count(id) > 1: raise Exception(f"Model {id} is defined twice") - models = [_resolve_model(m, current_lib.port_types) for m in cur_yaml_lib.models] + models = [ + _resolve_model(m, current_lib.port_types, current_lib.id) + for m in cur_yaml_lib.models + ] models_dict = dict((m.id, m) for m in models) @@ -154,28 +157,49 @@ def _add_dependencies_to_stack( import_stack.append(first_dependency) -def _convert_field(field: InputField) -> PortField: +def _convert_field(field: FieldSchema) -> PortField: return PortField(name=field.id) -def _convert_port_type(port_type: InputPortType) -> PortType: +def _convert_port_type(port_type: PortTypeSchema) -> PortType: return PortType( id=port_type.id, fields=[_convert_field(f) for f in port_type.fields] ) -def _resolve_model(input_model: InputModel, port_types: Dict[str, PortType]) -> Model: +def _forbid_dual_or_rc(expr: ExpressionNode, context: str) -> None: + if contains_dual_or_reduced_cost(expr): + raise ValueError( + f"Operators dual/reduced_cost are not allowed in {context}." + ) + + +def _resolve_model( + input_model: ModelSchema, port_types: Dict[str, PortType], library_id: str +) -> Model: identifiers = ModelIdentifiers( variables={v.id for v in input_model.variables}, parameters={p.id for p in input_model.parameters}, + constraints={c.id for c in input_model.binding_constraints} + | {c.id for c in input_model.constraints}, ) + binding_constraints = [ + _to_constraint(c, identifiers) for c in input_model.binding_constraints + ] + constraints = [_to_constraint(c, identifiers) for c in input_model.constraints] + + for c in binding_constraints + constraints: + _forbid_dual_or_rc(c.expression, f"constraint '{c.name}'") + objective_contributions = None if input_model.objective_contributions: objective_contributions = { contrib.id: parse_expression(contrib.expression, identifiers) for contrib in input_model.objective_contributions } + for oid, expr in objective_contributions.items(): + _forbid_dual_or_rc(expr, f"objective contribution '{oid}'") extra_outputs = ( { @@ -186,7 +210,7 @@ def _resolve_model(input_model: InputModel, port_types: Dict[str, PortType]) -> else None ) return model( - id=input_model.id, + id=f"{library_id}.{input_model.id}", parameters=[_to_parameter(p) for p in input_model.parameters], variables=[_to_variable(v, identifiers) for v in input_model.variables], ports=[_resolve_model_port(p, port_types) for p in input_model.ports], @@ -194,23 +218,21 @@ def _resolve_model(input_model: InputModel, port_types: Dict[str, PortType]) -> _resolve_field_definition(d, identifiers) for d in input_model.port_field_definitions ], - binding_constraints=[ - _to_constraint(c, identifiers) for c in input_model.binding_constraints - ], - constraints=[_to_constraint(c, identifiers) for c in input_model.constraints], + binding_constraints=binding_constraints, + constraints=constraints, objective_contributions=objective_contributions, extra_outputs=extra_outputs, ) def _resolve_model_port( - port: InputModelPort, port_types: Dict[str, PortType] + port: ModelPortSchema, port_types: Dict[str, PortType] ) -> ModelPort: return ModelPort(port_name=port.id, port_type=port_types[port.type]) def _resolve_field_definition( - definition: InputPortFieldDefinition, ids: ModelIdentifiers + definition: PortFieldDefinitionSchema, ids: ModelIdentifiers ) -> PortFieldDefinition: return port_field_def( port_name=definition.port, @@ -219,7 +241,7 @@ def _resolve_field_definition( ) -def _to_parameter(param: InputParameter) -> Parameter: +def _to_parameter(param: ParameterSchema) -> Parameter: return Parameter( name=param.id, type=ValueType.CONTINUOUS, @@ -235,21 +257,22 @@ def _to_expression_if_present( return parse_expression(expr, identifiers) -def _to_variable(var: InputVariable, identifiers: ModelIdentifiers) -> Variable: +def _to_variable(var: VariableSchema, identifiers: ModelIdentifiers) -> Variable: return Variable( name=var.id, - data_type={"continuous": ValueType.CONTINUOUS, "integer": ValueType.INTEGER}[ - var.variable_type - ], + data_type={ + "continuous": ValueType.CONTINUOUS, + "integer": ValueType.INTEGER, + "binary": ValueType.BINARY, + }[var.variable_type], structure=IndexingStructure(var.time_dependent, var.scenario_dependent), lower_bound=_to_expression_if_present(var.lower_bound, identifiers), upper_bound=_to_expression_if_present(var.upper_bound, identifiers), - context=ProblemContext.OPERATIONAL, ) def _to_constraint( - constraint: InputConstraint, identifiers: ModelIdentifiers + constraint: ConstraintSchema, identifiers: ModelIdentifiers ) -> Constraint: lb = _to_expression_if_present(constraint.lower_bound, identifiers) ub = _to_expression_if_present(constraint.upper_bound, identifiers) diff --git a/src/gems/model/variable.py b/src/gems/model/variable.py index 9d4ed33d..5899ca9e 100644 --- a/src/gems/model/variable.py +++ b/src/gems/model/variable.py @@ -17,7 +17,7 @@ from gems.expression.degree import is_constant from gems.expression.equality import expressions_equal_if_present from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext, ValueType +from gems.model.common import ValueType @dataclass @@ -31,7 +31,6 @@ class Variable: lower_bound: Optional[ExpressionNode] upper_bound: Optional[ExpressionNode] structure: IndexingStructure - context: ProblemContext def __post_init__(self) -> None: if self.lower_bound and not is_constant(self.lower_bound): @@ -59,19 +58,15 @@ def int_variable( lower_bound: Optional[ExpressionNode] = None, upper_bound: Optional[ExpressionNode] = None, structure: IndexingStructure = IndexingStructure(True, True), - context: ProblemContext = ProblemContext.OPERATIONAL, ) -> Variable: - return Variable( - name, ValueType.INTEGER, lower_bound, upper_bound, structure, context - ) + return Variable(name, ValueType.INTEGER, lower_bound, upper_bound, structure) def bool_var( name: str, structure: IndexingStructure = IndexingStructure(True, True), - context: ProblemContext = ProblemContext.OPERATIONAL, ) -> Variable: - return Variable(name, ValueType.BOOLEAN, literal(0), literal(1), structure, context) + return Variable(name, ValueType.BINARY, literal(0), literal(1), structure) def float_variable( @@ -79,8 +74,5 @@ def float_variable( lower_bound: Optional[ExpressionNode] = None, upper_bound: Optional[ExpressionNode] = None, structure: IndexingStructure = IndexingStructure(True, True), - context: ProblemContext = ProblemContext.OPERATIONAL, ) -> Variable: - return Variable( - name, ValueType.CONTINUOUS, lower_bound, upper_bound, structure, context - ) + return Variable(name, ValueType.CONTINUOUS, lower_bound, upper_bound, structure) diff --git a/src/gems/optim_config/__init__.py b/src/gems/optim_config/__init__.py new file mode 100644 index 00000000..c3ac0b6b --- /dev/null +++ b/src/gems/optim_config/__init__.py @@ -0,0 +1,29 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from .parsing import ( + ElementLocation, + OptimConfig, + ResolutionConfig, + ResolutionMode, + load_optim_config, + validate_optim_config, +) + +__all__ = [ + "ElementLocation", + "OptimConfig", + "ResolutionConfig", + "ResolutionMode", + "load_optim_config", + "validate_optim_config", +] diff --git a/src/gems/optim_config/parsing.py b/src/gems/optim_config/parsing.py new file mode 100644 index 00000000..5c3df92c --- /dev/null +++ b/src/gems/optim_config/parsing.py @@ -0,0 +1,336 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set + +from pydantic import Field, ValidationError, model_validator +from yaml import safe_load + +from gems.expression.expression import ( + AdditionNode, + BinaryOperatorNode, + ExpressionNode, + MaxNode, + MinNode, + UnaryOperatorNode, + VariableNode, +) +from gems.utils import ModifiedBaseModel + +if TYPE_CHECKING: + from gems.model.model import Model + from gems.study.system import System + + +class ElementLocation(str, Enum): + MASTER = "master" + SUBPROBLEMS = "subproblems" + MASTER_AND_SUBPROBLEMS = "master-and-subproblems" + + +class ElementLocationConfig(ModifiedBaseModel): + id: str + location: ElementLocation + + +class ModelDecompositionConfig(ModifiedBaseModel): + variables: List[ElementLocationConfig] = Field(default_factory=list) + constraints: List[ElementLocationConfig] = Field(default_factory=list) + objective_contributions: List[ElementLocationConfig] = Field(default_factory=list) + + +class OutOfBoundsMode(str, Enum): + CYCLIC = "cyclic" + DROP = "drop" + + +class OutOfBoundsConstraintConfig(ModifiedBaseModel): + id: str + mode: OutOfBoundsMode + + +class OutOfBoundsProcessingConfig(ModifiedBaseModel): + constraints: List[OutOfBoundsConstraintConfig] = Field(default_factory=list) + + +class ModelOptimConfig(ModifiedBaseModel): + id: str + model_decomposition: Optional[ModelDecompositionConfig] = None + out_of_bounds_processing: Optional[OutOfBoundsProcessingConfig] = None + + +class ResolutionMode(str, Enum): + FRONTAL = "frontal" + SEQUENTIAL_SUBPROBLEMS = "sequential-subproblems" + PARALLEL_SUBPROBLEMS = "parallel-subproblems" + BENDERS_DECOMPOSITION = "benders-decomposition" + + +class ResolutionConfig(ModifiedBaseModel): + mode: ResolutionMode = ResolutionMode.FRONTAL + block_length: Optional[int] = None + block_overlap: int = 0 + + @model_validator(mode="after") + def _block_length_required_for_windowed_modes(self) -> "ResolutionConfig": + windowed = { + ResolutionMode.SEQUENTIAL_SUBPROBLEMS, + ResolutionMode.PARALLEL_SUBPROBLEMS, + } + if self.mode in windowed and self.block_length is None: + raise ValueError(f"'block_length' is required for mode '{self.mode.value}'") + return self + + +class TimeScopeConfig(ModifiedBaseModel): + first_time_step: int = 0 + last_time_step: int = 0 + + +class SolverOptionsConfig(ModifiedBaseModel): + name: str = "highs" + logs: bool = False + parameters: str = "" + + def parsed_parameters(self) -> Dict[str, Any]: + """Parse 'KEY VALUE KEY2 VALUE2 ...' into a dict with numeric coercion.""" + if not self.parameters.strip(): + return {} + tokens = self.parameters.split() + if len(tokens) % 2 != 0: + raise ValueError( + f"parameters must be space-separated key-value pairs, got: {self.parameters!r}" + ) + result: Dict[str, Any] = {} + for i in range(0, len(tokens), 2): + key, raw = tokens[i], tokens[i + 1] + try: + result[key] = int(raw) + except ValueError: + try: + result[key] = float(raw) + except ValueError: + result[key] = raw + return result + + +class ScenarioScopeConfig(ModifiedBaseModel): + nb_scenarios: int = 1 + + @property + def scenario_ids(self) -> List[int]: + return list(range(self.nb_scenarios)) + + +class OptimConfig(ModifiedBaseModel): + time_scope: TimeScopeConfig = Field(default_factory=TimeScopeConfig) + solver_options: SolverOptionsConfig = Field(default_factory=SolverOptionsConfig) + scenario_scope: ScenarioScopeConfig = Field(default_factory=ScenarioScopeConfig) + resolution: ResolutionConfig = Field(default_factory=ResolutionConfig) + models: List[ModelOptimConfig] = Field(default_factory=list) + + +def load_optim_config(config_path: Path) -> Optional[OptimConfig]: + """Load optim-config.yml from the same directory as components_path. + + Returns None if the file does not exist. + Raises ValueError on parsing or validation failure. + """ + if not config_path.exists(): + return None + try: + with config_path.open() as config_file: + return OptimConfig.model_validate(safe_load(config_file)) + except ValidationError as e: + raise ValueError(f"Invalid {config_path.stem}: {e}") + + +_MASTER_LOCS: Set[ElementLocation] = { + ElementLocation.MASTER, + ElementLocation.MASTER_AND_SUBPROBLEMS, +} + + +def _collect_variable_names(expr: ExpressionNode) -> Set[str]: + """Recursively collect all variable names referenced in an expression.""" + if isinstance(expr, VariableNode): + return {expr.name} + if isinstance(expr, (AdditionNode, MaxNode, MinNode)): + result: Set[str] = set() + for operand in expr.operands: + result |= _collect_variable_names(operand) + return result + if isinstance(expr, UnaryOperatorNode): + return _collect_variable_names(expr.operand) + if isinstance(expr, BinaryOperatorNode): + return _collect_variable_names(expr.left) | _collect_variable_names(expr.right) + return set() + + +def _check_oob_constraint_ids( + oob_processing: OutOfBoundsProcessingConfig, + model: "Model", + model_config_id: str, + errors: List[str], +) -> None: + for constraint_config in oob_processing.constraints: + if ( + constraint_config.id not in model.constraints + and constraint_config.id not in model.binding_constraints + ): + errors.append( + f"Out-of-bounds constraint '{constraint_config.id}' not found in model '{model_config_id}'" + ) + + +def _check_id_existence( + decomposition: ModelDecompositionConfig, + model: "Model", + model_config_id: str, + errors: List[str], +) -> None: + for variable_config in decomposition.variables: + if variable_config.id not in model.variables: + errors.append( + f"Variable '{variable_config.id}' not found in model '{model_config_id}'" + ) + for constraint_config in decomposition.constraints: + if ( + constraint_config.id not in model.constraints + and constraint_config.id not in model.binding_constraints + ): + errors.append( + f"Constraint '{constraint_config.id}' not found in model '{model_config_id}'" + ) + obj_keys = set(model.objective_contributions or {}) + for obj_config in decomposition.objective_contributions: + if obj_config.id not in obj_keys: + errors.append( + f"Objective-contribution '{obj_config.id}' not found in model '{model_config_id}'" + ) + + +def _check_master_variables_not_time_dependent( + decomposition: ModelDecompositionConfig, + model: "Model", + model_config_id: str, + errors: List[str], +) -> None: + """Variables assigned to master or master-and-subproblems must not depend on time.""" + for variable_config in decomposition.variables: + if ( + variable_config.location in _MASTER_LOCS + and variable_config.id in model.variables + ): + if model.variables[variable_config.id].structure.time: + errors.append( + f"Variable '{variable_config.id}' in model '{model_config_id}' is time-dependent " + f"but is assigned to '{variable_config.location.value}'; " + "master variables must not depend on time" + ) + + +def _check_master_constraints_use_master_variables( + decomposition: ModelDecompositionConfig, + model: "Model", + model_config_id: str, + errors: List[str], +) -> None: + """Constraints in master must only reference variables in master or master-and-subproblems.""" + master_var_ids = { + variable_config.id + for variable_config in decomposition.variables + if variable_config.location in _MASTER_LOCS + and variable_config.id in model.variables + } + for constraint_config in decomposition.constraints: + if constraint_config.location == ElementLocation.MASTER: + constraint = model.constraints.get( + constraint_config.id + ) or model.binding_constraints.get(constraint_config.id) + if constraint is not None: + for var_name in sorted( + _collect_variable_names(constraint.expression) - master_var_ids + ): + errors.append( + f"Constraint '{constraint_config.id}' in model '{model_config_id}' references variable '{var_name}' " + "which is not assigned to master or master-and-subproblems" + ) + + +def _check_master_objectives_use_master_variables( + decomposition: ModelDecompositionConfig, + model: "Model", + model_config_id: str, + errors: List[str], +) -> None: + """Objective contributions in master must only reference variables in master or master-and-subproblems.""" + master_var_ids = { + variable_config.id + for variable_config in decomposition.variables + if variable_config.location in _MASTER_LOCS + and variable_config.id in model.variables + } + obj_contribs = model.objective_contributions or {} + for obj_config in decomposition.objective_contributions: + if obj_config.location == ElementLocation.MASTER: + expr = obj_contribs.get(obj_config.id) + if expr is not None: + for var_name in sorted(_collect_variable_names(expr) - master_var_ids): + errors.append( + f"Objective contribution '{obj_config.id}' in model '{model_config_id}' references variable '{var_name}' " + "which is not assigned to master or master-and-subproblems" + ) + + +def validate_optim_config(config: OptimConfig, system: "System") -> None: + """Cross-validate optim-config entries against the resolved system. + + Checks that every referenced ID exists, that master variables do not + depend on time, and that master constraints and objectives only reference + variables assigned to master or master-and-subproblems. + Raises ValueError listing all violations. + """ + models_in_system = {c.model.id: c.model for c in system.all_components} + errors: List[str] = [] + + for model_config in config.models: + model = models_in_system.get(model_config.id) + if model is None: + errors.append(f"Model '{model_config.id}' not found in system") + else: + if model_config.model_decomposition is not None: + decomposition = model_config.model_decomposition + _check_id_existence(decomposition, model, model_config.id, errors) + _check_master_variables_not_time_dependent( + decomposition, model, model_config.id, errors + ) + _check_master_constraints_use_master_variables( + decomposition, model, model_config.id, errors + ) + _check_master_objectives_use_master_variables( + decomposition, model, model_config.id, errors + ) + if model_config.out_of_bounds_processing is not None: + _check_oob_constraint_ids( + model_config.out_of_bounds_processing, + model, + model_config.id, + errors, + ) + + if errors: + raise ValueError( + f"Errors in optim config file:\n" + "\n".join(f" - {e}" for e in errors) + ) diff --git a/tests/unittests/expressions/linear_expressions/__init__.py b/src/gems/session/__init__.py similarity index 83% rename from tests/unittests/expressions/linear_expressions/__init__.py rename to src/gems/session/__init__.py index 058c6b22..aec4e2b6 100644 --- a/tests/unittests/expressions/linear_expressions/__init__.py +++ b/src/gems/session/__init__.py @@ -9,3 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. + +from .session import SimulationSession + +__all__ = ["SimulationSession"] diff --git a/src/gems/session/session.py b/src/gems/session/session.py new file mode 100644 index 00000000..252ee557 --- /dev/null +++ b/src/gems/session/session.py @@ -0,0 +1,223 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, List, Optional, Tuple +from uuid import uuid4 + +import xarray as xr + +from gems.optim_config.parsing import OptimConfig, ResolutionMode, load_optim_config +from gems.simulation.optimization import OptimizationProblem, build_problem +from gems.simulation.simulation_table import ( + SimulationTable, + SimulationTableBuilder, + merge_simulation_tables, +) +from gems.simulation.time_block import TimeBlock +from gems.study.folder import load_study +from gems.study.study import Study + + +@dataclass +class SimulationSession: + study: Study + optim_config: OptimConfig + run_id: str = field(default_factory=lambda: str(uuid4())) + output_dir: Optional[Path] = None + + @property + def scenario_ids(self) -> List[int]: + return list(range(self.optim_config.scenario_scope.nb_scenarios)) + + def run(self) -> SimulationTable: + """Entry point. Dispatches to the appropriate resolution strategy.""" + mode = self.optim_config.resolution.mode + if mode == ResolutionMode.FRONTAL: + return self._run_frontal() + elif mode == ResolutionMode.SEQUENTIAL_SUBPROBLEMS: + return self._run_sequential() + elif mode == ResolutionMode.PARALLEL_SUBPROBLEMS: + return self._run_parallel() + elif mode == ResolutionMode.BENDERS_DECOMPOSITION: + return self._run_benders() + raise ValueError(f"Unknown resolution mode: {mode}") + + # ------------------------------------------------------------------ + # Resolution strategies + # ------------------------------------------------------------------ + + def _run_frontal(self) -> SimulationTable: + block = TimeBlock( + 0, + list( + range( + self.optim_config.time_scope.first_time_step, + self.optim_config.time_scope.last_time_step + 1, + ) + ), + ) + _, table = self._run_block(block, scenario_ids=self.scenario_ids) + return table + + def _run_sequential(self) -> SimulationTable: + cfg = self.optim_config.resolution + block_length: int = cfg.block_length # type: ignore[assignment] + block_overlap: int = cfg.block_overlap + + tables: List[SimulationTable] = [] + for scenario_id in self.scenario_ids: + t_start = self.optim_config.time_scope.first_time_step + block_id = 0 + carry_over: Dict[Tuple[str, str], xr.DataArray] = {} + + while t_start < self.optim_config.time_scope.last_time_step: + end = min( + t_start + block_length, + self.optim_config.time_scope.last_time_step + 1, + ) + timesteps = list(range(t_start, end)) + block = TimeBlock(block_id, timesteps) + problem, table = self._run_block( + block, + scenario_ids=[scenario_id], + initial_values=carry_over or None, + ) + tables.append(table) + carry_over = self._extract_carry_over( + problem, local_index=len(timesteps) - 1 + ) + t_start += block_length - block_overlap + block_id += 1 + + return self._reduce(tables) + + def _run_parallel(self) -> SimulationTable: + cfg = self.optim_config.resolution + block_length: int = cfg.block_length # type: ignore[assignment] + + tables: List[SimulationTable] = [] + for scenario_id in self.scenario_ids: + starts = range( + self.optim_config.time_scope.first_time_step, + self.optim_config.time_scope.last_time_step + 1, + block_length, + ) + blocks = [ + TimeBlock( + i, + list( + range( + t, + min( + t + block_length, + self.optim_config.time_scope.last_time_step + 1, + ), + ) + ), + ) + for i, t in enumerate(starts) + ] + for block in blocks: + _, table = self._run_block(block, scenario_ids=[scenario_id]) + tables.append(table) + + return self._reduce(tables) + + def _run_benders(self) -> SimulationTable: + import pandas as pd + + from gems.simulation import ( + BendersRunner, + build_couplings, + build_decomposed_problems, + dump_couplings, + ) + + block = TimeBlock( + 1, + list( + range( + self.optim_config.time_scope.first_time_step, + self.optim_config.time_scope.last_time_step + 1, + ) + ), + ) + decomposed = build_decomposed_problems( + self.study, block, self.scenario_ids, self.optim_config + ) + + if decomposed.master is not None and self.output_dir is not None: + dump_couplings( + build_couplings(decomposed, self.optim_config), self.output_dir + ) + BendersRunner(emplacement=self.output_dir).run() + else: + raise RuntimeError( + "Benders decomposition requires a master problem and an output directory for coupling files." + ) + return SimulationTable(pd.DataFrame()) + + # ------------------------------------------------------------------ + # Map / reduce helpers + # ------------------------------------------------------------------ + + def _run_block( + self, + block: TimeBlock, + scenario_ids: List[int], + initial_values: Optional[Dict[Tuple[str, str], xr.DataArray]] = None, + ) -> Tuple[OptimizationProblem, SimulationTable]: + """MAP: build and solve one block, then convert to a SimulationTable. + + Returns both the solved problem (for carry-over extraction or inspection) + and the SimulationTable with correct absolute-time and scenario indices. + scenario_ids_remap equals scenario_ids because the list of MC scenario IDs + IS the mapping from internal 0-based position to actual MC identifier. + """ + problem = build_problem( + self.study, + block, + scenario_ids, + optim_config=self.optim_config, + initial_values=initial_values, + ) + problem.solve( + solver_name=self.optim_config.solver_options.name, + solver_logs=self.optim_config.solver_options.logs, + **self.optim_config.solver_options.parsed_parameters(), + ) + table = SimulationTableBuilder().build( + problem, scenario_ids_remap=scenario_ids, table_id=self.run_id + ) + return problem, table + + def _reduce(self, tables: List[SimulationTable]) -> SimulationTable: + """REDUCE: merge SimulationTables from one scenario's blocks into one.""" + return merge_simulation_tables(tables, table_id=self.run_id) + + @staticmethod + def _extract_carry_over( + problem: OptimizationProblem, + local_index: int, + ) -> Dict[Tuple[str, str], xr.DataArray]: + """Extract variable values at *local_index* for use as initial values in the next block.""" + carry_over: Dict[Tuple[str, str], xr.DataArray] = {} + solution = problem.linopy_model.solution + if solution is None: + return carry_over + for (model, var_name), linopy_var in problem._linopy_vars.items(): + if "time" in linopy_var.dims and linopy_var.name in solution: + sol_da: xr.DataArray = solution[linopy_var.name] + carry_over[(model, var_name)] = sol_da.isel(time=local_index, drop=True) + return carry_over diff --git a/src/gems/simulation/__init__.py b/src/gems/simulation/__init__.py index 46501a29..371f5be1 100644 --- a/src/gems/simulation/__init__.py +++ b/src/gems/simulation/__init__.py @@ -10,13 +10,15 @@ # # This file is part of the Antares project. -from .benders_decomposed import ( - BendersDecomposedProblem, - build_benders_decomposed_problem, +from .couplings import CouplingRow, build_couplings, dump_couplings +from .optimization import ( + DecomposedProblems, + DecompositionFilter, + LinopyModel, + OptimizationProblem, + build_decomposed_problems, + build_problem, ) -from .decision_tree import DecisionTreeNode, InterDecisionTimeScenarioConfig -from .optimization import BlockBorderManagement, OptimizationProblem, build_problem -from .output_values import BendersSolution, OutputValues -from .runner import BendersRunner, MergeMPSRunner -from .strategy import MergedProblemStrategy, ModelSelectionStrategy +from .runner import BendersRunner +from .simulation_table import SimulationColumns, SimulationTableBuilder from .time_block import TimeBlock diff --git a/src/gems/simulation/benders_decomposed.py b/src/gems/simulation/benders_decomposed.py deleted file mode 100644 index cb34fb4d..00000000 --- a/src/gems/simulation/benders_decomposed.py +++ /dev/null @@ -1,299 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -The xpansion module extends the optimization module -with Benders solver related functions -""" - -import pathlib -from typing import Any, Dict, List, Optional - -from gems.simulation.decision_tree import DecisionTreeNode -from gems.simulation.optimization import ( - BlockBorderManagement, - OptimizationProblem, - build_problem, - fusion_problems, -) -from gems.simulation.output_values import ( - BendersDecomposedSolution, - BendersMergedSolution, - BendersSolution, -) -from gems.simulation.runner import BendersRunner, MergeMPSRunner -from gems.simulation.strategy import ( - ExpectedValue, - InvestmentProblemStrategy, - OperationalProblemStrategy, -) -from gems.simulation.time_block import TimeBlock -from gems.study.data import DataBase -from gems.utils import read_json, serialize, serialize_json - - -class BendersDecomposedProblem: - """ - A simpler interface for the Benders Decomposed problem - """ - - master: OptimizationProblem - subproblems: List[OptimizationProblem] - - emplacement: pathlib.Path - output_path: pathlib.Path - structure_filename: str - - solution: Optional[BendersSolution] - is_merged: bool - - def __init__( - self, - master: OptimizationProblem, - subproblems: List[OptimizationProblem], - emplacement: str = "outputs/lp", - output_path: str = "expansion", - struct_filename: str = "structure.txt", - ) -> None: - self.master = master - self.subproblems = subproblems - - self.emplacement = pathlib.Path(emplacement) - self.output_path = pathlib.Path(output_path) - self.structure_filename = struct_filename - - self.solution = None - self.is_merged = False - - def export_structure(self) -> str: - """ - Write the structure.txt file - """ - - if not self.subproblems: - raise RuntimeError("Subproblem list must have at least one sub problem") - - # A mapping similar to the Xpansion mapping for keeping track of variable indexes - # in Master and Sub-problem files - problem_to_candidates: Dict[str, Dict[str, int]] = {} - candidates = set() - - problem_to_candidates["master"] = {} - for solver_var_info in self.master.context._solver_variables.values(): - problem_to_candidates["master"][ - solver_var_info.name - ] = solver_var_info.column_id - candidates.add(solver_var_info.name) - - for problem in self.subproblems: - problem_to_candidates[problem.name] = {} - - for solver_var_info in problem.context._solver_variables.values(): - if solver_var_info.name in candidates: - # If candidate was identified in master - problem_to_candidates[problem.name][ - solver_var_info.name - ] = solver_var_info.column_id - - structure_str = "" - for problem_name, candidate_to_index in problem_to_candidates.items(): - for candidate, index in candidate_to_index.items(): - structure_str += f"{problem_name:>50}{candidate:>50}{index:>10}\n" - - return structure_str - - def export_options( - self, *, solver_name: str = "XPRESS", log_level: int = 0 - ) -> Dict[str, Any]: - # Default values - options_values = { - "MAX_ITERATIONS": -1, - "ABSOLUTE_GAP": 1, - "RELATIVE_GAP": 1e-6, - "RELAXED_GAP": 1e-5, - "AGGREGATION": False, - "OUTPUTROOT": ".", - "TRACE": True, - "SLAVE_WEIGHT": "CONSTANT", - "SLAVE_WEIGHT_VALUE": 1, - "MASTER_NAME": f"{self.master.name}", - "STRUCTURE_FILE": f"{self.structure_filename}", - "INPUTROOT": ".", - "CSV_NAME": "benders_output_trace", - "BOUND_ALPHA": True, - "SEPARATION_PARAM": 0.5, - "BATCH_SIZE": 0, - "JSON_FILE": f"{self.output_path}/out.json", - "LAST_ITERATION_JSON_FILE": f"{self.output_path}/last_iteration.json", - "MASTER_FORMULATION": "integer", - "SOLVER_NAME": solver_name, - "TIME_LIMIT": 1_000_000_000_000, - "LOG_LEVEL": log_level, - "LAST_MASTER_MPS": "master_last_iteration", - "LAST_MASTER_BASIS": "master_last_basis.bss", - } - return options_values - - def initialise( - self, - *, - solver_name: str = "XPRESS", - log_level: int = 0, - is_debug: bool = False, - ) -> None: - serialize( - f"{self.master.name}.mps", self.master.export_as_mps(), self.emplacement - ) - for subproblem in self.subproblems: - serialize( - f"{subproblem.name}.mps", subproblem.export_as_mps(), self.emplacement - ) - serialize( - f"{self.structure_filename}", self.export_structure(), self.emplacement - ) - serialize_json( - "options.json", - self.export_options(solver_name=solver_name, log_level=log_level), - self.emplacement, - ) - - if is_debug: - serialize( - f"{self.master.name}.lp", self.master.export_as_lp(), self.emplacement - ) - for subproblem in self.subproblems: - serialize( - f"{subproblem.name}.lp", subproblem.export_as_lp(), self.emplacement - ) - - def read_solution(self) -> None: - try: - data = read_json("out.json", self.emplacement / self.output_path) - - except FileNotFoundError: - # TODO For now, it will return as if nothing is wrong - # modify it with runner's run - print("Return without reading it for now") - return - - if self.is_merged: - self.solution = BendersMergedSolution(data) - else: - self.solution = BendersDecomposedSolution(data) - - def run( - self, - *, - solver_name: str = "XPRESS", - log_level: int = 0, - should_merge: bool = False, - show_debug: bool = False, - ) -> bool: - self.initialise( - solver_name=solver_name, log_level=log_level, is_debug=show_debug - ) - - if not should_merge: - return_code = BendersRunner(self.emplacement).run() - else: - self.is_merged = True - return_code = MergeMPSRunner(self.emplacement).run() - - if return_code == 0: - self.read_solution() - return True - else: - return False - - -def build_benders_decomposed_problem( - decision_tree_root: DecisionTreeNode, - database: DataBase, - *, - border_management: BlockBorderManagement = BlockBorderManagement.CYCLE, - solver_id: str = "GLOP", - struct_filename: str = "structure.txt", -) -> BendersDecomposedProblem: - """ - Entry point to build the xpansion pathway problem. - - For each node of the tree, it builds a Master and a Subproblem. - Then it defines a coupled problem that merges all masters along with - its pathway constraints into one 'tree master' problem. - - Returns a Benders Decomposed problem - """ - - if not decision_tree_root.is_leaves_prob_sum_one(): - raise ValueError("Decision tree leaves' probability must sum one!") - - null_time_block = TimeBlock( - 0, [0] - ) # Not necessary for master, but list must be non-empty - null_scenario = 0 # Not necessary for master - - decision_tree_root._add_coupling_constraints() - - coupler = build_problem( - decision_tree_root.coupling_network, - database, - null_time_block, - null_scenario, - problem_name="coupler", - solver_id=solver_id, - build_strategy=InvestmentProblemStrategy(), - risk_strategy=ExpectedValue(0.0), - use_full_var_name=False, - ) - - masters = [] # Benders Decomposed Master Problem - subproblems = [] # Benders Decomposed Sub-problems - - for tree_node in decision_tree_root.traverse(): - suffix_tree = f"_{tree_node.id}" if decision_tree_root.size > 1 else "" - - masters.append( - build_problem( - tree_node.network, - database, - null_time_block, - null_scenario, - problem_name=f"master{suffix_tree}", - solver_id=solver_id, - build_strategy=InvestmentProblemStrategy(), - decision_tree_node=tree_node.id, - risk_strategy=ExpectedValue(tree_node.prob), - ) - ) - - for block in tree_node.config.blocks: - suffix_block = f"_b{block.id}" if len(tree_node.config.blocks) > 1 else "" - - subproblems.append( - build_problem( - tree_node.network, - database, - block, - tree_node.config.scenarios, - problem_name=f"subproblem{suffix_tree}{suffix_block}", - solver_id=solver_id, - build_strategy=OperationalProblemStrategy(), - decision_tree_node=tree_node.id, - risk_strategy=ExpectedValue(tree_node.prob), - ) - ) - - master = fusion_problems(masters, coupler) - - return BendersDecomposedProblem( - master, subproblems, struct_filename=struct_filename - ) diff --git a/src/gems/simulation/couplings.py b/src/gems/simulation/couplings.py new file mode 100644 index 00000000..ac586edc --- /dev/null +++ b/src/gems/simulation/couplings.py @@ -0,0 +1,107 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from dataclasses import dataclass +from pathlib import Path +from typing import TYPE_CHECKING, List, Optional + +if TYPE_CHECKING: + from gems.optim_config.parsing import OptimConfig + from gems.simulation.optimization import DecomposedProblems + + +@dataclass(frozen=True) +class CouplingRow: + """A single entry in the Benders structure file.""" + + problem_id: str + component_id: str + variable_int_id: int + + +def _format_row(row: CouplingRow) -> str: + return f"{row.problem_id:>24}{row.component_id:>48}{row.variable_int_id:>9}" + + +def _master_coupling_row( + decomposed: "DecomposedProblems", + model_id: str, + var_id: str, + comp_id: str, +) -> Optional[CouplingRow]: + if decomposed.master is None: + return None + labels = decomposed.master.get_variable_labels(model_id, var_id) + if labels is None: + return None + return CouplingRow( + problem_id=decomposed.master.name, + component_id=comp_id, + variable_int_id=int(labels.sel(component=comp_id).item()), + ) + + +def _subproblem_coupling_row( + decomposed: "DecomposedProblems", + model_id: str, + var_id: str, + comp_id: str, +) -> Optional[CouplingRow]: + labels = decomposed.subproblem.get_variable_labels(model_id, var_id) + if labels is None: + return None + return CouplingRow( + problem_id=decomposed.subproblem.name, + component_id=comp_id, + variable_int_id=int(labels.sel(component=comp_id).item()), + ) + + +def _coupling_rows_for_variable( + decomposed: "DecomposedProblems", + model_id: str, + var_id: str, +) -> List[CouplingRow]: + rows: List[CouplingRow] = [] + for comp in decomposed.subproblem.study.model_components.get(model_id, []): + for row in ( + _master_coupling_row(decomposed, model_id, var_id, comp.id), + _subproblem_coupling_row(decomposed, model_id, var_id, comp.id), + ): + if row is not None: + rows.append(row) + return rows + + +def build_couplings( + decomposed: "DecomposedProblems", + optim_config: "OptimConfig", +) -> List[CouplingRow]: + from gems.optim_config.parsing import ElementLocation + + rows: List[CouplingRow] = [] + for mc in optim_config.models: + if mc.model_decomposition is not None: + for var_cfg in mc.model_decomposition.variables: + if var_cfg.location == ElementLocation.MASTER_AND_SUBPROBLEMS: + rows.extend( + _coupling_rows_for_variable(decomposed, mc.id, var_cfg.id) + ) + return rows + + +def dump_couplings(rows: List[CouplingRow], output_dir: Path) -> None: + """Write ``structure.txt`` to *output_dir*.""" + output_dir.mkdir(parents=True, exist_ok=True) + (output_dir / "structure.txt").write_text( + "\n".join(_format_row(row) for row in rows) + "\n" + ) diff --git a/src/gems/simulation/decision_tree.py b/src/gems/simulation/decision_tree.py deleted file mode 100644 index 7def945d..00000000 --- a/src/gems/simulation/decision_tree.py +++ /dev/null @@ -1,199 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -import math -from dataclasses import dataclass, field -from typing import Dict, Generator, Iterable, List, Optional, Set - -from anytree import LevelOrderIter, NodeMixin - -from gems.expression import ExpressionNode, literal, var -from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext -from gems.model.constraint import Constraint -from gems.model.model import model -from gems.model.variable import Variable, float_variable -from gems.simulation.time_block import TimeBlock -from gems.study.network import Component, Network, create_component - - -@dataclass(frozen=True) -class InterDecisionTimeScenarioConfig: - blocks: List[TimeBlock] - scenarios: int - - -@dataclass -class CouplingInfo: - variables: Set[str] = field(default_factory=set) - constraints: Dict[str, ExpressionNode] = field(default_factory=dict) - - -class DecisionTreeNode(NodeMixin): - id: str - config: InterDecisionTimeScenarioConfig - network: Network - prob: float - - coupling_network: Network - coupling_info: CouplingInfo - - def __init__( - self, - id: str, - config: InterDecisionTimeScenarioConfig, - network: Network, - parent: Optional["DecisionTreeNode"] = None, - children: Optional[Iterable["DecisionTreeNode"]] = None, - prob: float = 1.0, - coupling_network: Network = Network("_Coupler"), - coupling_info: CouplingInfo = CouplingInfo(), - ) -> None: - self.id = id - self.config = config - self.network = network - self.coupling_network = coupling_network - self.coupling_info = coupling_info - self.parent = parent - - if prob < 0 or 1 < prob: - raise ValueError("Probability must be a value in the range [0, 1]") - - self.prob = prob * (parent.prob if parent is not None else 1) - if children: - self.children = children - - def traverse( - self, depth: Optional[int] = None - ) -> Generator["DecisionTreeNode", None, None]: - yield from LevelOrderIter(self, maxlevel=depth) - - def is_leaves_prob_sum_one(self) -> bool: - if not self.children: - return True - - # Since we multiply the child's prob by the parent's prob - # in the constructor, the sum of the children prob should - # equal 1 * parent.prob if the values were set correctly - if not math.isclose(self.prob, sum(child.prob for child in self.children)): - return False - - # Recursively check if child nodes have their children's - # probability sum equal to one - return all(child.is_leaves_prob_sum_one() for child in self.children) - - def define_coupling_constraint( - self, - component_par: Component, - cumulative_par_var_id: str, - component_chd: Component, - cumulative_chd_var_id: str, - incremental_chd_var_id: str, - ) -> None: - """ - Define a coupling constraint of the form: - - var(cumulative_par_var_id) == var(cumulative_chd_var_id) - var(incremental_chd_var_id) - - where 'cumulative_par_var_id' is a variable of component_par;\n - and 'cumulative_chd_var_id' and 'incremental_chd_var_id' are variables of component_chd - """ - if self.parent and (component_par not in self.parent.network.all_components): - raise RuntimeError( - f"Component {component_par.id} not present in parent's network!" - ) - - if component_chd not in self.network.all_components: - raise RuntimeError(f"Component {component_chd.id} not present in network!") - - if not component_par.is_variable_in_model(cumulative_par_var_id): - raise ValueError( - f"Cumulative variable {cumulative_par_var_id} not present in parent's {component_par.id}" - ) - - elif not component_chd.is_variable_in_model(cumulative_chd_var_id): - raise ValueError( - f"Cumulative variable {cumulative_chd_var_id} not present in {component_chd.id}" - ) - - elif not component_chd.is_variable_in_model(incremental_chd_var_id): - raise ValueError( - f"Incremental variable {incremental_chd_var_id} not present in {component_chd.id}" - ) - - prefix_par = "" - cumulative_var_par_id = "" - if self.parent: - prefix_par = f"{self.parent.id}.{component_par.id}" - cumulative_var_par_id = f"{prefix_par}.{cumulative_par_var_id}" - - self.coupling_info.variables.add(cumulative_var_par_id) - - prefix_chd = f"{self.id}.{component_chd.id}" - cumulative_var_chd_id = f"{prefix_chd}.{cumulative_chd_var_id}" - incremental_var_par_id = f"{prefix_chd}.{incremental_chd_var_id}" - - self.coupling_info.variables.add(cumulative_var_chd_id) - self.coupling_info.variables.add(incremental_var_par_id) - - self.coupling_info.constraints[ - f"{prefix_par}.{prefix_chd}.{len(self.coupling_info.constraints)}" - ] = (var(cumulative_var_chd_id) - var(incremental_var_par_id)) == ( - var(cumulative_var_par_id) if cumulative_var_par_id else literal(0) - ) - - def _add_coupling_constraints(self) -> None: - variables: List[Variable] = [] - constraints: List[Constraint] = [] - - """ - Here and below, we use (and mostly abuse!) of the fact that - both coupling_network and coupling_info attributes are initialized, - or bounded, at the function definition since they are default arguments! - It allows us to not iterate over the tree, since all nodes share the same - coupling_network and coupling_info objects. - - However, IT IS BAD DESIGN! - But for now it will suffice since the inner workings will change once - Thomas has changed the AST instantiation - TODO Update this once the new AST is merged - """ - for var_name in self.coupling_info.variables: - variables.append( - # TODO For now, unbounded positive float variable - float_variable( - var_name, - lower_bound=literal(0), - structure=IndexingStructure(False, False), - context=ProblemContext.INVESTMENT, - ), - ) - - for cst_name, expr in self.coupling_info.constraints.items(): - constraints.append( - Constraint( - name=cst_name, - expression=expr, - context=ProblemContext.INVESTMENT, - ), - ) - - self.coupling_network.add_component( - create_component( - model( - id="", - variables=variables, - constraints=constraints, - ), - id="coupling", - ) - ) diff --git a/src/gems/simulation/extra_output.py b/src/gems/simulation/extra_output.py index eede420f..f3ef08fb 100644 --- a/src/gems/simulation/extra_output.py +++ b/src/gems/simulation/extra_output.py @@ -1,93 +1,142 @@ -from dataclasses import dataclass -from typing import Any, Dict, Optional - -from gems.expression.evaluate import ValueProvider -from gems.simulation.optimization import OptimizationProblem -from gems.simulation.output_values_base import BaseOutputValue -from gems.study.data import ComponentParameterIndex, TimeScenarioIndex +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Extra output storage and vectorized evaluation. + +Provides :class:`ExtraOutput` for storing post-solve expression values and +:class:`VectorizedExtraOutputBuilder`, a concrete subclass of +:class:`~gems.simulation.vectorized_builder.VectorizedBuilderBase` that +resolves ``VariableNode`` to an ``xr.DataArray`` of solved values, enabling +nonlinear operations (products of variables, floor, ceil, min, max) that +are not permitted during pre-solve constraint building. + +Port arrays for ``sum_connections`` support are built by calling +:func:`~gems.simulation.optimization.build_port_arrays` with a +:class:`VectorizedExtraOutputBuilder` factory. +""" + +from dataclasses import dataclass, field +from typing import Dict, Optional, Tuple + +import numpy as np +import xarray as xr + +from gems.expression.expression import DualNode, ReducedCostNode, VariableNode +from gems.model.port import PortFieldId +from gems.simulation.vectorized_builder import VectorizedBuilderBase +from gems.study.system import Component @dataclass -class ExtraOutput(BaseOutputValue): +class ExtraOutput: """ - Stores evaluated outputs (from ExpressionNodes), not solver variables. - Inherits all common fields (_name, _value, _size, ignore) and methods - (__eq__, is_close, __str__, value property) from BaseOutputValue. + Stores a post-solve extra output expression as a vectorized xr.DataArray + with dims ⊆ {component, time, scenario}. """ - def _set( - self, - timestep: Optional[int], - scenario: Optional[int], - value: float, - ) -> None: - timestep = 0 if timestep is None else timestep - scenario = 0 if scenario is None else scenario - key = TimeScenarioIndex(timestep, scenario) - - if key not in self._value: - size_s = max(self._size[0], scenario + 1) - size_t = max(self._size[1], timestep + 1) - self._size = (size_s, size_t) - - self._value[key] = value + _name: str + _data: Optional[xr.DataArray] = field(init=False, default=None) + ignore: bool = field(default=False, init=False) + def __eq__(self, other: object) -> bool: + if not isinstance(other, ExtraOutput): + return NotImplemented + return self.is_close(other, rel_tol=0.0, abs_tol=0.0) -class ExtraOutputValueProvider(ValueProvider): - # ... (content remains the same, as it only interacts with public methods/inherited fields like _value) ... - """Provides variable and parameter values for extra output expressions.""" - - def __init__( + def is_close( self, - component: Any, - problem: OptimizationProblem, - idx: TimeScenarioIndex, - ) -> None: - self.component = component - self.problem = problem - self.idx = idx - self.context = self._build_context() - - def _build_context(self) -> Dict[str, float]: - ctx: Dict[str, float] = {} - - # --- Variables --- - if hasattr(self.component, "_variables"): - for vname, vobj in self.component._variables.items(): - if hasattr(vobj, "_value"): - val = vobj._value.get(self.idx) - if val is not None: - ctx[vname] = val - if hasattr(self.component, "_id"): - ctx[f"{self.component._id}.{vname}"] = val - - # --- Parameters --- - model = getattr(self.component, "model", None) - if model is not None and hasattr(model, "parameters"): - for pname in model.parameters: - try: - val = self.problem.context.database.get_value( - ComponentParameterIndex(self.component._id, pname), - self.idx.time, - self.idx.scenario, - ) - ctx[pname] = val - if hasattr(self.component, "_id"): - ctx[f"{self.component._id}.{pname}"] = val - except KeyError: - continue - - return ctx - - # ValueProvider interface - def get_variable_value(self, name: str) -> float: - return self.context[name] - - def get_parameter_value(self, name: str) -> float: - return self.context[name] - - def get_component_variable_value(self, component_id: str, name: str) -> float: - return self.context[name] - - def get_component_parameter_value(self, component_id: str, name: str) -> float: - return self.context[name] + other: "ExtraOutput", + *, + rel_tol: float = 1.0e-9, + abs_tol: float = 0.0, + ) -> bool: + if not isinstance(other, ExtraOutput): + return NotImplemented # type: ignore[return-value] + if self.ignore or other.ignore: + return True + if (self._data is None) != (other._data is None): + return False + if self._data is None: + return True + assert other._data is not None # narrowing: both non-None by checks above + try: + lhs, rhs = xr.align(self._data, other._data, join="exact") + except ValueError: + return False + return bool(np.allclose(lhs.values, rhs.values, rtol=rel_tol, atol=abs_tol)) + + def __str__(self) -> str: + return f"{self._name} : {self._data!r} {'(ignored)' if self.ignore else ''}" + + +@dataclass(kw_only=True) +class VectorizedExtraOutputBuilder(VectorizedBuilderBase[xr.DataArray]): + """ + Evaluates a model-level extra output expression as a vectorized xr.DataArray. + + A concrete subclass of :class:`VectorizedBuilderBase` that resolves + decision variables to their post-solve optimal values (``xr.DataArray``), + enabling nonlinear operations such as products of variables, floor, ceil, + min, and max that are forbidden during pre-solve constraint building. + + Parameters + ---------- + model_id: + The model.id string of the Model object whose AST is being visited. + param_arrays: + Mapping from (model_id, param_name) to a DataArray of parameter values, + with dims in {component, time, scenario} (or a subset). + var_solution_arrays: + Mapping from (model_id, var_name) to a DataArray of solution values, + with dims in {component, time, scenario} (or a subset). + port_arrays: + Pre-computed xr.DataArray for each PortFieldId of this model. + Keyed by PortFieldId(port_name, field_name). + block_length: + Number of time steps in the current time block. + """ + + var_solution_arrays: Dict[Tuple[str, str], xr.DataArray] + constraint_dual_arrays: Dict[Tuple[str, str], xr.DataArray] = field( + default_factory=dict + ) + var_reduced_cost_arrays: Dict[Tuple[str, str], xr.DataArray] = field( + default_factory=dict + ) + + def variable(self, node: VariableNode) -> xr.DataArray: + key = (self.model_id, node.name) + if key not in self.var_solution_arrays: + raise KeyError( + f"Variable {node.name!r} not found in solution for model " + f"{self.model_id!r}." + ) + return self.var_solution_arrays[key] + + def dual(self, node: DualNode) -> xr.DataArray: + key = (self.model_id, node.constraint_id) + if key not in self.constraint_dual_arrays: + raise KeyError( + f"Dual of constraint '{node.constraint_id}' not found for model " + f"{self.model_id!r}." + ) + return self.constraint_dual_arrays[key] + + def reduced_cost(self, node: ReducedCostNode) -> xr.DataArray: + key = (self.model_id, node.variable_id) + if key not in self.var_reduced_cost_arrays: + raise KeyError( + f"Reduced cost of variable '{node.variable_id}' not found for model " + f"{self.model_id!r}." + ) + return self.var_reduced_cost_arrays[key] diff --git a/src/gems/simulation/linear_expression.py b/src/gems/simulation/linear_expression.py deleted file mode 100644 index a0584c07..00000000 --- a/src/gems/simulation/linear_expression.py +++ /dev/null @@ -1,438 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -Specific modelling for "instantiated" linear expressions, -with only variables and literal coefficients. -""" - -import dataclasses -from dataclasses import dataclass -from typing import Callable, Dict, List, Optional, TypeVar, Union - -from gems.expression.expression import ( - OneScenarioIndex, - ScenarioIndex, - TimeIndex, - TimeShift, - TimeStep, -) - -T = TypeVar("T") - -EPS = 10 ** (-16) - - -def is_close_abs(value: float, other_value: float, eps: float) -> bool: - return abs(value - other_value) < eps - - -def is_zero(value: float) -> bool: - return is_close_abs(value, 0, EPS) - - -def is_one(value: float) -> bool: - return is_close_abs(value, 1, EPS) - - -def is_minus_one(value: float) -> bool: - return is_close_abs(value, -1, EPS) - - -@dataclass(frozen=True) -class TimeExpansion: - """ - Carries knowledge of which timesteps this term refers to. - Simplest one is "only the current timestep" - """ - - def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: - return [current_timestep] - - def apply(self, other: "TimeExpansion") -> "TimeExpansion": - """ - Apply another time expansion on this one. - For example, a shift of -1 applied to a shift one +1 could provide - a no-op TimeExpansion. Not yet supported for now, though. - """ - return other - - -@dataclass(frozen=True) -class AllTimeExpansion(TimeExpansion): - def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: - return [t for t in range(block_length)] - - def apply(self, other: "TimeExpansion") -> "TimeExpansion": - raise ValueError("No time operation allowed on all-time sum.") - - -@dataclass(frozen=True) -class TimeEvalExpansion(TimeExpansion): - timestep: int - - def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: - return [self.timestep] - - def apply(self, other: "TimeExpansion") -> "TimeExpansion": - raise ValueError( - "Time operation on evaluated expression not supported for now." - ) - - -@dataclass(frozen=True) -class TimeShiftExpansion(TimeExpansion): - shift: int - - def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: - return [current_timestep + self.shift] - - def apply(self, other: "TimeExpansion") -> "TimeExpansion": - raise ValueError("Time operation on shifted expression not supported for now.") - - -@dataclass(frozen=True) -class TimeSumExpansion(TimeExpansion): - from_shift: int - to_shift: int - - def get_timesteps(self, current_timestep: int, block_length: int) -> List[int]: - return [ - t - for t in range( - current_timestep + self.from_shift, current_timestep + self.to_shift - ) - ] - - def apply(self, other: "TimeExpansion") -> "TimeExpansion": - raise ValueError("Time operation on time-sums not supported for now.") - - -@dataclass(frozen=True) -class TermKey: - """ - Utility class to provide key for a term that contains all term information except coefficient - """ - - component_id: str - variable_name: str - time_index: Optional[int] - scenario_index: Optional[int] - - -def _str_for_coeff(coeff: float) -> str: - if is_one(coeff): - return "+" - elif is_minus_one(coeff): - return "-" - else: - return "{:+g}".format(coeff) - - -def _time_index_to_str(time_index: TimeIndex) -> str: - if isinstance(time_index, TimeShift): - if time_index.timeshift == 0: - return "t" - elif time_index.timeshift > 0: - return f"t + {time_index.timeshift}" - else: - return f"t - {-time_index.timeshift}" - if isinstance(time_index, TimeStep): - return f"{time_index.timestep}" - return "" - - -def _scenario_index_to_str(scenario_index: ScenarioIndex) -> str: - if isinstance(scenario_index, OneScenarioIndex): - return f"{scenario_index.scenario}" - return "" - - -def _str_for_time_expansion(exp: TimeExpansion) -> str: - if isinstance(exp, TimeShiftExpansion): - return f".shift({exp.shift})" - elif isinstance(exp, TimeSumExpansion): - return f".sum({exp.from_shift}, {exp.to_shift})" - elif isinstance(exp, AllTimeExpansion): - return ".sum()" - else: - return "" - - -@dataclass(frozen=True) -class Term: - """ - One term in a linear expression: for example the "10x" par in "10x + 5y + 5" - - Args: - coefficient: the coefficient for that term, for example "10" in "10x" - variable_name: the name of the variable, for example "x" in "10x" - """ - - coefficient: float - component_id: str - variable_name: str - time_index: Optional[int] - scenario_index: Optional[int] - - # TODO: It may be useful to define __add__, __sub__, etc on terms, which should return a linear expression ? - - def is_zero(self) -> bool: - return is_zero(self.coefficient) - - def __str__(self) -> str: - # Useful for debugging tests - return repr(self) - - def __repr__(self) -> str: - # Useful for debugging tests - result = ( - f"{_str_for_coeff(self.coefficient)}{self.component_id}.{self.variable_name}" - f"[{self.time_index},{self.scenario_index}]" - ) - return result - - -def generate_key(term: Term) -> TermKey: - return TermKey( - term.component_id, - term.variable_name, - term.time_index, - term.scenario_index, - ) - - -def _merge_dicts( - lhs: Dict[TermKey, Term], - rhs: Dict[TermKey, Term], - merge_func: Callable[[Optional[Term], Optional[Term]], Term], -) -> Dict[TermKey, Term]: - res = {} - for k, left in lhs.items(): - right = rhs.get(k, None) - res[k] = merge_func(left, right) - for k, right in rhs.items(): - if k not in lhs: - res[k] = merge_func(None, right) - return res - - -def _add_terms(lhs: Optional[Term], rhs: Optional[Term]) -> Term: - if lhs is not None and rhs is not None: - return dataclasses.replace(rhs, coefficient=lhs.coefficient + rhs.coefficient) - elif lhs is not None and rhs is None: - return lhs - elif lhs is None and rhs is not None: - return rhs - raise ValueError("Cannot add 2 null terms.") - - -def _substract_terms(lhs: Optional[Term], rhs: Optional[Term]) -> Term: - if lhs is not None and rhs is not None: - return dataclasses.replace(lhs, coefficient=lhs.coefficient - rhs.coefficient) - elif lhs is not None and rhs is None: - return lhs - elif lhs is None and rhs is not None: - return dataclasses.replace(rhs, coefficient=-rhs.coefficient) - raise ValueError("Cannot subtract 2 null terms.") - - -class LinearExpression: - """ - Represents a linear expression with respect to variable names, for example 10x + 5y + 2. - - Operators may be used for construction. - - Args: - terms: the list of variable terms, for example 10x and 5y in "10x + 5y + 2". - constant: the constant term, for example 2 in "10x + 5y + 2" - - Examples: - Operators may be used for construction: - - >>> LinearExpression([], 10) + LinearExpression([Term(10, "x")], 0) - LinearExpression([Term(10, "x")], 10) - """ - - terms: Dict[TermKey, Term] - constant: float - - def __init__( - self, - terms: Optional[Union[Dict[TermKey, Term], List[Term]]] = None, - constant: Optional[float] = None, - ) -> None: - self.constant = 0 - self.terms = {} - - if constant is not None: - # += b - self.constant = constant - if terms is not None: - # Allows to give two different syntax in the constructor: - # - List[Term] is natural - # - Dict[str, Term] is useful when constructing a linear expression from the terms of another expression - if isinstance(terms, dict): - for term_key, term in terms.items(): - if not term.is_zero(): - self.terms[term_key] = term - elif isinstance(terms, list): - for term in terms: - if not term.is_zero(): - self.terms[generate_key(term)] = term - else: - raise TypeError( - f"Terms must be either of type Dict[str, Term] or List[Term], whereas {terms} is of type {type(terms)}" - ) - - def is_zero(self) -> bool: - return len(self.terms) == 0 and is_zero(self.constant) - - def str_for_constant(self) -> str: - if is_zero(self.constant): - return "" - else: - return "{:+g}".format(self.constant) - - def __repr__(self) -> str: - # Useful for debugging tests - result = "" - if self.is_zero(): - result += "0" - else: - for term in self.terms.values(): - result += repr(term) - - result += self.str_for_constant() - - return result - - def __eq__(self, rhs: object) -> bool: - return ( - isinstance(rhs, LinearExpression) - and is_close_abs(self.constant, rhs.constant, EPS) - and self.terms - == rhs.terms # /!\ There may be float equality comparison in the terms values - ) - - def __iadd__(self, rhs: "LinearExpression") -> "LinearExpression": - if not isinstance(rhs, LinearExpression): - return NotImplemented - self.constant += rhs.constant - aggregated_terms = _merge_dicts(self.terms, rhs.terms, _add_terms) - self.terms = aggregated_terms - self.remove_zeros_from_terms() - return self - - def __add__(self, rhs: "LinearExpression") -> "LinearExpression": - result = LinearExpression() - result += self - result += rhs - return result - - def __isub__(self, rhs: "LinearExpression") -> "LinearExpression": - if not isinstance(rhs, LinearExpression): - return NotImplemented - self.constant -= rhs.constant - aggregated_terms = _merge_dicts(self.terms, rhs.terms, _substract_terms) - self.terms = aggregated_terms - self.remove_zeros_from_terms() - return self - - def __sub__(self, rhs: "LinearExpression") -> "LinearExpression": - result = LinearExpression() - result += self - result -= rhs - return result - - def __neg__(self) -> "LinearExpression": - result = LinearExpression() - result -= self - return result - - def __imul__(self, rhs: "LinearExpression") -> "LinearExpression": - if not isinstance(rhs, LinearExpression): - return NotImplemented - - if self.terms and rhs.terms: - raise ValueError("Cannot multiply two non constant expression") - else: - if self.terms: - left_expr = self - const_expr = rhs - else: - # It is possible that both expr are constant - left_expr = rhs - const_expr = self - if is_close_abs(const_expr.constant, 0, EPS): - return LinearExpression() - elif is_close_abs(const_expr.constant, 1, EPS): - _copy_expression(left_expr, self) - else: - left_expr.constant *= const_expr.constant - for term_key, term in left_expr.terms.items(): - left_expr.terms[term_key] = Term( - term.coefficient * const_expr.constant, - term.component_id, - term.variable_name, - term.time_index, - term.scenario_index, - ) - _copy_expression(left_expr, self) - return self - - def __mul__(self, rhs: "LinearExpression") -> "LinearExpression": - result = LinearExpression() - result += self - result *= rhs - return result - - def __itruediv__(self, rhs: "LinearExpression") -> "LinearExpression": - if not isinstance(rhs, LinearExpression): - return NotImplemented - - if rhs.terms: - raise ValueError("Cannot divide by a non constant expression") - else: - if is_close_abs(rhs.constant, 0, EPS): - raise ZeroDivisionError("Cannot divide expression by zero") - elif is_close_abs(rhs.constant, 1, EPS): - return self - else: - self.constant /= rhs.constant - for term_key, term in self.terms.items(): - self.terms[term_key] = Term( - term.coefficient / rhs.constant, - term.component_id, - term.variable_name, - term.time_index, - term.scenario_index, - ) - return self - - def __truediv__(self, rhs: "LinearExpression") -> "LinearExpression": - result = LinearExpression() - result += self - result /= rhs - - return result - - def remove_zeros_from_terms(self) -> None: - # TODO: Not optimized, checks could be done directly when doing operations on self.linear_term to avoid copies - for term_key, term in self.terms.copy().items(): - if is_close_abs(term.coefficient, 0, EPS): - del self.terms[term_key] - - -def _copy_expression(src: LinearExpression, dst: LinearExpression) -> None: - dst.terms = src.terms - dst.constant = src.constant diff --git a/src/gems/simulation/linearize.py b/src/gems/simulation/linearize.py index a360effb..6ade5305 100644 --- a/src/gems/simulation/linearize.py +++ b/src/gems/simulation/linearize.py @@ -10,324 +10,144 @@ # # This file is part of the Antares project. -import math -from abc import ABC, abstractmethod +""" +Vectorized linopy expression builder. + +Provides :class:`VectorizedLinearExprBuilder`, a concrete subclass of +:class:`~gems.simulation.vectorized_builder.VectorizedBuilderBase` that +resolves ``VariableNode`` to a pre-solve ``linopy.Variable`` and overrides +arithmetic / nonlinear methods with linopy-specific behaviour. + +Also re-exports :data:`~gems.simulation.vectorized_builder.VectorizedExpr` +and :func:`~gems.simulation.vectorized_builder._linopy_add` for backward +compatibility with callers that import them from this module. +""" + from dataclasses import dataclass -from typing import Any, Dict, List, Optional, Union +from typing import Dict, Tuple + +import linopy +import numpy as np +import xarray as xr -from gems.expression import ( - AdditionNode, - DivisionNode, - ExpressionVisitor, - MultiplicationNode, - NegationNode, -) from gems.expression.expression import ( - AllTimeSumNode, + AdditionNode, CeilNode, - ComparisonNode, - ComponentParameterNode, - ComponentVariableNode, - CurrentScenarioIndex, - ExpressionNode, FloorNode, - LiteralNode, MaxNode, MinNode, - NoScenarioIndex, - NoTimeIndex, - OneScenarioIndex, - ParameterNode, - PortFieldAggregatorNode, - PortFieldNode, - ProblemParameterNode, - ProblemVariableNode, - ScenarioIndex, - ScenarioOperatorNode, - TimeEvalNode, - TimeIndex, - TimeShift, - TimeShiftNode, - TimeStep, - TimeSumNode, VariableNode, ) from gems.expression.visitor import visit -from gems.simulation.linear_expression import LinearExpression, Term, TermKey - - -class ParameterGetter(ABC): - @abstractmethod - def get_parameter_value( - self, - component_id: str, - parameter_name: str, - timestep: Optional[int], - scenario: Optional[int], - ) -> float: - pass - - -@dataclass -class MutableTerm: - coefficient: float - component_id: str - variable_name: str - time_index: Optional[int] - scenario_index: Optional[int] - - def to_key(self) -> TermKey: - return TermKey( - self.component_id, - self.variable_name, - self.time_index, - self.scenario_index, - ) - - def to_term(self) -> Term: - return Term( - self.coefficient, - self.component_id, - self.variable_name, - self.time_index, - self.scenario_index, - ) - - -@dataclass -class LinearExpressionData: - terms: List[MutableTerm] - constant: float - - def build(self) -> LinearExpression: - res_terms: Dict[TermKey, Any] = {} - for t in self.terms: - k = t.to_key() - if k in res_terms: - current_t = res_terms[k] - current_t.coefficient += t.coefficient - else: - res_terms[k] = t - for k, v in res_terms.items(): - res_terms[k] = v.to_term() - return LinearExpression(res_terms, self.constant) +from gems.model.port import PortFieldId +from gems.simulation.vectorized_builder import ( + VectorizedBuilderBase, + VectorizedExpr, + _linopy_add, +) -@dataclass(frozen=True) -class LinearExpressionBuilder(ExpressionVisitor[LinearExpressionData]): +@dataclass(kw_only=True) +class VectorizedLinearExprBuilder(VectorizedBuilderBase[VectorizedExpr]): """ - Reduces a generic expression to a linear expression. - - The input expression must respect the constraints of the output of - the operators expansion expression: - it must only contain `ProblemVariableNode` for variables - and `ProblemParameterNode` parameters. It cannot contain anymore - time aggregators or scenario aggregators, nor port-related nodes. + Builds a linopy LinearExpression from a model-level AST. + + Receives pre-computed linopy variables, parameter DataArrays, and port arrays + indexed on the [component, time, scenario] dimensions (or a subset thereof). + Produces a result with the same combined dimensions. + + Parameters + ---------- + model_id: + The model.id string of the Model object whose AST is being visited. + linopy_vars: + Mapping from (model_id, var_name) to the corresponding linopy Variable, + with dims at least [component] and optionally [time, scenario]. + param_arrays: + Mapping from (model_id, param_name) to a DataArray of parameter values, + with dims in {component, time, scenario} (or a subset). + port_arrays: + Pre-computed linopy expressions for each PortFieldId of this model, + resulting from the incidence-matrix port resolution pass. + Keyed by PortFieldId(port_name, field_name). + block_length: + Number of time steps in the current time block. """ - # TODO: linear expressions should be re-usable for different timesteps and scenarios - timestep: Optional[int] - scenario: Optional[int] - value_provider: Optional[ParameterGetter] = None - - def negation(self, node: NegationNode) -> LinearExpressionData: - operand = visit(node.operand, self) - operand.constant = -operand.constant - for t in operand.terms: - t.coefficient = -t.coefficient - return operand - - def addition(self, node: AdditionNode) -> LinearExpressionData: - operands = [visit(o, self) for o in node.operands] - terms = [] - constant: float = 0 - for o in operands: - constant += o.constant - terms.extend(o.terms) - return LinearExpressionData(terms=terms, constant=constant) + linopy_vars: Dict[Tuple[str, str], linopy.Variable] - def multiplication(self, node: MultiplicationNode) -> LinearExpressionData: - lhs = visit(node.left, self) - rhs = visit(node.right, self) - if not lhs.terms: - multiplier = lhs.constant - actual_expr = rhs - elif not rhs.terms: - multiplier = rhs.constant - actual_expr = lhs - else: - raise ValueError( - "At least one operand of a multiplication must be a constant expression." - ) - actual_expr.constant *= multiplier - for t in actual_expr.terms: - t.coefficient *= multiplier - return actual_expr + # ------------------------------------------------------------------ # + # Abstract method implementation # + # ------------------------------------------------------------------ # - def division(self, node: DivisionNode) -> LinearExpressionData: - lhs = visit(node.left, self) - rhs = visit(node.right, self) - if rhs.terms: - raise ValueError( - "The second operand of a division must be a constant expression." + def variable(self, node: VariableNode) -> linopy.Variable: + key = (self.model_id, node.name) + if key not in self.linopy_vars: + raise KeyError( + f"Variable {node.name!r} not found for model {self.model_id!r}. " + "Ensure all linopy variables are created before building constraints." ) - divider = rhs.constant - actual_expr = lhs - actual_expr.constant /= divider - for t in actual_expr.terms: - t.coefficient /= divider - return actual_expr + return self.linopy_vars[key] - def _get_timestep(self, time_index: TimeIndex) -> Optional[int]: - if isinstance(time_index, TimeShift): - if self.timestep is None: - raise ValueError("Cannot shift a time-independent expression.") - return self.timestep + time_index.timeshift - if isinstance(time_index, TimeStep): - return time_index.timestep - if isinstance(time_index, NoTimeIndex): - return None - else: - raise TypeError(f"Type {type(time_index)} is not a valid TimeIndex type.") + # ------------------------------------------------------------------ # + # Overrides: arithmetic # + # ------------------------------------------------------------------ # - def _get_scenario(self, scenario_index: ScenarioIndex) -> Optional[int]: - if isinstance(scenario_index, OneScenarioIndex): - return scenario_index.scenario - elif isinstance(scenario_index, CurrentScenarioIndex): - return self.scenario - elif isinstance(scenario_index, NoScenarioIndex): - return None - else: - raise TypeError( - f"Type {type(scenario_index)} is not a valid ScenarioIndex type." - ) + def addition(self, node: AdditionNode) -> VectorizedExpr: + """Left-to-right addition with linopy-aware operand swapping. - def literal(self, node: LiteralNode) -> LinearExpressionData: - return LinearExpressionData([], node.value) - - def floor(self, node: FloorNode) -> LinearExpressionData: - operand = visit(node.operand, self) - if operand.terms: - raise ValueError( - "Linear expression cannot contain a floor operator on a non-constant expression." - ) - return LinearExpressionData([], math.floor(operand.constant)) - - def ceil(self, node: CeilNode) -> LinearExpressionData: - operand = visit(node.operand, self) - if operand.terms: - raise ValueError( - "Linear expression cannot contain a ceil operator on a non-constant expression." - ) - return LinearExpressionData([], math.ceil(operand.constant)) - - def maximum(self, node: MaxNode) -> LinearExpressionData: - operands = [visit(op, self) for op in node.operands] - if any(op.terms for op in operands): - raise ValueError( - "Linear expression cannot contain a max operator on a non-constant expression." - ) - return LinearExpressionData([], max(op.constant for op in operands)) - - def minimum(self, node: MinNode) -> LinearExpressionData: + ``xr.DataArray.__add__(linopy_type)`` fails because xarray does not + recognise linopy objects. :func:`_linopy_add` puts the linopy type on + the left so linopy's ``__add__`` / ``__radd__`` handles DataArrays. + """ operands = [visit(op, self) for op in node.operands] - if any(op.terms for op in operands): - raise ValueError( - "Linear expression cannot contain a min operator on a non-constant expression." - ) - return LinearExpressionData([], min(op.constant for op in operands)) - - def comparison(self, node: ComparisonNode) -> LinearExpressionData: - raise ValueError("Linear expression cannot contain a comparison operator.") - - def variable(self, node: VariableNode) -> LinearExpressionData: - raise ValueError( - "Variables need to be associated with their component ID before linearization." - ) + result: VectorizedExpr = operands[0] + for op in operands[1:]: + result = _linopy_add(result, op) + return result - def parameter(self, node: ParameterNode) -> LinearExpressionData: - raise ValueError("Parameters must be evaluated before linearization.") + # ------------------------------------------------------------------ # + # Overrides: nonlinear math functions (guard — variables not allowed) # + # ------------------------------------------------------------------ # - def comp_variable(self, node: ComponentVariableNode) -> LinearExpressionData: - raise ValueError( - "Variables need to be associated with their timestep/scenario before linearization." - ) - - def pb_variable(self, node: ProblemVariableNode) -> LinearExpressionData: - return LinearExpressionData( - [ - MutableTerm( - 1, - node.component_id, - node.name, - time_index=self._get_timestep(node.time_index), - scenario_index=self._get_scenario(node.scenario_index), - ) - ], - 0, + def floor(self, node: FloorNode) -> VectorizedExpr: + operand = visit(node.operand, self) + if isinstance(operand, xr.DataArray): + return np.floor(operand) # type: ignore[return-value] + raise NotImplementedError( + "floor() is only supported for parameter (DataArray) expressions; " + "it cannot be used with decision variables in a linear programme." ) - def comp_parameter(self, node: ComponentParameterNode) -> LinearExpressionData: - raise ValueError( - "Parameters need to be associated with their timestep/scenario before linearization." + def ceil(self, node: CeilNode) -> VectorizedExpr: + operand = visit(node.operand, self) + if isinstance(operand, xr.DataArray): + return np.ceil(operand) # type: ignore[return-value] + raise NotImplementedError( + "ceil() is only supported for parameter (DataArray) expressions; " + "it cannot be used with decision variables in a linear programme." ) - def pb_parameter(self, node: ProblemParameterNode) -> LinearExpressionData: - # TODO SL: not the best place to do this. - # in the future, we should evaluate coefficients of variables as time vectors once for all timesteps - time_index = self._get_timestep(node.time_index) - scenario_index = self._get_scenario(node.scenario_index) - return LinearExpressionData( - [], - self._value_provider().get_parameter_value( - node.component_id, node.name, time_index, scenario_index - ), + def maximum(self, node: MaxNode) -> VectorizedExpr: + operands = [visit(op, self) for op in node.operands] + if all(isinstance(op, xr.DataArray) for op in operands): + result: xr.DataArray = operands[0] # type: ignore[assignment] + for op in operands[1:]: + result = xr.where(result >= op, result, op) # type: ignore[no-untyped-call] + return result + raise NotImplementedError( + "maximum() is only supported for parameter (DataArray) expressions; " + "it cannot be used with decision variables in a linear programme." ) - def time_eval(self, node: TimeEvalNode) -> LinearExpressionData: - raise ValueError("Time operators need to be expanded before linearization.") - - def time_shift(self, node: TimeShiftNode) -> LinearExpressionData: - raise ValueError("Time operators need to be expanded before linearization.") - - def time_sum(self, node: TimeSumNode) -> LinearExpressionData: - raise ValueError("Time operators need to be expanded before linearization.") - - def all_time_sum(self, node: AllTimeSumNode) -> LinearExpressionData: - raise ValueError("Time operators need to be expanded before linearization.") - - def _value_provider(self) -> ParameterGetter: - if self.value_provider is None: - raise ValueError( - "A value provider must be specified to linearize a time operator node." - " This is required in order to evaluate the value of potential parameters" - " used to specified the time ids on which the time operator applies." - ) - return self.value_provider - - def scenario_operator(self, node: ScenarioOperatorNode) -> LinearExpressionData: - raise ValueError("Scenario operators need to be expanded before linearization.") - - def port_field(self, node: PortFieldNode) -> LinearExpressionData: - raise ValueError("Port fields must be replaced before linearization.") - - def port_field_aggregator( - self, node: PortFieldAggregatorNode - ) -> LinearExpressionData: - raise ValueError( - "Port fields aggregators must be replaced before linearization." + def minimum(self, node: MinNode) -> VectorizedExpr: + operands = [visit(op, self) for op in node.operands] + if all(isinstance(op, xr.DataArray) for op in operands): + result = operands[0] + for op in operands[1:]: + result = xr.where(result <= op, result, op) # type: ignore[no-untyped-call,assignment] + return result + raise NotImplementedError( + "minimum() is only supported for parameter (DataArray) expressions; " + "it cannot be used with decision variables in a linear programme." ) - - -def linearize_expression( - expression: ExpressionNode, - timestep: Optional[int], - scenario: Optional[int], - value_provider: Optional[ParameterGetter] = None, -) -> LinearExpression: - return visit( - expression, - LinearExpressionBuilder( - value_provider=value_provider, timestep=timestep, scenario=scenario - ), - ).build() diff --git a/src/gems/simulation/optimization.py b/src/gems/simulation/optimization.py index b34ebc4d..b40fc4cb 100644 --- a/src/gems/simulation/optimization.py +++ b/src/gems/simulation/optimization.py @@ -11,919 +11,936 @@ # This file is part of the Antares project. """ -The optimization module contains the logic to translate the input model -into a mathematical optimization problem. +Linopy-based optimization problem builder. + +Provides :func:`build_problem` and :class:`OptimizationProblem`. +The builder groups system components by model, then constructs the full +optimization problem in four phases: + +1. Parameter arrays — convert database values to xarray DataArrays indexed + on ``[component, time, scenario]``. +2. Decision variables — create one linopy ``Variable`` per model variable, + covering all components of that model at once. +3. Port arrays — resolve port connections via an incidence matrix so that + port-field expressions are available as linopy ``LinearExpression`` objects. +4. Constraints and objective — traverse each constraint AST once with + :class:`~gems.simulation.linearize.VectorizedLinearExprBuilder` to + produce vectorized linopy constraints added in a single + ``Model.add_constraints()`` call per constraint type. """ -import itertools -import math +from collections import defaultdict from dataclasses import dataclass -from enum import Enum -from typing import Dict, Iterable, List, Optional +from pathlib import Path +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple -import ortools.linear_solver.pywraplp as lp +import linopy +import numpy as np +import xarray as xr -from gems.expression import EvaluationVisitor, ExpressionNode, ValueProvider, visit -from gems.expression.context_adder import add_component_context -from gems.expression.indexing import IndexingStructureProvider, compute_indexation -from gems.expression.indexing_structure import IndexingStructure -from gems.expression.operators_expansion import ProblemDimensions, expand_operators -from gems.expression.port_resolver import PortFieldKey, resolve_port +from gems.expression.expression import is_unbounded +from gems.expression.visitor import visit from gems.model.common import ValueType -from gems.model.constraint import Constraint -from gems.model.port import PortFieldId -from gems.simulation.linear_expression import LinearExpression, Term -from gems.simulation.linearize import ParameterGetter, linearize_expression -from gems.simulation.strategy import ( - MergedProblemStrategy, - ModelSelectionStrategy, - RiskManagementStrategy, - UniformRisk, +from gems.model.model import Model +from gems.model.port import PortField, PortFieldId +from gems.simulation.linearize import ( + VectorizedExpr, + VectorizedLinearExprBuilder, + _linopy_add, ) from gems.simulation.time_block import TimeBlock -from gems.study.data import DataBase -from gems.study.network import Component, Network -from gems.utils import get_or_add +from gems.simulation.vectorized_builder import ShiftValidityVisitor +from gems.study.study import Study +from gems.study.system import Component +if TYPE_CHECKING: + from gems.optim_config.parsing import ElementLocation, OptimConfig, OutOfBoundsMode -@dataclass(eq=True, frozen=True) -class TimestepComponentVariableKey: - """ - Identifies the solver variable for one timestep and one component variable. +# --------------------------------------------------------------------------- +# Public types +# --------------------------------------------------------------------------- + +LinopyModel = linopy.Model +"""Alias for :class:`linopy.Model`, distinguishing it from :class:`gems.model.Model`.""" + +# --------------------------------------------------------------------------- +# Decomposition filter +# --------------------------------------------------------------------------- + + +class DecompositionFilter: + """Decides which model elements belong to a given problem side (master or subproblems). + + Elements not listed in the config default to ``subproblems``. + + Parameters + ---------- + config: + Parsed OptimConfig from optim-config.yml. + target_locations: + The set of :class:`~gems.optim_config.parsing.ElementLocation` values + that should be *included* (not filtered out) by this filter. """ - component_id: str - variable_name: str - block_timestep: Optional[int] = None - scenario: Optional[int] = None - - -def _get_parameter_value( - context: "OptimizationContext", - block_timestep: Optional[int], - scenario: Optional[int], - component_id: str, - name: str, -) -> float: - data = context.database.get_data(component_id, name) - absolute_timestep = context.block_timestep_to_absolute_timestep(block_timestep) - return data.get_value(absolute_timestep, scenario, context.tree_node) - - -def _make_value_provider( - context: "OptimizationContext", - block_timestep: Optional[int], - scenario: Optional[int], -) -> ValueProvider: + def __init__( + self, config: "OptimConfig", target_locations: "Set[ElementLocation]" + ) -> None: + from gems.optim_config.parsing import ElementLocation as EL + + self._target = target_locations + self._default = EL.SUBPROBLEMS + self._vars: Dict[Tuple[str, str], "ElementLocation"] = {} + self._cons: Dict[Tuple[str, str], "ElementLocation"] = {} + self._objs: Dict[Tuple[str, str], "ElementLocation"] = {} + + for mc in config.models: + if mc.model_decomposition is not None: + for v in mc.model_decomposition.variables: + self._vars[(mc.id, v.id)] = v.location + for c in mc.model_decomposition.constraints: + self._cons[(mc.id, c.id)] = c.location + for o in mc.model_decomposition.objective_contributions: + self._objs[(mc.id, o.id)] = o.location + + def include_variable(self, model_id: str, var_name: str) -> bool: + loc = self._vars.get((model_id, var_name), self._default) + return loc in self._target + + def include_constraint(self, model_id: str, constraint_name: str) -> bool: + loc = self._cons.get((model_id, constraint_name), self._default) + return loc in self._target + + def include_objective(self, model_id: str, obj_id: str) -> bool: + loc = self._objs.get((model_id, obj_id), self._default) + return loc in self._target + + +def _apply_validity_mask(expr: VectorizedExpr, mask: xr.DataArray) -> VectorizedExpr: + """Filter *expr* to valid (component, time) entries using *mask*. + + When *expr* has both component and time dimensions the mask is applied + element-wise via ``where``. When only the time dimension is present the + intersection across components is used (conservative fallback). + """ + if not hasattr(expr, "dims"): + return expr + dims = expr.dims # type: ignore[union-attr] + if "component" in dims and "time" in dims: + return expr.where(mask) # type: ignore[union-attr,return-value] + if "time" in dims: + valid_times: List[int] = mask.all("component").values.nonzero()[0].tolist() + return expr.isel(time=valid_times) # type: ignore[union-attr,return-value] + return expr + + +class OutOfBoundsFilter: + """Maps (model_id, constraint_id) to its :class:`OutOfBoundsMode`. + + Used by :class:`_OptimizationProblemBuilder` to determine whether a + constraint should be dropped at timesteps where a shifted term falls + outside the current block. Constraints not listed in the config default + to cyclic wrap-around (no masking applied). + + Parameters + ---------- + config: + Parsed OptimConfig from optim-config.yml. """ - Create a value provider which takes its values from - the parameter values as defined in the network data. - Cannot evaluate expressions which contain variables. + def __init__(self, config: "OptimConfig") -> None: + self._modes: Dict[Tuple[str, str], "OutOfBoundsMode"] = {} + for model_config in config.models: + if model_config.out_of_bounds_processing is not None: + for constraint in model_config.out_of_bounds_processing.constraints: + self._modes[(model_config.id, constraint.id)] = constraint.mode + + def get_mode( + self, model_id: str, constraint_name: str + ) -> "Optional[OutOfBoundsMode]": + return self._modes.get((model_id, constraint_name)) + + +def build_port_arrays( + model: Model, + components: List[Component], + study: Study, + make_builder: Callable[[str, Model], Any], +) -> Dict[PortFieldId, Any]: + """Build port arrays for all ports of *model*. + + For each PortFieldId (port_name, field_name): + - If *model* defines the field (master): evaluate the definition with + ``make_builder(model.id, model)``. + - Otherwise (slave): sum contributions from connected master components + via incidence matrices. + + Parameters + ---------- + model : + The model for which to build port arrays. + components : + Components of this model. + study : + The study, used for component/connection lookup. + make_builder : + Factory ``(model_key: str, model: Model) -> builder``. + Called with an empty port_arrays context for master-field evaluation. """ + comp_ids = [c.id for c in components] + n = len(components) + port_arrays: Dict[PortFieldId, Any] = {} + + for port_name, model_port in model.ports.items(): + for port_field_obj in model_port.port_type.fields: + field_name = port_field_obj.name + pf_id = PortFieldId(port_name, field_name) + + if pf_id in model.port_fields_definitions: + builder = make_builder(model.id, model) + defn = model.port_fields_definitions[pf_id].definition + try: + port_arrays[pf_id] = visit(defn, builder) + except KeyError: + # A variable referenced in the port definition is not + # available in the current problem (e.g. a subproblem-only + # variable when building the master). Treat as zero. + port_arrays[pf_id] = xr.DataArray(0.0) + else: + port_arrays[pf_id] = _build_slave_port_array( + comp_ids, + n, + port_name, + field_name, + study, + make_builder, + ) - class Impl(ValueProvider): - def get_component_variable_value(self, component_id: str, name: str) -> float: - raise NotImplementedError( - "Cannot provide variable value at problem build time." - ) + return port_arrays - def get_component_parameter_value(self, component_id: str, name: str) -> float: - return _get_parameter_value( - context, block_timestep, scenario, component_id, name - ) - def get_variable_value(self, name: str) -> float: - raise NotImplementedError( - "Cannot provide variable value at problem build time." - ) +def _build_slave_port_array( + comp_ids: List[str], + n_components: int, + port_name: str, + field_name: str, + study: Study, + make_builder: Callable[[str, Model], Any], +) -> Any: + """Build a slave port array by summing contributions from connected masters. - def get_parameter_value(self, name: str) -> float: - raise ValueError( - "Parameter must be associated to its component before resolution." - ) + Groups connections by (master_model.id, master_port_field_id), builds an + incidence matrix A[i, j] for each group, and accumulates + ``sum_j A[i,j] * expr_master[j]`` into the result. + """ + per_master: Dict[Tuple[str, PortFieldId], List[Tuple[int, Component]]] = ( + defaultdict(list) + ) - return Impl() + comp_index = {comp_id: i for i, comp_id in enumerate(comp_ids)} + comp_id_set = set(comp_ids) + for cnx in study.system.connections: + for port_ref in [cnx.port1, cnx.port2]: + if ( + port_ref.port_id != port_name + or port_ref.component.id not in comp_id_set + ): + continue + i = comp_index[port_ref.component.id] + master_ref = cnx.master_port.get(PortField(name=field_name)) + if master_ref is None: + continue + master_comp = master_ref.component + master_pf_id = PortFieldId(master_ref.port_id, field_name) + per_master[(master_comp.model.id, master_pf_id)].append((i, master_comp)) + if not per_master: + return xr.DataArray(0.0) -def _compute_expression_value( - expression: ExpressionNode, - context: "OptimizationContext", - block_timestep: Optional[int], - scenario: Optional[int], -) -> float: - value_provider = _make_value_provider(context, block_timestep, scenario) - visitor = EvaluationVisitor(value_provider) - return visit(expression, visitor) + total: Optional[Any] = None + for (master_mk, master_pf_id), conn_list in per_master.items(): + master_comps = study.model_components[master_mk] + master_comp_ids = [c.id for c in master_comps] + n_prime = len(master_comps) -class BlockBorderManagement(Enum): - """ - Class to specify the way of handling the time horizon (or time block) border. - - IGNORE_OUT_OF_FRAME: Ignore terms in constraints that lead to out of horizon data - - CYCLE: Consider all timesteps to be specified modulo the horizon length, this is the actual functioning of Antares - """ + A_data = np.zeros((n_components, n_prime)) + for i, master_comp in conn_list: + j = master_comp_ids.index(master_comp.id) + A_data[i, j] += 1.0 - IGNORE_OUT_OF_FRAME = "IGNORE" - CYCLE = "CYCLE" + A = xr.DataArray( + A_data, + dims=["component", "component_master"], + coords={"component": comp_ids, "component_master": master_comp_ids}, + ) + master_model = study.models[master_mk] + defn = master_model.port_fields_definitions[master_pf_id].definition + master_builder = make_builder(master_mk, master_model) + try: + expr_master = visit(defn, master_builder) + except KeyError: + # The connected model has no variables in the current problem + # (e.g. a subproblem-only model when building the master). + # Its port contribution is treated as zero. + continue -@dataclass -class SolverVariableInfo: - """ - Helper class for constructing the structure file - for Benders solver. It keeps track of the corresponding - column of the variable in the MPS format as well as if it is - present in the objective function or not - """ - - name: str - column_id: int - is_in_objective: bool + expr_master_r = expr_master.rename({"component": "component_master"}) # type: ignore[union-attr] + contribution = (A * expr_master_r).sum("component_master") # type: ignore[operator] + total = contribution if total is None else _linopy_add(total, contribution) -def float_to_int(value: float) -> int: - if isinstance(value, int) or value.is_integer(): - return int(value) - else: - raise ValueError(f"{value} is not an integer.") + return total if total is not None else xr.DataArray(0.0) -class OptimizationContext: +class OptimizationProblem: """ - Helper class to build the optimization problem. - - - Maintains mappings between model and solver objects, - - Maintains mappings between port fields and expressions, - - Provides implementations of interfaces required by various visitors - used to transform expressions (values providers ...). + Wraps a linopy.Model and provides the high-level API for solving and + extracting results. """ def __init__( self, - network: Network, - database: DataBase, + name: str, + linopy_model: LinopyModel, + study: Study, block: TimeBlock, - scenarios: int, - border_management: BlockBorderManagement, - build_strategy: ModelSelectionStrategy = MergedProblemStrategy(), - risk_strategy: RiskManagementStrategy = UniformRisk(), - decision_tree_node: str = "", - use_full_var_name: bool = True, - ): - self._network = network - self._database = database - self._block = block - self._scenarios = scenarios - self._border_management = border_management - self._build_strategy = build_strategy - self._risk_strategy = risk_strategy - self._tree_node = decision_tree_node - self._full_var_name = use_full_var_name - - self._component_variables: Dict[TimestepComponentVariableKey, lp.Variable] = {} - self._solver_variables: Dict[str, SolverVariableInfo] = {} - self._connection_fields_expressions: Dict[ - PortFieldKey, List[ExpressionNode] - ] = {} - - self._constant_value_provider = self._make_constant_value_provider() - self._indexing_structure_provider = self._make_data_structure_provider() - self._parameter_getter = self._make_parameter_getter() - - @property - def network(self) -> Network: - return self._network + linopy_vars: Dict[Tuple[str, str], linopy.Variable], + param_arrays: Dict[Tuple[str, str], xr.DataArray], + objective_constant: float = 0.0, + ) -> None: + self.name = name + self.linopy_model = linopy_model + self.study = study + self.block = block + self._linopy_vars = linopy_vars + self.param_arrays = param_arrays + # Constant term of the objective (linopy cannot represent pure-constant objectives). + self._objective_constant: float = objective_constant @property - def scenarios(self) -> int: - return self._scenarios + def block_length(self) -> int: + return len(self.block.timesteps) - @property - def tree_node(self) -> str: - return self._tree_node + def solve(self, solver_name: str = "highs", **kwargs: object) -> None: + """Solve the problem using the specified solver.""" + # Use io_api="direct" to bypass LP file writing and avoid LP name parsing + # issues in linopy's set_int_index (e.g. constraint names with spaces or + # variables with non-standard characters). + kwargs.setdefault("io_api", "direct") # type: ignore[call-overload] + self.linopy_model.solve(solver_name=solver_name, **kwargs) # type: ignore[arg-type] @property - def build_strategy(self) -> ModelSelectionStrategy: - return self._build_strategy + def status(self) -> str: + """Solver status: 'ok' (optimal found) or 'warning' (infeasible / other).""" + return str(self.linopy_model.status) @property - def risk_strategy(self) -> RiskManagementStrategy: - return self._risk_strategy + def termination_condition(self) -> str: + """Termination condition string, e.g., 'optimal', 'infeasible'.""" + return str(self.linopy_model.termination_condition) @property - def full_var_name(self) -> bool: - return self._full_var_name - - def block_length(self) -> int: - return len(self._block.timesteps) - - @property - def connection_fields_expressions(self) -> Dict[PortFieldKey, List[ExpressionNode]]: - return self._connection_fields_expressions - - # TODO: Need to think about data processing when creating blocks with varying or inequal time steps length (aggregation, sum ?, mean of data ?) - def block_timestep_to_absolute_timestep( - self, block_timestep: Optional[int] - ) -> Optional[int]: + def objective_value(self) -> float: + """Objective function value after solving.""" + return float(self.linopy_model.objective.value) + self._objective_constant # type: ignore[arg-type] + + def export_lp(self, path: Path) -> None: + """Write the problem to an LP file at *path*.""" + self.linopy_model.to_file(path, explicit_coordinate_names=True) + + def get_variable_labels( + self, model_id: str, var_name: str + ) -> Optional[xr.DataArray]: + """Return the linopy integer label DataArray for *var_name* of *model_id*. + + Each entry in the DataArray is the internal integer ID that linopy + assigned to the corresponding scalar variable instance. Returns + ``None`` if the variable was not built in this problem (e.g. it was + filtered out by a :class:`DecompositionFilter`). """ - Timestep may be None for parameters or variables that don't depend on time. - """ - if block_timestep is None: - return None - return self._block.timesteps[self.get_actual_block_timestep(block_timestep)] - - def get_actual_block_timestep(self, block_timestep: int) -> int: - if self._border_management == BlockBorderManagement.CYCLE: - return block_timestep % self.block_length() - else: - raise NotImplementedError() + lv = self._linopy_vars.get((model_id, var_name)) + return lv.labels if lv is not None else None - @property - def database(self) -> DataBase: - return self._database - def _manage_border_timesteps(self, timestep: int) -> int: - if self._border_management == BlockBorderManagement.CYCLE: - return timestep % self.block_length() - else: - raise NotImplementedError +# --------------------------------------------------------------------------- +# Internal builder +# --------------------------------------------------------------------------- - def get_time_indices(self, index_structure: IndexingStructure) -> Iterable[int]: - return range(self.block_length()) if index_structure.time else range(1) - def get_scenario_indices(self, index_structure: IndexingStructure) -> Iterable[int]: - return range(self.scenarios) if index_structure.scenario else range(1) +class _OptimizationProblemBuilder: + """ + Builds the linopy problem in 4 phases: + 1. Build parameter DataArrays for all models. + 2. Create all linopy Variables (uses param arrays for bounds). + 3. Build port arrays via incidence matrices. + 4. Add constraints and objectives to the linopy model. + """ - def get_component_variable( + def __init__( self, - block_timestep: Optional[int], - scenario: Optional[int], - component_id: str, - variable_name: str, - ) -> lp.Variable: - if block_timestep is not None: - block_timestep = self._manage_border_timesteps(block_timestep) - return self._component_variables[ - TimestepComponentVariableKey( - component_id, variable_name, block_timestep, scenario + name: str, + study: Study, + block: TimeBlock, + scenario_ids: List[int], + location_filter: Optional[DecompositionFilter] = None, + oob_filter: Optional[OutOfBoundsFilter] = None, + initial_values: Optional[Dict[Tuple[str, str], xr.DataArray]] = None, + ) -> None: + self.name = name + self.study = study + self.block = block + self.scenario_ids = scenario_ids + self._location_filter = location_filter + self._oob_filter = oob_filter + self._initial_values = initial_values or {} + + self.block_length = len(block.timesteps) + self.time_coord = list(range(self.block_length)) + self.local_scenario_coord = list(range(len(scenario_ids))) + + # Populated during build + self.linopy_model = linopy.Model() + self.linopy_vars: Dict[Tuple[str, str], linopy.Variable] = {} + self.param_arrays: Dict[Tuple[str, str], xr.DataArray] = {} + self.port_arrays: Dict[str, Dict[PortFieldId, VectorizedExpr]] = {} + + def build(self) -> OptimizationProblem: + # Phase 1: parameter arrays + for mk, components in self.study.model_components.items(): + self._build_param_arrays_for_model(self.study.models[mk], components) + + # Phase 2: linopy variables + for mk, components in self.study.model_components.items(): + self._create_variables_for_model(self.study.models[mk], components) + + # Phase 3: port arrays + for mk, components in self.study.model_components.items(): + self._build_port_arrays_for_model(self.study.models[mk], components) + + # Phase 4: constraints + objectives + total_obj: Optional[VectorizedExpr] = None + for mk, components in self.study.model_components.items(): + model = self.study.models[mk] + port_arrays_for_model = self.port_arrays.get(mk, {}) + self._create_constraints_for_model(model, port_arrays_for_model) + total_obj = self._add_objectives_for_model( + model, port_arrays_for_model, total_obj ) - ] - def get_all_component_variables( - self, - ) -> Dict[TimestepComponentVariableKey, lp.Variable]: - return self._component_variables + # Phase 5: carry-over constraints (sequential mode only) + for (mk, var_name), init_val in self._initial_values.items(): + linopy_var = self.linopy_vars.get((mk, var_name)) + if linopy_var is not None and "time" in linopy_var.dims: + safe = f"{mk}__{var_name}".replace("-", "_") + self.linopy_model.add_constraints( + linopy_var.isel(time=0) == init_val, # type: ignore[arg-type] + name=f"carry_over__{safe}", + ) - def register_component_variable( - self, - block_timestep: Optional[int], - scenario: Optional[int], - component_id: str, - model_var_name: str, - variable: lp.Variable, - ) -> None: - key = TimestepComponentVariableKey( - component_id, model_var_name, block_timestep, scenario - ) - if key not in self._component_variables: - self._solver_variables[variable.name()] = SolverVariableInfo( - variable.name(), len(self._solver_variables), False + # Extract constant objective contribution (linopy cannot hold pure constants). + objective_constant = 0.0 + if total_obj is not None and not isinstance( + total_obj, (xr.DataArray, int, float) + ): + self.linopy_model.add_objective(total_obj) # type: ignore[arg-type] + elif total_obj is not None: + if isinstance(total_obj, xr.DataArray): + objective_constant = float(total_obj.sum()) + else: + objective_constant = float(total_obj) + + # linopy requires at least one variable to solve; add a fixed dummy if needed. + if len(self.linopy_model.variables) == 0: + dummy = self.linopy_model.add_variables( + lower=xr.DataArray([0.0], dims=["__dummy_dim"]), + upper=xr.DataArray([0.0], dims=["__dummy_dim"]), + name="__dummy", ) - self._component_variables[key] = variable - - def register_connection_fields_expressions( - self, - component_id: str, - port_name: str, - field_name: str, - expression: ExpressionNode, - ) -> None: - key = PortFieldKey(component_id, PortFieldId(port_name, field_name)) - get_or_add(self._connection_fields_expressions, key, lambda: []).append( - expression + self.linopy_model.add_objective(0 * dummy) # type: ignore[operator] + + return OptimizationProblem( + name=self.name, + linopy_model=self.linopy_model, + study=self.study, + block=self.block, + linopy_vars=self.linopy_vars, + param_arrays=self.param_arrays, + objective_constant=objective_constant, ) - def evaluate_time_bound(self, expression: ExpressionNode) -> int: - res = visit(expression, EvaluationVisitor(self._constant_value_provider)) - return float_to_int(res) + # ------------------------------------------------------------------ + # Phase 1 — Parameter arrays + # ------------------------------------------------------------------ - def _make_data_structure_provider(self) -> IndexingStructureProvider: - """ - Retrieve information in data structure (parameter and variable) from the model - """ - network = self.network - - class Impl(IndexingStructureProvider): - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return ( - network.get_component(component_id).model.variables[name].structure - ) - - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return ( - network.get_component(component_id).model.parameters[name].structure + def _build_param_arrays_for_model( + self, model: Model, components: List[Component] + ) -> None: + T = self.block_length + S = len(self.scenario_ids) + C = len(components) + comp_ids = [c.id for c in components] + abs_timesteps = self.block.timesteps # mapping: block_t → abs_t + + for param in model.parameters.values(): + use_time = param.structure.time + use_scenario = param.structure.scenario + + # Determine the minimal shape for this parameter based on its + # declared structure. Using minimal shapes avoids spurious broadcasting + # (e.g., invest_cost * p_max should not gain time/scenario dims). + if use_time and use_scenario: + data = np.zeros((C, T, S)) + dims = ["component", "time", "scenario"] + coords: Dict[str, object] = { + "component": comp_ids, + "time": self.time_coord, + "scenario": self.local_scenario_coord, + } + elif use_time: + data = np.zeros((C, T)) + dims = ["component", "time"] + coords = {"component": comp_ids, "time": self.time_coord} + elif use_scenario: + data = np.zeros((C, S)) + dims = ["component", "scenario"] + coords = {"component": comp_ids, "scenario": self.local_scenario_coord} + else: + data = np.zeros((C,)) + dims = ["component"] + coords = {"component": comp_ids} + + mc_scenarios = self.scenario_ids if use_scenario else None + for i, c in enumerate(components): + v = self.study.database.get_values( + c.id, + param.name, + abs_timesteps if use_time else None, + mc_scenarios, ) + if use_time and use_scenario: + data[i, :, :] = v # (T, S) + elif use_time: + data[i, :] = v # (T,) or scalar + elif use_scenario: + data[i, :] = v # (S,) or scalar + else: + data[i] = v # scalar + + arr = xr.DataArray(data, dims=dims, coords=coords) + self.param_arrays[(model.id, param.name)] = arr + + # ------------------------------------------------------------------ + # Phase 2 — Variables + # ------------------------------------------------------------------ + + def _create_variables_for_model( + self, model: Model, components: List[Component] + ) -> None: + comp_ids = [c.id for c in components] - def get_parameter_structure(self, name: str) -> IndexingStructure: - raise RuntimeError("Component context should have been initialized.") - - def get_variable_structure(self, name: str) -> IndexingStructure: - raise RuntimeError("Component context should have been initialized.") - - return Impl() - - def expand_operators(self, expression: ExpressionNode) -> ExpressionNode: - dimensions = ProblemDimensions(self.block_length(), self.scenarios) - time_bound_evaluator = self.evaluate_time_bound - return expand_operators( - expression, - dimensions, - time_bound_evaluator, - self._indexing_structure_provider, - ) - - def _make_parameter_getter(self) -> ParameterGetter: - ctxt = self - - class Impl(ParameterGetter): - def get_parameter_value( - self, - component_id: str, - parameter_name: str, - timestep: Optional[int], - scenario: Optional[int], - ) -> float: - return _get_parameter_value( - ctxt, - timestep, - scenario, - component_id, - parameter_name, + for var in model.variables.values(): + if not self._location_filter or self._location_filter.include_variable( + model.id, var.name + ): + # Build coords for this variable + coords: Dict[str, object] = {"component": comp_ids} + dims = ["component"] + if var.structure.time: + coords["time"] = self.time_coord + dims.append("time") + if var.structure.scenario: + coords["scenario"] = self.local_scenario_coord + dims.append("scenario") + + # Shape of this variable (used to broadcast scalar bounds) + var_shape = tuple( + ( + len(comp_ids) + if d == "component" + else ( + len(self.time_coord) + if d == "time" + else len(self.local_scenario_coord) + ) + ) + for d in dims ) - return Impl() - - def linearize_expression( - self, - expanded: ExpressionNode, - timestep: Optional[int] = None, - scenario: Optional[int] = None, - ) -> LinearExpression: - return linearize_expression( - expanded, timestep, scenario, self._parameter_getter - ) - - def compute_indexing(self, expression: ExpressionNode) -> IndexingStructure: - return compute_indexation(expression, self._indexing_structure_provider) - - def _make_constant_value_provider(self) -> ValueProvider: - """ - Value provider which only provides values for constant parameters - """ - context = self - network = self.network - - class Impl(ValueProvider): - def get_component_variable_value( - self, component_id: str, name: str - ) -> float: - raise NotImplementedError( - "Cannot provide variable value at problem build time." + # Build a minimal builder for bound expressions (no variables needed) + bound_builder = VectorizedLinearExprBuilder( + model_id=model.id, + linopy_vars={}, + param_arrays=self.param_arrays, + port_arrays={}, + block_length=self.block_length, ) - def get_component_parameter_value( - self, component_id: str, name: str - ) -> float: - model = network.get_component(component_id).model - structure = model.parameters[name].structure - if structure.time or structure.scenario: - raise ValueError(f"Parameter {name} is not constant.") - return _get_parameter_value(context, None, None, component_id, name) - - def get_variable_value(self, name: str) -> float: - raise NotImplementedError( - "Cannot provide variable value at problem build time." + lower: object = ( + np.full(var_shape, -np.inf) + if var.lower_bound is None + else self._to_bound_array( + visit(var.lower_bound, bound_builder), var_shape, dims + ) ) - - def get_parameter_value(self, name: str) -> float: - raise ValueError( - "Parameter must be associated to its component before resolution." + upper: object = ( + np.full(var_shape, np.inf) + if var.upper_bound is None + else self._to_bound_array( + visit(var.upper_bound, bound_builder), var_shape, dims + ) ) - return Impl() - - -def _compute_indexing( - context: OptimizationContext, constraint: Constraint -) -> IndexingStructure: - return ( - context.compute_indexing(constraint.expression) - or context.compute_indexing(constraint.lower_bound) - or context.compute_indexing(constraint.upper_bound) - ) - - -def _instantiate_model_expression( - model_expression: ExpressionNode, - component_id: str, - optimization_context: OptimizationContext, -) -> ExpressionNode: - """ - Performs common operations that are necessary on model expressions before their actual use: - 1. add component ID for variables and parameters of THIS component - 2. replace port fields by their definition - """ - with_component = add_component_context(component_id, model_expression) - with_component_and_ports = resolve_port( - with_component, component_id, optimization_context.connection_fields_expressions - ) - return with_component_and_ports - - -def _create_constraint( - solver: lp.Solver, - context: OptimizationContext, - constraint: Constraint, -) -> None: - """ - Adds a component-related constraint to the solver. - """ - expanded = context.expand_operators(constraint.expression) - constraint_indexing = _compute_indexing(context, constraint) - - for block_timestep in context.get_time_indices(constraint_indexing): - for scenario in context.get_scenario_indices(constraint_indexing): - linear_expr_at_t = context.linearize_expression( - expanded, block_timestep, scenario - ) - - constraint_data = ConstraintData( - name=constraint.name, - lower_bound=_compute_expression_value( - constraint.lower_bound, - context, - block_timestep, - scenario, - ), - upper_bound=_compute_expression_value( - constraint.upper_bound, - context, - block_timestep, - scenario, - ), - expression=linear_expr_at_t, - ) - make_constraint( - solver, - context, - block_timestep, - scenario, - constraint_data, - ) - - -def _create_objective( - solver: lp.Solver, - opt_context: OptimizationContext, - component: Component, - objective_contribution: ExpressionNode, -) -> None: - instantiated_expr = _instantiate_model_expression( - objective_contribution, component.id, opt_context - ) - expanded = opt_context.expand_operators(instantiated_expr) - linear_expr = opt_context.linearize_expression(expanded) + # Validate bounds: upper must be >= lower across all timesteps/scenarios + lower_arr = lower if isinstance(lower, np.ndarray) else np.array(lower) + upper_arr = upper if isinstance(upper, np.ndarray) else np.array(upper) + for ci, comp_id in enumerate(comp_ids): + lo = np.asarray(lower_arr[ci] if lower_arr.ndim > 0 else lower_arr) + up = np.asarray(upper_arr[ci] if upper_arr.ndim > 0 else upper_arr) + finite = np.isfinite(lo) & np.isfinite(up) + violation = finite & (up < lo) + if np.any(violation): + idx = int(np.argmax(violation)) + raise ValueError( + f"Upper bound ({float(up.flat[idx]):g}) must be strictly " + f"greater than lower bound ({float(lo.flat[idx]):g}) " + f"for variable {comp_id}.{var.name}" + ) - _add_linear_expression_to_objective(solver, opt_context, linear_expr) + prefix = model.id.replace("-", "_") + name = f"{prefix}__{var.name}" + binary = var.data_type == ValueType.BINARY + integer = var.data_type == ValueType.INTEGER + + lv = self.linopy_model.add_variables( + lower=lower, + upper=upper, + coords=coords, + name=name, + binary=binary, + integer=integer, + ) + self.linopy_vars[(model.id, var.name)] = lv + # ------------------------------------------------------------------ + # Phase 3 — Port arrays + # ------------------------------------------------------------------ -def _add_linear_expression_to_objective( - solver: lp.Solver, - context: OptimizationContext, - linear_expr: LinearExpression, - existing_obj: Optional[lp.Objective] = None, -) -> lp.Objective: - """ - Adds terms from a LinearExpression to an existing lp.Objective, or creates a new one. - This function handles the accumulation logic required for objective contributions. - """ - obj: lp.Objective = existing_obj if existing_obj is not None else solver.Objective() - - for term in linear_expr.terms.values(): - solver_var = _get_solver_var(term, context) - context._solver_variables[solver_var.name()].is_in_objective = True - obj.SetCoefficient( - solver_var, - obj.GetCoefficient(solver_var) + term.coefficient, + def _build_port_arrays_for_model( + self, model: Model, components: List[Component] + ) -> None: + # If this model has no variables in the current problem (e.g. a + # subproblem-only model when building the master), its port + # contributions are zero — skip building port arrays for it. + has_vars = any( + (model.id, var_name) in self.linopy_vars for var_name in model.variables ) - - obj.SetOffset(linear_expr.constant + obj.offset()) - return obj - - -@dataclass -class ConstraintData: - name: str - lower_bound: float - upper_bound: float - expression: LinearExpression - - -def _get_solver_var( - term: Term, - context: OptimizationContext, -) -> lp.Variable: - return context.get_component_variable( - term.time_index, - term.scenario_index, - term.component_id, - term.variable_name, - ) - - -def make_constraint( - solver: lp.Solver, - context: OptimizationContext, - block_timestep: int, - scenario: int, - data: ConstraintData, -) -> None: - """ - Adds constraint to the solver. - """ - constraint_name = _solver_constraint_name(context, block_timestep, scenario, data) - - solver_constraint: lp.Constraint = solver.Constraint(constraint_name) - constant: float = 0 - for term in data.expression.terms.values(): - solver_var = _get_solver_var( - term, - context, + if not has_vars and model.variables: + self.port_arrays[model.id] = {} + return + self.port_arrays[model.id] = build_port_arrays( + model, + components, + self.study, + lambda mk_, m: self._make_builder(m, port_arrays={}), ) - coefficient = term.coefficient + solver_constraint.GetCoefficient(solver_var) - solver_constraint.SetCoefficient(solver_var, coefficient) - - constant += data.expression.constant - solver_constraint.SetBounds( - data.lower_bound - constant, data.upper_bound - constant - ) - - -def _solver_constraint_name( - context: OptimizationContext, - block_timestep: int, - scenario: int, - data: ConstraintData, -) -> str: - scenario_suffix = ( - f"_s{scenario}" if (scenario is not None and context.scenarios > 1) else "" - ) - block_suffix = ( - f"_t{block_timestep}" - if (block_timestep is not None and context.block_length() > 1) - else "" - ) - constraint_name = f"{data.name}{scenario_suffix}{block_suffix}" - return constraint_name + # ------------------------------------------------------------------ + # Phase 4 — Constraints and Objectives + # ------------------------------------------------------------------ -class OptimizationProblem: - name: str - solver: lp.Solver - context: OptimizationContext - - def __init__( + def _create_constraints_for_model( self, - name: str, - solver: lp.Solver, - opt_context: OptimizationContext, + model: Model, + port_arrays_for_model: Dict[PortFieldId, VectorizedExpr], ) -> None: - self.name = name - self.solver = solver - self.context = opt_context - - self._register_connection_fields_definitions() - self._create_variables() - self._create_constraints() - self._create_objectives() - - def _register_connection_fields_definitions(self) -> None: - for cnx in self.context.network.connections: - for field_name in list(cnx.master_port.keys()): - master_port = cnx.master_port[field_name] - port_definition = ( - master_port.component.model.port_fields_definitions.get( - PortFieldId( - port_name=master_port.port_id, field_name=field_name.name - ) - ) - ) - expression_node = port_definition.definition # type: ignore - instantiated_expression = add_component_context( - master_port.component.id, expression_node - ) - self.context.register_connection_fields_expressions( - component_id=cnx.port1.component.id, - port_name=cnx.port1.port_id, - field_name=field_name.name, - expression=instantiated_expression, - ) - self.context.register_connection_fields_expressions( - component_id=cnx.port2.component.id, - port_name=cnx.port2.port_id, - field_name=field_name.name, - expression=instantiated_expression, - ) - - def _solver_variable_name( - self, component_id: str, var_name: str, t: Optional[int], s: Optional[int] - ) -> str: - component_prefix = ( - f"{component_id}." if (self.context.full_var_name and component_id) else "" - ) - tree_prefix = ( - f"{self.context.tree_node}." - if (self.context.full_var_name and self.context.tree_node) - else "" - ) - scenario_suffix = ( - f"_s{s}" if (s is not None and self.context.scenarios > 1) else "" - ) - block_suffix = ( - f"_t{t}" if (t is not None and self.context.block_length() > 1) else "" - ) - - # Set solver var name - # Externally, for the Solver, this variable will have a full name - # Internally, it will be indexed by a structure that into account - # the component id, variable name, timestep and scenario separately - return ( - f"{tree_prefix}{component_prefix}{var_name}{scenario_suffix}{block_suffix}" - ) - - def _create_variables(self) -> None: - for component in self.context.network.all_components: - model = component.model - - for model_var in self.context.build_strategy.get_variables(model): - var_indexing = model_var.structure - instantiated_lb_expr = None - instantiated_ub_expr = None - - if model_var.lower_bound: - instantiated_lb_expr = _instantiate_model_expression( - model_var.lower_bound, component.id, self.context - ) - instantiated_lb_expr = self.context.expand_operators( - instantiated_lb_expr - ) - if model_var.upper_bound: - instantiated_ub_expr = _instantiate_model_expression( - model_var.upper_bound, component.id, self.context - ) - instantiated_ub_expr = self.context.expand_operators( - instantiated_ub_expr - ) - - time_indices: Iterable[Optional[int]] = [None] - if var_indexing.is_time_varying(): - time_indices = self.context.get_time_indices(var_indexing) - - scenario_indices: Iterable[Optional[int]] = [None] - if var_indexing.is_scenario_varying(): - scenario_indices = self.context.get_scenario_indices(var_indexing) + """Add all constraints for *model* to the linopy model.""" + builder = self._make_builder(model, port_arrays=port_arrays_for_model) - for t, s in itertools.product(time_indices, scenario_indices): - lower_bound = -self.solver.infinity() - upper_bound = self.solver.infinity() - if instantiated_lb_expr: - lb_linear = self.context.linearize_expression( - instantiated_lb_expr, t, s - ) - if lb_linear.terms: - raise ValueError( - f"Lower bound of variable '{model_var.name}' must be a constant expression (no decision variables)." - ) - lower_bound = lb_linear.constant - if instantiated_ub_expr: - ub_linear = self.context.linearize_expression( - instantiated_ub_expr, t, s - ) - if ub_linear.terms: - raise ValueError( - f"Upper bound of variable '{model_var.name}' must be a constant expression (no decision variables)." - ) - upper_bound = ub_linear.constant - - solver_var_name = self._solver_variable_name( - component.id, model_var.name, t, s - ) - - if lower_bound > upper_bound: - raise ValueError( - f"Upper bound ({upper_bound}) must be strictly greater than lower bound ({lower_bound}) for variable {solver_var_name}" - ) - - if model_var.data_type == ValueType.BOOLEAN: - solver_var = self.solver.BoolVar( - solver_var_name, - ) - elif model_var.data_type == ValueType.INTEGER: - solver_var = self.solver.IntVar( - lower_bound, - upper_bound, - solver_var_name, - ) - else: - solver_var = self.solver.NumVar( - lower_bound, - upper_bound, - solver_var_name, - ) - self.context.register_component_variable( - t, s, component.id, model_var.name, solver_var - ) - - def _create_constraints(self) -> None: - for component in self.context.network.all_components: - for constraint in self.context.build_strategy.get_constraints( - component.model + prefix = model.id.replace("-", "_") + for constraint in model.get_all_constraints(): + if not self._location_filter or self._location_filter.include_constraint( + model.id, constraint.name ): - instantiated_expr = _instantiate_model_expression( - constraint.expression, component.id, self.context - ) - instantiated_lb = _instantiate_model_expression( - constraint.lower_bound, component.id, self.context - ) - instantiated_ub = _instantiate_model_expression( - constraint.upper_bound, component.id, self.context - ) - - instantiated_constraint = Constraint( - name=f"{component.id}.{constraint.name}", - expression=instantiated_expr, - lower_bound=instantiated_lb, - upper_bound=instantiated_ub, - ) - _create_constraint( - self.solver, - self.context, - instantiated_constraint, - ) + # Compute a per-(component, time) validity mask for drop mode. + validity_mask: Optional[xr.DataArray] = None + if self._oob_filter is not None: + from gems.optim_config.parsing import OutOfBoundsMode + + mode = self._oob_filter.get_mode(model.id, constraint.name) + if mode == OutOfBoundsMode.DROP: + validity_mask = visit( + constraint.expression, + ShiftValidityVisitor( + model_id=model.id, + param_arrays=self.param_arrays, + block_length=self.block_length, + ), + ) - def _create_objectives(self) -> None: - """ - Iterates over all network components and creates their corresponding - optimization objectives in the solver. - """ - for component in self.context.network.all_components: - model = component.model - # We distinguish between objective_contributions and classical objective retreived from context build_strategy - if getattr(model, "objective_contributions", None): - self._create_objectives_from_contributions(component) + lhs = visit(constraint.expression, builder) + + # Skip constraints whose LHS evaluated to a pure DataArray (no + # decision variables — e.g. an unconnected port aggregation). + if isinstance(lhs, xr.DataArray): + continue + + if validity_mask is not None: + lhs = _apply_validity_mask(lhs, validity_mask) + + # Sanitize constraint name for LP format (spaces → underscores) + safe_name = constraint.name.replace(" ", "_").replace("-", "_") + + # Lower bound constraint: lhs >= lb (if lb != -inf) + if not is_unbounded(constraint.lower_bound): + lb = visit(constraint.lower_bound, builder) + if validity_mask is not None: + lb = _apply_validity_mask(lb, validity_mask) + name = f"{prefix}__{safe_name}__lb" + con_lb = lhs >= lb # type: ignore[operator] + self.linopy_model.add_constraints(con_lb, name=name) # type: ignore[arg-type] + + # Upper bound constraint: lhs <= ub (if ub != +inf) + if not is_unbounded(constraint.upper_bound): + ub = visit(constraint.upper_bound, builder) + if validity_mask is not None: + ub = _apply_validity_mask(ub, validity_mask) + name = f"{prefix}__{safe_name}__ub" + con_ub = lhs <= ub # type: ignore[operator] + self.linopy_model.add_constraints(con_ub, name=name) # type: ignore[arg-type] + + def _add_objectives_for_model( + self, + model: Model, + port_arrays_for_model: Dict[PortFieldId, VectorizedExpr], + total_obj: Optional[VectorizedExpr], + ) -> Optional[VectorizedExpr]: + """Accumulate objective contributions from *model*.""" + builder = self._make_builder(model, port_arrays=port_arrays_for_model) + + def _accumulate( + acc: Optional[VectorizedExpr], contribution: VectorizedExpr + ) -> VectorizedExpr: + if isinstance(contribution, xr.DataArray): + summed: VectorizedExpr = float(contribution.sum().item()) # type: ignore[assignment] else: - self._create_objectives_from_build_strategy(component) - - def _create_objectives_from_build_strategy(self, component: "Component") -> None: - model = component.model - for objective in self.context.build_strategy.get_objectives(model): - if objective is not None: - _create_objective( - self.solver, - self.context, - component, - self.context.risk_strategy(objective), - ) - - def _create_objectives_from_contributions(self, component: "Component") -> None: - """ - Creates objectives from the 'objective_contributions' dictionary defined - directly on the component's model (new format). + summed = contribution.sum() # type: ignore[union-attr] + return summed if acc is None else _linopy_add(acc, summed) + + if model.objective_contributions: + for obj_id, expr in model.objective_contributions.items(): + if ( + not self._location_filter + or self._location_filter.include_objective(model.id, obj_id) + ) and expr is not None: + obj_term = visit(expr, builder) + total_obj = _accumulate(total_obj, obj_term) + + return total_obj + + # ------------------------------------------------------------------ + # Helpers + # ------------------------------------------------------------------ + + @staticmethod + def _to_bound_array( + val: object, + var_shape: Tuple[int, ...], + dims: List[str], + ) -> np.ndarray: + """Convert a bound value to a numpy array shaped like *var_shape*. + + Handles scalar DataArrays, partial-dim DataArrays (e.g. a constant + parameter used as a bound for a time×scenario variable), plain floats, + and raw numpy arrays. """ - model = component.model - if model.objective_contributions is None: - return - - main_objective: Optional[lp.Objective] = None - for contrib_id, expr in model.objective_contributions.items(): - if expr is None: - continue - - instantiated = _instantiate_model_expression( - expr, component.id, self.context - ) - expanded = self.context.expand_operators(instantiated) - linear_expr = self.context.linearize_expression(expanded) - - main_objective = _add_linear_expression_to_objective( - self.solver, self.context, linear_expr, existing_obj=main_objective - ) + if isinstance(val, xr.DataArray): + if val.dims == (): + return np.full(var_shape, float(val.item())) + arr = val.values # shape may be a subset of var_shape dims + for ax, d in enumerate(dims): + if d not in val.dims: + arr = np.expand_dims(arr, axis=ax) + return np.broadcast_to(arr, var_shape).copy() # type: ignore[return-value] + if isinstance(val, (int, float)): + return np.full(var_shape, float(val)) + return val # type: ignore[return-value] + + def _make_builder( + self, + model: Model, + port_arrays: Dict[PortFieldId, VectorizedExpr], + ) -> VectorizedLinearExprBuilder: + return VectorizedLinearExprBuilder( + model_id=model.id, + linopy_vars=self.linopy_vars, + param_arrays=self.param_arrays, + port_arrays=port_arrays, + block_length=self.block_length, + ) - def export_as_mps(self) -> str: - return self.solver.ExportModelAsMpsFormat(fixed_format=True, obfuscated=False) - def export_as_lp(self) -> str: - return self.solver.ExportModelAsLpFormat(obfuscated=False) +# --------------------------------------------------------------------------- +# Public entry point +# --------------------------------------------------------------------------- def build_problem( - network: Network, - database: DataBase, + study: Study, block: TimeBlock, - scenarios: int, - *, + scenario_ids: List[int], + optim_config: "Optional[OptimConfig]" = None, problem_name: str = "optimization_problem", - border_management: BlockBorderManagement = BlockBorderManagement.CYCLE, - solver_id: str = "SCIP", - build_strategy: ModelSelectionStrategy = MergedProblemStrategy(), - risk_strategy: RiskManagementStrategy = UniformRisk(), - decision_tree_node: str = "", - use_full_var_name: bool = True, + initial_values: Optional[Dict[Tuple[str, str], xr.DataArray]] = None, ) -> OptimizationProblem: """ - Entry point to build the optimization problem for a time period. + Build and return an OptimizationProblem for the given time block. + + Parameters + ---------- + study: + Container holding both the System (components and connections) and + the DataBase (parameter values for those components). + block: + The time block to optimize. + scenario_ids: + List of MC scenario indices to include. Resolution to data-series + column indices is handled transparently by the DataBase. + optim_config: + Optional parsed OptimConfig. When provided, per-constraint + out-of-bounds-processing rules (cyclic vs. drop) are applied. + Constraints not listed default to cyclic. + problem_name: + Label for the linopy model. + initial_values: + Optional carry-over values keyed by ``(model_id, var_name)``. For + each entry a constraint ``var[time=0] == value`` is added, overriding + the cyclic border condition for the first timestep. """ - solver: lp.Solver = lp.Solver.CreateSolver(solver_id) - - database.requirements_consistency(network) - - opt_context = OptimizationContext( - network, - database, - block, - scenarios, - border_management, - build_strategy, - risk_strategy, - decision_tree_node, - use_full_var_name, + study.check_consistency() + + oob_filter = OutOfBoundsFilter(optim_config) if optim_config is not None else None + builder = _OptimizationProblemBuilder( + name=problem_name, + study=study, + block=block, + oob_filter=oob_filter, + scenario_ids=scenario_ids, + initial_values=initial_values, ) + return builder.build() - return OptimizationProblem(problem_name, solver, opt_context) +# --------------------------------------------------------------------------- +# Decomposed build — public entry point +# --------------------------------------------------------------------------- -def fusion_problems( - masters: List[OptimizationProblem], coupler: OptimizationProblem -) -> OptimizationProblem: - if len(masters) == 1: - # Nothing to fusion. Just past down the master - return masters[0] - - root_master = coupler - root_master.name = "master" - - root_vars: Dict[str, lp.Variable] = dict() - root_constraints: Dict[str, lp.Constraint] = dict() - root_objective = root_master.solver.Objective() - - # We stock the coupler's variables to check for - # same name variables in the masters - for var in root_master.solver.variables(): - root_vars[var.name()] = var - - for master in masters: - context = master.context - objective = master.solver.Objective() - - for var in master.solver.variables(): - # If variable not already in coupler, we add it - # Otherwise we update its upper and lower bounds - if var.name() not in root_vars: - root_var = root_master.solver.NumVar(var.lb(), var.ub(), var.name()) - root_master.context._solver_variables[var.name()] = SolverVariableInfo( - var.name(), - len(root_master.context._solver_variables), - context._solver_variables[var.name()].is_in_objective, - ) - else: - root_var = root_vars[var.name()] - root_var.SetLb(var.lb()) - root_var.SetUb(var.ub()) - root_master.context._solver_variables[ - var.name() - ].is_in_objective = context._solver_variables[ - var.name() - ].is_in_objective - - for cstr in master.solver.constraints(): - coeff = cstr.GetCoefficient(var) - # If variable present in constraint, we add the constraint to root - if coeff != 0: - key = f"{master.name}.{cstr.name()}" - if key not in root_constraints: - root_constraints[key] = root_master.solver.Constraint( - cstr.Lb(), cstr.Ub(), key - ) - root_cstr = root_constraints[key] - root_cstr.SetCoefficient(root_var, coeff) - obj_coeff = objective.GetCoefficient(var) - if obj_coeff != 0: - root_objective.SetCoefficient(root_var, obj_coeff) +@dataclass +class DecomposedProblems: + """Holds the results of a decomposed problem build. + + Attributes + ---------- + subproblem: + OptimizationProblem containing all elements whose location is + ``subproblems`` or ``master-and-subproblems``. + master: + OptimizationProblem containing all elements whose location is + ``master`` or ``master-and-subproblems``. ``None`` when the + optim-config declares no master-side elements. + """ + + subproblem: OptimizationProblem + master: Optional[OptimizationProblem] + - return root_master +def build_decomposed_problems( + study: Study, + block: TimeBlock, + scenario_ids: List[int], + optim_config: "OptimConfig", + *, + subproblem_name: str = "subproblem", + master_name: str = "master", +) -> DecomposedProblems: + """Build master and subproblem OptimizationProblems according to *optim_config*. + + The subproblem is always built; it contains every element whose declared + location is ``subproblems`` (the default) or ``master-and-subproblems``. + + The master is built only when at least one element in *optim_config* has + location ``master`` or ``master-and-subproblems``. + + Per-constraint out-of-bounds-processing rules (cyclic vs. drop) defined + in the ``out-of-bounds-processing`` section of optim-config are applied + to both the subproblem and the master. Constraints not listed default to + cyclic wrap-around. + + Parameters + ---------- + study: + Container holding both the System and the DataBase. + Same semantics as :func:`build_problem`. + block, scenario_ids: + Same semantics as :func:`build_problem`. + optim_config: + Parsed ``OptimConfig`` from an ``optim-config.yml`` file. + subproblem_name, master_name: + Labels used for the underlying linopy models. + """ + from gems.optim_config.parsing import ElementLocation + + study.check_consistency() + + oob_filter = OutOfBoundsFilter(optim_config) + + master_locs: Set["ElementLocation"] = { + ElementLocation.MASTER, + ElementLocation.MASTER_AND_SUBPROBLEMS, + } + sub_locs: Set["ElementLocation"] = { + ElementLocation.SUBPROBLEMS, + ElementLocation.MASTER_AND_SUBPROBLEMS, + } + + oob_filter = OutOfBoundsFilter(optim_config) + + subproblem = _OptimizationProblemBuilder( + name=subproblem_name, + study=study, + block=block, + scenario_ids=scenario_ids, + location_filter=DecompositionFilter(optim_config, sub_locs), + oob_filter=oob_filter, + ).build() + + master: Optional[OptimizationProblem] = None + if _has_any_master_element(optim_config): + master = _OptimizationProblemBuilder( + name=master_name, + study=study, + block=block, + scenario_ids=scenario_ids, + location_filter=DecompositionFilter(optim_config, master_locs), + oob_filter=oob_filter, + ).build() + + return DecomposedProblems(subproblem=subproblem, master=master) + + +def _has_any_master_element(config: "OptimConfig") -> bool: + """Return True if *config* declares at least one master-side element.""" + from gems.optim_config.parsing import ElementLocation + + master_locs = {ElementLocation.MASTER, ElementLocation.MASTER_AND_SUBPROBLEMS} + for mc in config.models: + if mc.model_decomposition is not None: + d = mc.model_decomposition + if any(v.location in master_locs for v in d.variables): + return True + if any(c.location in master_locs for c in d.constraints): + return True + if any(o.location in master_locs for o in d.objective_contributions): + return True + return False diff --git a/src/gems/simulation/output_values.py b/src/gems/simulation/output_values.py deleted file mode 100644 index e8d4eed7..00000000 --- a/src/gems/simulation/output_values.py +++ /dev/null @@ -1,388 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -Utility classes to obtain solver results. -""" - -import math -from dataclasses import dataclass, field -from typing import Any, Dict, Mapping, Optional, Set, TypeVar - -from gems.expression import evaluate -from gems.expression.evaluate import EvaluationError -from gems.simulation.extra_output import ExtraOutput, ExtraOutputValueProvider -from gems.simulation.optimization import OptimizationProblem -from gems.simulation.output_values_base import BaseOutputValue -from gems.study.data import TimeScenarioIndex - - -@dataclass -class OutputVariable(BaseOutputValue): - """ - Contains a single solver variable's values and status. - All shared logic is now in BaseOutputValue. - """ - - _basis_status: Dict[TimeScenarioIndex, str] = field( - init=False, default_factory=dict - ) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OutputVariable): - return NotImplemented - # Check base equality first (name, size, value) - if not super().__eq__(other): - return False - # Then check the unique field - return (self.ignore or other.ignore) or ( - self._basis_status == other._basis_status - ) - - def _set( - self, - timestep: Optional[int], - scenario: Optional[int], - value: float, - status: Optional[str] = None, - is_mip: bool = True, - ) -> None: - timestep = 0 if timestep is None else timestep - scenario = 0 if scenario is None else scenario - key = TimeScenarioIndex(timestep, scenario) - if key not in self._value: - size_s = max(self._size[0], scenario + 1) - size_t = max(self._size[1], timestep + 1) - self._size = (size_s, size_t) - - self._value[key] = value - if not is_mip and status is not None: - self._basis_status[key] = status - - -@dataclass -class OutputComponent: - _id: str - _variables: Dict[str, OutputVariable] = field(init=False, default_factory=dict) - _extra_outputs: Dict[str, ExtraOutput] = field(init=False, default_factory=dict) - model: Optional[Any] = field(default=None, init=False) - ignore: bool = field(default=False, init=False) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OutputComponent): - return NotImplemented - return self.is_close(other, rel_tol=0.0, abs_tol=0.0) - - def is_close( - self, - other: "OutputComponent", - *, - rel_tol: float = 1.0e-9, - abs_tol: float = 0.0, - ) -> bool: - return (self.ignore or other.ignore) or ( - self._id == other._id - and _are_mappings_close(self._variables, other._variables, rel_tol, abs_tol) - and _are_mappings_close( - self._extra_outputs, other._extra_outputs, rel_tol, abs_tol - ) - ) - - def __str__(self) -> str: - string = f"{self._id} : {'(ignored)' if self.ignore else ''}\n" - for var in self._variables.values(): - string += f" {str(var)}\n" - if self._extra_outputs: - string += " [Extra Outputs]\n" - for out in self._extra_outputs.values(): - string += f" {out._name}: {out._value}\n" - return string - - def var(self, variable_name: str) -> OutputVariable: - if variable_name not in self._variables: - self._variables[variable_name] = OutputVariable(variable_name) - return self._variables[variable_name] - - def extra_output(self, output_name: str) -> ExtraOutput: - if output_name not in self._extra_outputs: - self._extra_outputs[output_name] = ExtraOutput(output_name) - return self._extra_outputs[output_name] - - def evaluate_extra_outputs(self, problem: OptimizationProblem) -> None: - """Evaluate all model-defined extra outputs and populate self._extra_outputs.""" - if problem is None: - raise ValueError("Expected a valid OptimizationProblem, got None.") - - if self.model is None or self.model.extra_outputs is None: - return - - self._extra_outputs = {} - - for out_id, expr_node in self.model.extra_outputs.items(): - if out_id not in self._extra_outputs: - self._extra_outputs[out_id] = ExtraOutput(out_id) - - self._evaluate_single_extra_output( - self._extra_outputs[out_id], problem, expr_node - ) - - def _evaluate_single_extra_output( - self, - extra_output: ExtraOutput, - problem: OptimizationProblem, - expr_node: Any, - ) -> None: - """ - Evaluate a single ExtraOutput for all time/scenario indices - from the component's variables. - """ - all_indices: Set[TimeScenarioIndex] = set() - for var in self._variables.values(): - all_indices.update(var._value.keys()) - if not all_indices: - all_indices = {TimeScenarioIndex(0, 0)} - - sorted_indices = sorted(all_indices, key=lambda k: (k.time, k.scenario)) - - for idx in sorted_indices: - try: - expanded_expr = problem.context.expand_operators(expr_node) - provider = ExtraOutputValueProvider(self, problem, idx) - val = float(evaluate(expanded_expr, provider)) - except EvaluationError as e: - print( - f"[ERROR] Eval failed for '{extra_output._name}' in {self._id} " - f"at t={idx.time}, s={idx.scenario}: {e}" - ) - val = float("nan") - except Exception as e: - print( - f"[ERROR] Unexpected error for '{extra_output._name}' in {self._id}: {e}" - ) - val = float("nan") - - extra_output._set(idx.time, idx.scenario, val) - - -@dataclass -class OutputValues: - """ - Contains variables and extra outputs after solver work completion. - """ - - problem: Optional[OptimizationProblem] = field(default=None) - _components: Dict[str, OutputComponent] = field(init=False, default_factory=dict) - - def __post_init__(self) -> None: - self._build_components() - self._fill_components() - - def __eq__(self, other: object) -> bool: - if not isinstance(other, OutputValues): - return NotImplemented - return _are_mappings_close(self._components, other._components, 0.0, 0.0) - - def is_close( - self, other: "OutputValues", *, rel_tol: float = 1.0e-9, abs_tol: float = 0.0 - ) -> bool: - return _are_mappings_close( - self._components, other._components, rel_tol, abs_tol - ) - - def __str__(self) -> str: - return "\n" + "".join(f"{comp}\n" for comp in self._components.values()) - - def _build_components(self) -> None: - """ - Initializes component objects and links them to their models. - It only creates the structure, no values are set. - """ - if self.problem is None: - return - - # Ensure a Component object exists for every component in the network - for cmp in self.problem.context.network.all_components: - comp = self.component(cmp.id) - comp.model = cmp.model - - def _fill_components(self) -> None: - """ - Fills all output values (Variables from solver, ExtraOutputs from evaluation). - """ - # 1. Populate Variables - self._evaluate_variables() - - # 2. Evaluate Extra Outputs, which depend on the variables being set - self._evaluate_extra_outputs() - - def component(self, component_id: str) -> OutputComponent: - if component_id not in self._components: - self._components[component_id] = OutputComponent(component_id) - return self._components[component_id] - - def _evaluate_variables(self) -> None: - """ - Populates the OutputVariable values from the solver results. # Docstring updated - """ - if self.problem is None: - return - - is_mip = self.problem.solver.IsMip() - - for key, value in self.problem.context.get_all_component_variables().items(): - status = None if is_mip else value.basis_status() - self.component(key.component_id).var(str(key.variable_name))._set( - key.block_timestep, - key.scenario, - value.solution_value(), - status=status, - is_mip=is_mip, - ) - - def _evaluate_extra_outputs(self) -> None: - """Evaluate extra outputs for all components.""" - if self.problem is None: - return - for comp in self._components.values(): - comp.evaluate_extra_outputs(self.problem) - - -Comparable = TypeVar("Comparable", OutputComponent, OutputVariable, ExtraOutput) - - -def _are_mappings_close( - lhs: Mapping[str, Comparable], - rhs: Mapping[str, Comparable], - rel_tol: float, - abs_tol: float, -) -> bool: - lhs_keys = lhs.keys() - rhs_keys = rhs.keys() - - # Keys present only on the left - for key in lhs_keys - rhs_keys: - if not lhs[key].ignore: - return False - - # Keys present only on the right - for key in rhs_keys - lhs_keys: - if not rhs[key].ignore: - return False - - # Keys in common - for key in lhs_keys & rhs_keys: - left_item = lhs[key] - right_item = rhs[key] - if not left_item.ignore and not left_item.is_close( - right_item, rel_tol=rel_tol, abs_tol=abs_tol - ): - return False - - return True - - -@dataclass(frozen=True) -class BendersSolution: - data: Dict[str, Any] - - def __eq__(self, other: object) -> bool: - if not isinstance(other, BendersSolution): - return NotImplemented - return ( - self.overall_cost == other.overall_cost - and self.candidates == other.candidates - ) - - def is_close( - self, - other: "BendersSolution", - *, - rel_tol: float = 1.0e-9, - abs_tol: float = 0.0, - ) -> bool: - return ( - math.isclose( - self.overall_cost, other.overall_cost, abs_tol=abs_tol, rel_tol=rel_tol - ) - and self.candidates.keys() == other.candidates.keys() - and all( - math.isclose( - self.candidates[key], - other.candidates[key], - rel_tol=rel_tol, - abs_tol=abs_tol, - ) - for key in self.candidates - ) - ) - - def __str__(self) -> str: - lpad = 30 - rpad = 12 - - string = "Benders' solution:\n" - string += f"{'Overall cost':<{lpad}} : {self.overall_cost:>{rpad}}\n" - string += f"{'Investment cost':<{lpad}} : {self.investment_cost:>{rpad}}\n" - string += f"{'Operational cost':<{lpad}} : {self.operational_cost:>{rpad}}\n" - string += "-" * (lpad + rpad + 3) + "\n" - for candidate, investment in self.candidates.items(): - string += f"{candidate:<{lpad}} : {investment:>{rpad}}\n" - - return string - - @property - def investment_cost(self) -> float: - return self.data["solution"]["investment_cost"] - - @property - def operational_cost(self) -> float: - return self.data["solution"]["operational_cost"] - - @property - def overall_cost(self) -> float: - return self.data["solution"]["overall_cost"] - - @property - def candidates(self) -> Dict[str, float]: - return self.data["solution"]["values"] - - @property - def status(self) -> str: - return self.data["solution"]["problem_status"] - - @property - def absolute_gap(self) -> float: - return self.data["solution"]["optimality_gap"] - - @property - def relative_gap(self) -> float: - return self.data["solution"]["relative_gap"] - - @property - def stopping_criterion(self) -> str: - return self.data["solution"]["stopping_criterion"] - - -@dataclass(frozen=True, eq=False) -class BendersMergedSolution(BendersSolution): - @property - def lower_bound(self) -> float: - return self.data["solution"]["lb"] - - @property - def upper_bound(self) -> float: - return self.data["solution"]["ub"] - - -@dataclass(frozen=True, eq=False) -class BendersDecomposedSolution(BendersSolution): - @property - def nb_iterations(self) -> int: - return self.data["solution"]["iteration"] - - @property - def duration(self) -> float: - return self.data["run_duration"] diff --git a/src/gems/simulation/output_values_base.py b/src/gems/simulation/output_values_base.py deleted file mode 100644 index 1ffe8047..00000000 --- a/src/gems/simulation/output_values_base.py +++ /dev/null @@ -1,93 +0,0 @@ -# output_values_base.py - -import math -from abc import ABC, abstractmethod -from dataclasses import dataclass, field -from typing import Dict, List, Optional, Tuple, Union, cast - -from gems.study.data import TimeScenarioIndex - - -@dataclass -class BaseOutputValue(ABC): - """ - Abstract Base Class providing common structure and functionality - for Variable and ExtraOutput classes. It handles storage (_value, _size) - and shared logic (is_close, value property). - """ - - _name: str - _value: Dict[TimeScenarioIndex, float] = field(init=False, default_factory=dict) - _size: Tuple[int, int] = field(init=False, default=(0, 0)) - ignore: bool = field(default=False, init=False) - - def __eq__(self, other: object) -> bool: - if not isinstance(other, BaseOutputValue): - return NotImplemented - return (self.ignore or other.ignore) or ( - self._name == other._name - and self._size == other._size - and self._value == other._value - ) - - def is_close( - self, - other: "BaseOutputValue", - *, - rel_tol: float = 1.0e-9, - abs_tol: float = 0.0, - ) -> bool: - if not isinstance(other, BaseOutputValue): - return NotImplemented - - return (self.ignore or other.ignore) or ( - self._name == other._name - and self._size == other._size - and self._value.keys() == other._value.keys() - and all( - math.isclose( - self._value[key], - other._value[key], - rel_tol=rel_tol, - abs_tol=abs_tol, - ) - for key in self._value - ) - ) - - def __str__(self) -> str: - return f"{self._name} : {str(self.value)} {'(ignored)' if self.ignore else ''}" - - @property - def value(self) -> Union[None, float, List[float], List[List[float]]]: - size_s, size_t = self._size - if size_t == 1: - if size_s == 1: - return self._value.get(TimeScenarioIndex(0, 0)) - else: - return [self._value[TimeScenarioIndex(0, s)] for s in range(size_s)] - else: - return [ - [self._value[TimeScenarioIndex(t, s)] for t in range(size_t)] - for s in range(size_s) - ] - - @value.setter - def value(self, values: Union[float, List[float], List[List[float]]]) -> None: - size_s, size_t = 1, 1 - self._value.clear() - if isinstance(values, list): - size_s = len(values) - for scenario, timesteps in enumerate(values): - if isinstance(timesteps, list): - size_t = len(timesteps) - for timestep, value in enumerate(timesteps): - self._value[TimeScenarioIndex(timestep, scenario)] = value - else: - self._value[TimeScenarioIndex(0, scenario)] = cast(float, timesteps) - else: - self._value[TimeScenarioIndex(0, 0)] = values - self._size = (size_s, size_t) - - def get(self, t: int, s: int) -> float | None: - return self._value.get(TimeScenarioIndex(t, s)) diff --git a/src/gems/simulation/runner.py b/src/gems/simulation/runner.py index 41944eb0..fd764f76 100644 --- a/src/gems/simulation/runner.py +++ b/src/gems/simulation/runner.py @@ -10,6 +10,8 @@ # # This file is part of the Antares project. +"""Wrappers for AntaresXpansion external binaries (benders, merge_mps).""" + import os import pathlib import subprocess @@ -17,7 +19,7 @@ from typing import List -class CommandRunner: +class AntaresXpansionCommandRunner: def __init__( self, binary_path: pathlib.Path, @@ -56,11 +58,11 @@ def run(self) -> int: return res.returncode -class BendersRunner(CommandRunner): +class BendersRunner(AntaresXpansionCommandRunner): def __init__(self, emplacement: pathlib.Path) -> None: super().__init__(pathlib.Path("bin/benders"), ["options.json"], emplacement) -class MergeMPSRunner(CommandRunner): +class MergeMPSRunner(AntaresXpansionCommandRunner): def __init__(self, emplacement: pathlib.Path) -> None: super().__init__(pathlib.Path("bin/merge_mps"), ["options.json"], emplacement) diff --git a/src/gems/simulation/simulation_table.py b/src/gems/simulation/simulation_table.py index b4475207..0719af56 100644 --- a/src/gems/simulation/simulation_table.py +++ b/src/gems/simulation/simulation_table.py @@ -1,14 +1,173 @@ from datetime import datetime from enum import Enum from pathlib import Path -from typing import Any, Dict, Optional, Union +from typing import Any, Dict, List, Optional, Tuple, Union, cast +import numpy as np import pandas as pd -from attr import dataclass +import xarray as xr -# from gems.simulation.extra_output import ExtraOutput -# from optimization import OptimizationProblem # Adjust import as needed -from gems.simulation.output_values import OutputValues + +class OutputView: + """A Time × Scenario pivot for one (component, output) combination. + + Obtain via ``SimulationTable.component(...).output(...)``. + """ + + def __init__(self, df: pd.DataFrame) -> None: + # df: index = absolute-time-index, columns = scenario-index + self._df = df + + @property + def data(self) -> pd.DataFrame: + """Return the underlying Time × Scenario DataFrame.""" + return self._df + + def value( + self, + time_index: Optional[int] = None, + scenario_index: Optional[int] = None, + ) -> Union[pd.DataFrame, "pd.Series[Any]", float]: + """Return results filtered by time and/or scenario index. + + Called with no arguments returns the full Time × Scenario DataFrame. + Called with one argument returns a ``pd.Series``: + - ``value(scenario_index=s)`` → Series indexed by absolute-time-index + - ``value(time_index=t)`` → Series indexed by scenario-index + Called with both arguments returns a scalar ``float``. + """ + if time_index is None and scenario_index is None: + return self._df + if time_index is not None and scenario_index is not None: + return float(cast(Any, self._df.loc[time_index, scenario_index])) + if time_index is not None: + return self._df.loc[time_index] # Series over scenarios + return self._df[scenario_index] # Series over time + + def __repr__(self) -> str: + return repr(self._df) + + +class ComponentView: + """Filtered view of simulation results for one component. + + Obtain via ``SimulationTable.component(...)``. + """ + + def __init__(self, df: pd.DataFrame) -> None: + self._df = df + + def output(self, output_id: str) -> OutputView: + """Return an OutputView for the given output name.""" + col_output = SimulationColumns.OUTPUT.value + col_time = SimulationColumns.ABSOLUTE_TIME_INDEX.value + col_scenario = SimulationColumns.SCENARIO_INDEX.value + col_value = SimulationColumns.VALUE.value + + filtered = self._df[self._df[col_output] == output_id].copy() + # Dimension-independent outputs store None for the missing index. + # Fill with 0 so the pivot is always well-formed and the accessor + # API (value(time_index=t, scenario_index=s)) keeps working. + filtered[col_time] = filtered[col_time].fillna(0) + filtered[col_scenario] = filtered[col_scenario].fillna(0) + pivot = filtered.pivot_table( + index=col_time, + columns=col_scenario, + values=col_value, + aggfunc="first", + ) + pivot.index.name = col_time + pivot.columns.name = col_scenario + return OutputView(pivot) + + +class SimulationTable: + """Wrapper around the raw simulation results DataFrame. + + Provides a fluent accessor API:: + + st = SimulationTableBuilder().build(problem) + + # Full Time × Scenario DataFrame + st.component("gen_1").output("p").value() + + # Scalar at a specific time and scenario + st.component("gen_1").output("p").value(time_index=0, scenario_index=0) + + # Time series for scenario 0 + st.component("gen_1").output("p").value(scenario_index=0) + + # Scenario distribution at time step 3 + st.component("gen_1").output("p").value(time_index=3) + + The underlying long-format DataFrame is accessible via the ``data`` property. + """ + + def __init__(self, df: pd.DataFrame, table_id: str = "") -> None: + self._df = df + self.table_id = table_id + + @property + def data(self) -> pd.DataFrame: + """Return the underlying long-format DataFrame.""" + return self._df + + def component(self, component_id: str) -> ComponentView: + """Return a ComponentView filtered to the given component ID.""" + mask = self._df[SimulationColumns.COMPONENT.value] == component_id + return ComponentView(self._df[mask]) + + def to_csv(self, output_dir: Path) -> Path: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + path = output_dir / f"simulation_table_{self.table_id}.csv" + self._df.to_csv(path, index=False) + return path + + def to_parquet(self, output_dir: Path) -> Path: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + path = output_dir / f"simulation_table_{self.table_id}.parquet" + self._df.to_parquet(path, index=False) + return path + + def to_netcdf(self, output_dir: Path) -> Path: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + path = output_dir / f"simulation_table_{self.table_id}.nc" + self.to_dataset().to_netcdf(path) + return path + + def to_dataset(self) -> xr.Dataset: + """Return simulation results as an xr.Dataset. + + Each output variable becomes a DataArray with dimensions + (component, absolute-time-index, scenario-index). + Scalar rows without component/time/scenario (e.g. objective-value) + are stored as zero-dimensional variables. + """ + df = self._df + col_comp = SimulationColumns.COMPONENT.value + col_out = SimulationColumns.OUTPUT.value + col_time = SimulationColumns.ABSOLUTE_TIME_INDEX.value + col_scen = SimulationColumns.SCENARIO_INDEX.value + col_val = SimulationColumns.VALUE.value + + main = df.dropna(subset=[col_comp, col_time, col_scen]) + indexed = main.set_index([col_comp, col_time, col_scen, col_out])[col_val] + unstacked = indexed.unstack(col_out) + ds = xr.Dataset.from_dataframe(unstacked) + + scalars = df[df[col_comp].isna() & df[col_time].isna()] + for _, row in scalars.iterrows(): + ds[row[col_out]] = xr.DataArray(float(row[col_val])) + + return ds + + +from gems.expression.visitor import visit +from gems.simulation.extra_output import VectorizedExtraOutputBuilder +from gems.simulation.optimization import OptimizationProblem, build_port_arrays class SimulationColumns(str, Enum): @@ -23,7 +182,7 @@ class SimulationColumns(str, Enum): class SimulationTableBuilder: - """Builds simulation tables from solver output values.""" + """Builds simulation tables directly from a OptimizationProblem.""" def __init__(self, simulation_id: Optional[str] = None) -> None: self.simulation_id: str = simulation_id or datetime.now().strftime( @@ -31,130 +190,281 @@ def __init__(self, simulation_id: Optional[str] = None) -> None: ) def build( - self, output_values: OutputValues, absolute_time_offset: Optional[int] = None - ) -> pd.DataFrame: - if output_values.problem is None: - raise ValueError("OutputValues problem is not set.") - - context = output_values.problem.context - block = context._block.id - block_size = context.block_length() + self, + problem: OptimizationProblem, + absolute_time_offset: Optional[int] = None, + scenario_ids_remap: Optional[List[int]] = None, + table_id: str = "", + ) -> SimulationTable: + block = problem.block.id + block_size = problem.block_length - absolute_time_offset = absolute_time_offset or (block - 1) * block_size - assert absolute_time_offset is not None + if absolute_time_offset is None: + # Use the first element of the block's absolute timestep list so that + # the offset is correct for all modes, including blocks with overlap and + # block ids that are not 1-based. + absolute_time_offset = problem.block.timesteps[0] - rows: list[dict[str, Any]] = [] - rows += self._collect_solver_outputs(output_values, block, absolute_time_offset) - rows += self._collect_extra_outputs(output_values, block, absolute_time_offset) - rows.append(self._collect_objective_value(output_values, block)) + dfs: list[pd.DataFrame] = [] + dfs += self._collect_vars_outputs( + problem, block, absolute_time_offset, scenario_ids_remap + ) + dfs += self._collect_extra_outputs( + problem, block, absolute_time_offset, scenario_ids_remap + ) + dfs.append(self._collect_objective_value(problem, block)) - return pd.DataFrame(rows, columns=[c.value for c in SimulationColumns]) + return SimulationTable(pd.concat(dfs, ignore_index=True), table_id=table_id) # ------------------------------------------------------------------------- # Solver outputs # ------------------------------------------------------------------------- - def _collect_solver_outputs( - self, output_values: OutputValues, block: int, abs_offset: int - ) -> list[dict[str, Any]]: - rows: list[dict[str, Any]] = [] - - for comp_id, comp in output_values._components.items(): - if hasattr(comp, "_variables"): - for var in comp._variables.values(): - if hasattr(var, "_value"): - for ts_index, val in var._value.items(): - basis_status = None - if hasattr(var, "_basis_status"): - if isinstance(var._basis_status, str): - basis_status = var._basis_status - elif isinstance(var._basis_status, dict): - basis_status = var._basis_status.get(ts_index) - - var_name = var._name if hasattr(var, "_name") else "" - rows.append( - { - SimulationColumns.BLOCK.value: block, - SimulationColumns.COMPONENT.value: comp_id, - SimulationColumns.OUTPUT.value: var_name, - SimulationColumns.ABSOLUTE_TIME_INDEX.value: abs_offset - + ts_index.time, - SimulationColumns.BLOCK_TIME_INDEX.value: ts_index.time, - SimulationColumns.SCENARIO_INDEX.value: ts_index.scenario, - SimulationColumns.VALUE.value: val, - SimulationColumns.BASIS_STATUS.value: basis_status, - } - ) - return rows + + def _collect_vars_outputs( + self, + problem: OptimizationProblem, + block: int, + abs_offset: int, + scenario_ids_remap: Optional[List[int]] = None, + ) -> list[pd.DataFrame]: + dfs: list[pd.DataFrame] = [] + solution = problem.linopy_model.solution + if solution is None: + return dfs + + for (_, var_name), lv in problem._linopy_vars.items(): + if lv.name not in solution: + continue + + sol_da: xr.DataArray = solution[lv.name] + own_components = list(lv.coords["component"].values) + sol_da = sol_da.sel(component=own_components) + + dfs.append( + self._da_to_df( + sol_da, + var_name, + block, + abs_offset, + basis_status=None, + scenario_ids_remap=scenario_ids_remap, + ) + ) + + return dfs # ------------------------------------------------------------------------- # Extra outputs # ------------------------------------------------------------------------- + def _collect_extra_outputs( - self, output_values: OutputValues, block: int, abs_offset: int - ) -> list[dict[str, Any]]: - rows: list[dict[str, Any]] = [] - - for comp_id, comp in output_values._components.items(): - if hasattr(comp, "_extra_outputs"): - for name, extra_output in comp._extra_outputs.items(): - for ts_index, val in extra_output._value.items(): - # if hasattr(extra_output, "values"): - # for ts_index, val in extra_output.values.items(): - t: int = ts_index.time - s: int = ts_index.scenario - rows.append( - { - SimulationColumns.BLOCK.value: block, - SimulationColumns.COMPONENT.value: comp_id, - SimulationColumns.OUTPUT.value: name, - SimulationColumns.ABSOLUTE_TIME_INDEX.value: abs_offset - + t, - SimulationColumns.BLOCK_TIME_INDEX.value: t, - SimulationColumns.SCENARIO_INDEX.value: s, - SimulationColumns.VALUE.value: val, - SimulationColumns.BASIS_STATUS.value: None, - } - ) - return rows + self, + problem: OptimizationProblem, + block: int, + abs_offset: int, + scenario_ids_remap: Optional[List[int]] = None, + ) -> list[pd.DataFrame]: + dfs: list[pd.DataFrame] = [] + + var_solution_arrays: Dict[Tuple[str, str], xr.DataArray] = {} + solution = problem.linopy_model.solution + if solution is not None: + for (mk, vname), lv in problem._linopy_vars.items(): + if lv.name in solution: + var_solution_arrays[(mk, vname)] = solution[lv.name] + + constraint_dual_arrays = self._collect_constraint_duals(problem) + var_reduced_cost_arrays = self._collect_reduced_costs(problem) + + for mk, components in problem.study.model_components.items(): + model = problem.study.models[mk] + if not model.extra_outputs: + continue + + port_arrays = build_port_arrays( + model, + components, + problem.study, + lambda mk_, m: VectorizedExtraOutputBuilder( + model_id=mk_, + param_arrays=problem.param_arrays, + var_solution_arrays=var_solution_arrays, + constraint_dual_arrays=constraint_dual_arrays, + var_reduced_cost_arrays=var_reduced_cost_arrays, + port_arrays={}, + block_length=problem.block_length, + ), + ) + + for out_id, expr_node in model.extra_outputs.items(): + builder = VectorizedExtraOutputBuilder( + model_id=mk, + param_arrays=problem.param_arrays, + var_solution_arrays=var_solution_arrays, + constraint_dual_arrays=constraint_dual_arrays, + var_reduced_cost_arrays=var_reduced_cost_arrays, + port_arrays=port_arrays, + block_length=problem.block_length, + ) + result_da: xr.DataArray = cast(xr.DataArray, visit(expr_node, builder)) + + if "component" in result_da.dims: + own_ids = [c.id for c in components] + present = [ + c for c in own_ids if c in result_da.coords["component"].values + ] + result_da = result_da.sel(component=present) + + dfs.append( + self._da_to_df( + result_da, + out_id, + block, + abs_offset, + basis_status=None, + scenario_ids_remap=scenario_ids_remap, + ) + ) + + return dfs + + # ------------------------------------------------------------------------- + # Dual / reduced-cost arrays (helpers for _collect_extra_outputs) + # ------------------------------------------------------------------------- + + @staticmethod + def _collect_constraint_duals( + problem: OptimizationProblem, + ) -> Dict[Tuple[str, str], xr.DataArray]: + """Return constraint shadow prices keyed by (model_key, constraint_name).""" + dual_dataset = problem.linopy_model.dual + result: Dict[Tuple[str, str], xr.DataArray] = {} + for mk in problem.study.model_components: + model = problem.study.models[mk] + prefix = mk.replace("-", "_") + all_constraints = {**model.constraints, **model.binding_constraints} + for cname in all_constraints: + safe = cname.replace(" ", "_").replace("-", "_") + dual_val: xr.DataArray = xr.DataArray(0.0) + lb_name = f"{prefix}__{safe}__lb" + ub_name = f"{prefix}__{safe}__ub" + if lb_name in dual_dataset: + dual_val = dual_val + dual_dataset[lb_name] # type: ignore[operator] + if ub_name in dual_dataset: + dual_val = dual_val + dual_dataset[ub_name] # type: ignore[operator] + result[(mk, cname)] = dual_val + return result + + @staticmethod + def _collect_reduced_costs( + problem: OptimizationProblem, + ) -> Dict[Tuple[str, str], xr.DataArray]: + """Return variable reduced costs keyed by (model_key, var_name).""" + solver_model = getattr(problem.linopy_model, "solver_model", None) + if solver_model is None or not hasattr(solver_model, "getSolution"): + return {} + try: + solution = solver_model.getSolution() + col_dual_vals = list(solution.col_dual) + vlabels = problem.linopy_model.matrices.vlabels + rc_series = pd.Series(col_dual_vals, index=vlabels, dtype=float) + rc_series.loc[-1] = float("nan") + + result: Dict[Tuple[str, str], xr.DataArray] = {} + for (mk, vname), lv in problem._linopy_vars.items(): + idx = np.ravel(lv.labels.values) + rc_vals = rc_series.reindex(idx).values.reshape(lv.labels.shape) + result[(mk, vname)] = xr.DataArray( + rc_vals, coords=lv.labels.coords, dims=lv.labels.dims + ) + return result + except Exception: + return {} # ------------------------------------------------------------------------- # Objective value # ------------------------------------------------------------------------- + def _collect_objective_value( - self, output_values: OutputValues, block: int - ) -> dict[str, Any]: - assert output_values.problem is not None, "OutputValues problem is not set" - assert ( - output_values.problem.solver is not None - ), "OutputValues problem.solver is not set" - return { - SimulationColumns.BLOCK.value: block, - SimulationColumns.COMPONENT.value: None, - SimulationColumns.OUTPUT.value: "objective-value", - SimulationColumns.ABSOLUTE_TIME_INDEX.value: None, - SimulationColumns.BLOCK_TIME_INDEX.value: None, - SimulationColumns.SCENARIO_INDEX.value: None, - SimulationColumns.VALUE.value: output_values.problem.solver.Objective().Value(), - SimulationColumns.BASIS_STATUS.value: None, - } - - -@dataclass -class SimulationTableWriter: - """Handles writing simulation tables to CSV.""" - - simulation_table: pd.DataFrame - - def write_csv( - self, - output_dir: Union[str, Path], - simulation_id: str, - optim_nb: int, - ) -> Path: - """Write the simulation table to CSV.""" - output_dir = Path(output_dir) - output_dir.mkdir(parents=True, exist_ok=True) - filename = f"simulation_table_{simulation_id}_{optim_nb}.csv" - filepath = output_dir / filename - self.simulation_table.to_csv(filepath, index=False) - return filepath + self, problem: OptimizationProblem, block: int + ) -> pd.DataFrame: + return pd.DataFrame( + [ + { + SimulationColumns.BLOCK.value: block, + SimulationColumns.COMPONENT.value: None, + SimulationColumns.OUTPUT.value: "objective-value", + SimulationColumns.ABSOLUTE_TIME_INDEX.value: None, + SimulationColumns.BLOCK_TIME_INDEX.value: None, + SimulationColumns.SCENARIO_INDEX.value: None, + SimulationColumns.VALUE.value: problem.objective_value, + SimulationColumns.BASIS_STATUS.value: None, + } + ] + ) + + # ------------------------------------------------------------------------- + # Helpers + # ------------------------------------------------------------------------- + + @staticmethod + def _da_to_df( + da: xr.DataArray, + output_name: str, + block: int, + abs_offset: int, + basis_status: Optional[str], + scenario_ids_remap: Optional[List[int]] = None, + ) -> pd.DataFrame: + """Vectorize a [component?, time?, scenario?] DataArray into a DataFrame. + + Index columns (absolute-time-index, block-time-index, scenario-index) are + set to None for dimensions that are absent from the original DataArray, + signalling that the output is independent of that dimension. + """ + has_time = "time" in da.dims + has_scenario = "scenario" in da.dims + + if "component" not in da.dims: + da = da.expand_dims(component=[None]) + if not has_time: + da = da.expand_dims(time=[0]) + if not has_scenario: + da = da.expand_dims(scenario=[0]) + + da = da.transpose("component", "time", "scenario") + comp_vals: List[Any] = list(da.coords["component"].values) + n_c, n_t, n_s = da.shape + + ci = np.repeat(np.arange(n_c), n_t * n_s) + ti = np.tile(np.repeat(np.arange(n_t), n_s), n_c) + raw_si = ( + scenario_ids_remap if scenario_ids_remap is not None else list(range(n_s)) + ) + si = np.tile(raw_si, n_c * n_t) + + return pd.DataFrame( + { + SimulationColumns.BLOCK.value: block, + SimulationColumns.COMPONENT.value: [ + str(c) if c is not None else None for c in np.array(comp_vals)[ci] + ], + SimulationColumns.OUTPUT.value: output_name, + SimulationColumns.ABSOLUTE_TIME_INDEX.value: ( + (abs_offset + ti) if has_time else None + ), + SimulationColumns.BLOCK_TIME_INDEX.value: ti if has_time else None, + SimulationColumns.SCENARIO_INDEX.value: si if has_scenario else None, + SimulationColumns.VALUE.value: da.values.ravel().astype(float), + SimulationColumns.BASIS_STATUS.value: basis_status, + } + ) + + +def merge_simulation_tables( + tables: List[SimulationTable], table_id: str = "" +) -> SimulationTable: + """Concatenate multiple SimulationTables into one.""" + return SimulationTable( + pd.concat([t.data for t in tables], ignore_index=True), table_id=table_id + ) diff --git a/src/gems/simulation/strategy.py b/src/gems/simulation/strategy.py deleted file mode 100644 index 467dd32a..00000000 --- a/src/gems/simulation/strategy.py +++ /dev/null @@ -1,113 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from abc import ABC, abstractmethod -from typing import Generator, Optional - -from gems.expression import ExpressionNode, literal -from gems.model import Constraint, Model, ProblemContext, Variable - - -class ModelSelectionStrategy(ABC): - """ - Abstract class to specify the strategy of the created problem. - Its derived classes select variables and constraints for the optimization problems: - - InvestmentProblemStrategy: Keep investment and coupling variables and constraints only for a BendersDecomposed master - - OperationalProblemStrategy: Keep operational and coupling variables and constraints only for a BendersDecomposed sub-problems - - MergedProblemStrategy: Keep all variables and constraints - """ - - def get_variables(self, model: Model) -> Generator[Variable, None, None]: - for variable in model.variables.values(): - if self._keep_from_context(variable.context): - yield variable - - def get_constraints(self, model: Model) -> Generator[Constraint, None, None]: - for constraint in model.get_all_constraints(): - if self._keep_from_context(constraint.context): - yield constraint - - @abstractmethod - def _keep_from_context(self, context: ProblemContext) -> bool: - ... - - @abstractmethod - def get_objectives( - self, model: Model - ) -> Generator[Optional[ExpressionNode], None, None]: - ... - - -class MergedProblemStrategy(ModelSelectionStrategy): - def _keep_from_context(self, context: ProblemContext) -> bool: - return True - - def get_objectives( - self, model: Model - ) -> Generator[Optional[ExpressionNode], None, None]: - yield model.objective_operational_contribution - yield model.objective_investment_contribution - - -class InvestmentProblemStrategy(ModelSelectionStrategy): - def _keep_from_context(self, context: ProblemContext) -> bool: - return ( - context == ProblemContext.INVESTMENT or context == ProblemContext.COUPLING - ) - - def get_objectives( - self, model: Model - ) -> Generator[Optional[ExpressionNode], None, None]: - yield model.objective_investment_contribution - - -class OperationalProblemStrategy(ModelSelectionStrategy): - def _keep_from_context(self, context: ProblemContext) -> bool: - return ( - context == ProblemContext.OPERATIONAL or context == ProblemContext.COUPLING - ) - - def get_objectives( - self, model: Model - ) -> Generator[Optional[ExpressionNode], None, None]: - yield model.objective_operational_contribution - - -class RiskManagementStrategy(ABC): - """ - Abstract functor class for risk management - Its derived classes will implement risk measures: - - UniformRisk : The default case. All expressions have the same weight - - ExpectedValue : Computes the product prob * expression - TODO For now, it will only take into account the Expected Value - TODO In the future could have other risk measures? - """ - - def __call__(self, expr: ExpressionNode) -> ExpressionNode: - return self._modify_expression(expr) - - @abstractmethod - def _modify_expression(self, expr: ExpressionNode) -> ExpressionNode: - ... - - -class UniformRisk(RiskManagementStrategy): - def _modify_expression(self, expr: ExpressionNode) -> ExpressionNode: - return expr - - -class ExpectedValue(RiskManagementStrategy): - def __init__(self, prob: float) -> None: - self._prob = prob - - def _modify_expression(self, expr: ExpressionNode) -> ExpressionNode: - return literal(self._prob) * expr diff --git a/src/gems/simulation/time_block.py b/src/gems/simulation/time_block.py index 195ee40c..50e89b89 100644 --- a/src/gems/simulation/time_block.py +++ b/src/gems/simulation/time_block.py @@ -11,21 +11,7 @@ # This file is part of the Antares project. from dataclasses import dataclass -from typing import List, Optional - - -# TODO: Move keys elsewhere as variables have no sense in this file -@dataclass(eq=True, frozen=True) -class TimestepComponentVariableKey: - """ - Identifies the solver variable for one timestep and one component variable. - """ - - tree_node_name: str - component_id: str - variable_name: str - block_timestep: Optional[int] = None - scenario: Optional[int] = None +from typing import List @dataclass(frozen=True) diff --git a/src/gems/simulation/vectorized_builder.py b/src/gems/simulation/vectorized_builder.py new file mode 100644 index 00000000..486a9560 --- /dev/null +++ b/src/gems/simulation/vectorized_builder.py @@ -0,0 +1,721 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Shared abstract base for vectorized expression builders. + +Provides :data:`VectorizedExpr`, :func:`_linopy_add`, and +:class:`VectorizedBuilderBase`, the abstract parent of both the pre-solve +(:class:`~gems.simulation.linearize.VectorizedLinearExprBuilder`) and +post-solve (:class:`~gems.simulation.extra_output.VectorizedExtraOutputBuilder`) +visitors. + +The only axis of variation between the two concrete builders is how a +``VariableNode`` is evaluated: + +- Pre-solve: returns a ``linopy.Variable`` (symbolic decision variable). +- Post-solve: returns an ``xr.DataArray`` of optimal solution values. + +All 18 other :class:`~gems.expression.visitor.ExpressionVisitor` methods are +implemented here once, with DataArray-friendly semantics as their default. +``VectorizedLinearExprBuilder`` overrides a small subset to add linopy-specific +behaviour (operand-swap in addition, type guards in nonlinear functions). +""" + +import functools +from abc import abstractmethod +from dataclasses import dataclass +from typing import Any, Dict, Generic, Optional, Tuple, TypeVar, Union + +import linopy +import numpy as np +import xarray as xr + +from gems.expression.evaluate import EvaluationContext, EvaluationVisitor +from gems.expression.expression import ( + AdditionNode, + AllTimeSumNode, + CeilNode, + ComparisonNode, + DivisionNode, + DualNode, + ExpressionNode, + FloorNode, + LiteralNode, + MaxNode, + MinNode, + MultiplicationNode, + NegationNode, + ParameterNode, + PortFieldAggregatorNode, + PortFieldNode, + ReducedCostNode, + ScenarioOperatorNode, + TimeEvalNode, + TimeShiftNode, + TimeSumNode, + VariableNode, +) +from gems.expression.visitor import ( + ExpressionVisitor, + ExpressionVisitorOperations, + visit, +) +from gems.model.port import PortFieldId + +# --------------------------------------------------------------------------- +# Public types +# --------------------------------------------------------------------------- + +VectorizedExpr = Union[xr.DataArray, linopy.LinearExpression, linopy.Variable] +"""Union of all value types that may appear during vectorized expression building.""" + +# Backward-compatible alias kept for external callers that imported the old name. +LinopyExpression = VectorizedExpr + +T_expr = TypeVar("T_expr", bound=VectorizedExpr) +"""Type variable for the concrete expression type used by a builder subclass.""" + + +# --------------------------------------------------------------------------- +# Module-level helper — also used by optimization.py +# --------------------------------------------------------------------------- + + +def _linopy_add(a: VectorizedExpr, b: VectorizedExpr) -> VectorizedExpr: + """Add two linopy-compatible expressions, keeping linopy types on the left. + + ``xr.DataArray.__add__`` does not recognise linopy objects, so + ``DataArray + LinearExpression`` raises. Putting the linopy type on the + left delegates to ``linopy.__add__`` / ``__radd__``, which handles + DataArrays correctly. For two DataArrays this degenerates to ``a + b``. + """ + if isinstance(a, xr.DataArray) and not isinstance(b, xr.DataArray): + return b + a # type: ignore[operator] + return a + b # type: ignore[operator] + + +# --------------------------------------------------------------------------- +# Abstract base class +# --------------------------------------------------------------------------- + + +@dataclass(kw_only=True) +class VectorizedBuilderBase(ExpressionVisitor[VectorizedExpr], Generic[T_expr]): + """ + Abstract base for vectorized expression builders. + + Implements all :class:`~gems.expression.visitor.ExpressionVisitor` methods + with DataArray-friendly defaults. The sole abstract method is + :meth:`variable` — subclasses differ only in how they resolve a + ``VariableNode`` (symbolic linopy variable vs. solved DataArray value). + + Parameters + ---------- + model_id: + The ``model.id`` string of the ``Model`` object whose AST is being + visited. Used as the first element of dict lookup keys and in error + messages. + param_arrays: + Mapping from ``(model_id, param_name)`` to a DataArray of parameter + values, with dims in ``{component, time, scenario}`` (or a subset). + port_arrays: + Pre-computed expressions for each ``PortFieldId`` of this model. + Keyed by ``PortFieldId(port_name, field_name)``. + block_length: + Number of time steps in the current time block. + """ + + model_id: str + param_arrays: Dict[Tuple[str, str], xr.DataArray] + port_arrays: Dict[PortFieldId, T_expr] + block_length: int + + # ------------------------------------------------------------------ # + # Abstract # + # ------------------------------------------------------------------ # + + @abstractmethod + def variable(self, node: VariableNode) -> T_expr: ... + + # ------------------------------------------------------------------ # + # Leaf nodes # + # ------------------------------------------------------------------ # + + def literal(self, node: LiteralNode) -> xr.DataArray: + return xr.DataArray(node.value) + + def parameter(self, node: ParameterNode) -> xr.DataArray: + key = (self.model_id, node.name) + if key not in self.param_arrays: + raise KeyError( + f"Parameter {node.name!r} not found for model {self.model_id!r}." + ) + return self.param_arrays[key] + + # ------------------------------------------------------------------ # + # Arithmetic operators # + # ------------------------------------------------------------------ # + + def negation(self, node: NegationNode) -> VectorizedExpr: + return -visit(node.operand, self) # type: ignore[operator,return-value] + + def addition(self, node: AdditionNode) -> VectorizedExpr: + """Simple left-to-right addition (DataArray default). + + Overridden in + :class:`~gems.simulation.linearize.VectorizedLinearExprBuilder` + to handle mixed DataArray / linopy-type operands via :func:`_linopy_add`. + """ + operands = [visit(op, self) for op in node.operands] + result = operands[0] + for op in operands[1:]: + result = result + op # type: ignore[operator] + return result # type: ignore[return-value] + + def multiplication(self, node: MultiplicationNode) -> VectorizedExpr: + left = visit(node.left, self) + right = visit(node.right, self) + return left * right # type: ignore[operator,return-value] + + def division(self, node: DivisionNode) -> VectorizedExpr: + left = visit(node.left, self) + right = visit(node.right, self) + return left / right # type: ignore[operator,return-value] + + def comparison(self, node: ComparisonNode) -> VectorizedExpr: + raise NotImplementedError( + f"ComparisonNode is not supported by {type(self).__name__}. " + "Decompose comparisons into expressions before visiting." + ) + + # ------------------------------------------------------------------ # + # Time operators # + # ------------------------------------------------------------------ # + + def time_shift(self, node: TimeShiftNode) -> VectorizedExpr: + operand = visit(node.operand, self) + + # Fast path: compile-time integer constant. + try: + shift = self._eval_int(node.time_shift) + return self._apply_time_shift(operand, shift) # type: ignore[return-value] + except (ValueError, KeyError): + pass + + shift_result = visit(node.time_shift, self) + if not isinstance(shift_result, xr.DataArray): + raise ValueError( + f"Time shift expression must evaluate to a parameter (DataArray), " + f"got {type(shift_result).__name__!r}." + ) + if not shift_result.dims: + return self._apply_time_shift( # type: ignore[return-value] + operand, self._da_to_int(shift_result) + ) + + # Slow path: per-component shift — sum masked contributions. + shift_int = shift_result.astype(int) + unique_shifts = np.unique(shift_int.values) + acc: Optional[Any] = None + for s in unique_shifts: + mask: xr.DataArray = (shift_int == s).astype(float) + shifted = self._apply_time_shift(operand, int(s)) + contrib = shifted * mask # type: ignore[operator] + acc = contrib if acc is None else _linopy_add(acc, contrib) + return acc # type: ignore[return-value] + + def time_eval(self, node: TimeEvalNode) -> VectorizedExpr: + timestep = self._eval_int_expr(node.eval_time) % self.block_length + operand = visit(node.operand, self) + if not self._has_dim(operand, "time"): + return operand # type: ignore[return-value] + return operand.isel(time=timestep) # type: ignore[union-attr,attr-defined,return-value] + + def time_sum(self, node: TimeSumNode) -> VectorizedExpr: + try: + from_shift_scalar: Optional[int] = self._eval_int(node.from_time) + except (ValueError, KeyError): + from_shift_scalar = None + try: + to_shift_scalar: Optional[int] = self._eval_int(node.to_time) + except (ValueError, KeyError): + to_shift_scalar = None + + operand = visit(node.operand, self) + + # Fast path: both bounds are compile-time integer constants. + if from_shift_scalar is not None and to_shift_scalar is not None: + result = self._apply_time_shift(operand, from_shift_scalar) + for shift in range(from_shift_scalar + 1, to_shift_scalar + 1): + result = _linopy_add(result, self._apply_time_shift(operand, shift)) + return result # type: ignore[return-value] + + # Slow path: at least one bound depends on a parameter (per-component). + from_da = ( + xr.DataArray(float(from_shift_scalar)) + if from_shift_scalar is not None + else visit(node.from_time, self) + ) + to_da = ( + xr.DataArray(float(to_shift_scalar)) + if to_shift_scalar is not None + else visit(node.to_time, self) + ) + if not isinstance(from_da, xr.DataArray): + raise ValueError( + f"time_sum from_time must be a parameter expression (DataArray), " + f"got {type(from_da).__name__!r}." + ) + if not isinstance(to_da, xr.DataArray): + raise ValueError( + f"time_sum to_time must be a parameter expression (DataArray), " + f"got {type(to_da).__name__!r}." + ) + from_int = from_da.astype(int) + to_int = to_da.astype(int) + min_from = int(from_int.values.min()) + max_to = int(to_int.values.max()) + + acc: Optional[Any] = None + for shift in range(min_from, max_to + 1): + shifted = self._apply_time_shift(operand, shift) + include_from = ( + (from_int <= shift).astype(float) + if isinstance(from_int, xr.DataArray) and from_int.dims + else xr.DataArray(1.0) + ) + include_to = ( + (to_int >= shift).astype(float) + if isinstance(to_int, xr.DataArray) and to_int.dims + else xr.DataArray(1.0) + ) + mask: xr.DataArray = include_from * include_to + contrib = shifted * mask # type: ignore[operator] + acc = contrib if acc is None else _linopy_add(acc, contrib) + return acc # type: ignore[return-value] + + def all_time_sum(self, node: AllTimeSumNode) -> VectorizedExpr: + operand = visit(node.operand, self) + if self._has_dim(operand, "time"): + return operand.sum("time") # type: ignore[union-attr,attr-defined,return-value] + return operand * self.block_length # type: ignore[operator,return-value] + + # ------------------------------------------------------------------ # + # Scenario operators # + # ------------------------------------------------------------------ # + + def scenario_operator(self, node: ScenarioOperatorNode) -> VectorizedExpr: + if node.name != "Expectation": + raise NotImplementedError( + f"Scenario operator {node.name!r} is not supported. " + "Only 'Expectation' is currently implemented." + ) + operand = visit(node.operand, self) + if self._has_dim(operand, "scenario"): + return operand.sum("scenario") / operand.sizes["scenario"] # type: ignore[union-attr,attr-defined,operator,return-value] + return operand # type: ignore[return-value] + + # ------------------------------------------------------------------ # + # Port operators # + # ------------------------------------------------------------------ # + + def port_field(self, node: PortFieldNode) -> VectorizedExpr: + key = PortFieldId(node.port_name, node.field_name) + if key not in self.port_arrays: + raise KeyError( + f"No port array found for {node.port_name}.{node.field_name} " + f"in model {self.model_id!r}." + ) + return self.port_arrays[key] + + def port_field_aggregator(self, node: PortFieldAggregatorNode) -> VectorizedExpr: + if node.aggregator != "PortSum": + raise NotImplementedError( + f"Port aggregator {node.aggregator!r} is not supported. " + "Only 'PortSum' is currently implemented." + ) + if not isinstance(node.operand, PortFieldNode): + raise ValueError( + f"PortFieldAggregatorNode operand must be a PortFieldNode, " + f"got {type(node.operand).__name__!r}." + ) + port_field_node: PortFieldNode = node.operand + key = PortFieldId(port_field_node.port_name, port_field_node.field_name) + if key not in self.port_arrays: + # No connections: the sum over an empty set is zero. + return xr.DataArray(0.0) # type: ignore[return-value] + return self.port_arrays[key] + + # ------------------------------------------------------------------ # + # Math functions (DataArray default — no guard) # + # ------------------------------------------------------------------ # + # Overridden in VectorizedLinearExprBuilder to raise when operands contain + # linopy types: these operations cannot be expressed as linear constraints. + + def floor(self, node: FloorNode) -> VectorizedExpr: + operand = visit(node.operand, self) + return np.floor(operand) # type: ignore[return-value,arg-type,call-overload] + + def ceil(self, node: CeilNode) -> VectorizedExpr: + operand = visit(node.operand, self) + return np.ceil(operand) # type: ignore[return-value,arg-type,call-overload] + + def maximum(self, node: MaxNode) -> VectorizedExpr: + operands = [visit(op, self) for op in node.operands] + result = operands[0] + for op in operands[1:]: + result = xr.where(result >= op, result, op) # type: ignore[no-untyped-call,assignment,operator] + return result # type: ignore[return-value] + + def minimum(self, node: MinNode) -> VectorizedExpr: + operands = [visit(op, self) for op in node.operands] + result = operands[0] + for op in operands[1:]: + result = xr.where(result <= op, result, op) # type: ignore[no-untyped-call,assignment,operator] + return result # type: ignore[return-value] + + def dual(self, node: DualNode) -> VectorizedExpr: + raise NotImplementedError( + f"dual() is only available in the extra-output builder, " + f"not in {type(self).__name__}." + ) + + def reduced_cost(self, node: ReducedCostNode) -> VectorizedExpr: + raise NotImplementedError( + f"reduced_cost() is only available in the extra-output builder, " + f"not in {type(self).__name__}." + ) + + # ------------------------------------------------------------------ # + # Private helpers # + # ------------------------------------------------------------------ # + + def _apply_time_shift(self, operand: Any, shift: int) -> Any: + """Apply a cyclic (modulo ``self.block_length``) time shift to *operand*. + + Works for both ``xr.DataArray`` and linopy Variable/LinearExpression. + Coordinates are reassigned after ``isel`` so that subsequent xarray + arithmetic aligns positionally rather than by the shifted values. + """ + if not self._has_dim(operand, "time"): + return operand + T = self.block_length + positions = (np.arange(T) + shift) % T + indexer = xr.DataArray(positions, dims="time") + result = operand.isel(time=indexer) # type: ignore[union-attr] + if "time" in result.coords: # type: ignore[operator] + result = result.assign_coords(time=list(range(T))) # type: ignore[union-attr] + return result + + @staticmethod + def _eval_int(node: ExpressionNode) -> int: + """Evaluate a constant expression node to an integer (e.g. a time shift).""" + visitor = EvaluationVisitor(EvaluationContext()) + value = visit(node, visitor) + if isinstance(value, int): + return value + if isinstance(value, float) and value.is_integer(): + return int(value) + raise ValueError( + f"Expected an integer constant expression, got {value!r} from {node!r}." + ) + + @staticmethod + def _da_to_int(da: xr.DataArray) -> int: + """Extract the first element of a DataArray as an integer.""" + val = float(da.values.flat[0]) + if not val.is_integer(): + raise ValueError( + f"Expected integer DataArray value for time shift, got {val!r}." + ) + return int(val) + + def _eval_int_expr(self, node: ExpressionNode) -> int: + """Evaluate a constant integer expression, falling back to ``self`` if needed. + + Tries the static evaluator first; if that fails (e.g. the expression + references a model parameter), evaluates it via ``self`` and extracts + the scalar integer. + """ + try: + return self._eval_int(node) + except KeyError: + result = visit(node, self) + if isinstance(result, xr.DataArray): + return self._da_to_int(result) + raise ValueError( + f"Expected a constant integer expression for time operation, " + f"got {result!r} from {node!r}." + ) + + @staticmethod + def _has_dim(operand: Any, dim: str) -> bool: + """Return True if *operand* has a dimension named *dim*.""" + return dim in operand.dims + + +# --------------------------------------------------------------------------- +# Shift validity helpers — used for OutOfBoundsMode.DROP +# --------------------------------------------------------------------------- + + +class _TimeDependentShiftError(ValueError): + """Raised when a shift amount resolves to a time-dependent parameter.""" + + +class _ScenarioDependentShiftError(ValueError): + """Raised when a shift amount resolves to a scenario-dependent parameter.""" + + +@dataclass(kw_only=True) +class _ShiftAmountEvaluator(ExpressionVisitorOperations[xr.DataArray]): + """Evaluate a shift-amount sub-expression to an xr.DataArray. + + Handles literals, parameter references, and arithmetic over them. + All time/scenario/port/variable nodes raise so that callers can catch + and treat the shift as unevaluable. + """ + + model_id: str + param_arrays: Dict[Tuple[str, str], xr.DataArray] + + def literal(self, node: LiteralNode) -> xr.DataArray: + return xr.DataArray(node.value) + + def parameter(self, node: ParameterNode) -> xr.DataArray: + da = self.param_arrays[(self.model_id, node.name)] + if "time" in da.dims: + raise _TimeDependentShiftError( + f"Parameter '{node.name}' depends on time and cannot be used as a" + " shift amount." + ) + if "scenario" in da.dims: + raise _ScenarioDependentShiftError( + f"Parameter '{node.name}' depends on scenario and cannot be used as a" + " shift amount." + ) + return da + + def variable(self, node: VariableNode) -> xr.DataArray: + raise ValueError("VariableNode cannot appear in a time-shift amount") + + def comparison(self, node: ComparisonNode) -> xr.DataArray: + raise NotImplementedError + + def time_shift(self, node: TimeShiftNode) -> xr.DataArray: + raise NotImplementedError + + def time_eval(self, node: TimeEvalNode) -> xr.DataArray: + raise NotImplementedError + + def time_sum(self, node: TimeSumNode) -> xr.DataArray: + raise NotImplementedError + + def all_time_sum(self, node: AllTimeSumNode) -> xr.DataArray: + raise NotImplementedError + + def scenario_operator(self, node: ScenarioOperatorNode) -> xr.DataArray: + raise NotImplementedError + + def port_field(self, node: PortFieldNode) -> xr.DataArray: + raise NotImplementedError + + def port_field_aggregator(self, node: PortFieldAggregatorNode) -> xr.DataArray: + raise NotImplementedError + + def floor(self, node: FloorNode) -> xr.DataArray: + return np.floor(visit(node.operand, self)) # type: ignore[return-value] + + def ceil(self, node: CeilNode) -> xr.DataArray: + return np.ceil(visit(node.operand, self)) # type: ignore[return-value] + + def maximum(self, node: MaxNode) -> xr.DataArray: + ops = [visit(op, self) for op in node.operands] + return functools.reduce( + lambda a, b: xr.where(a >= b, a, b), ops # type: ignore[return-value,no-untyped-call] + ) + + def minimum(self, node: MinNode) -> xr.DataArray: + ops = [visit(op, self) for op in node.operands] + return functools.reduce( + lambda a, b: xr.where(a <= b, a, b), ops # type: ignore[return-value,no-untyped-call] + ) + + def dual(self, node: DualNode) -> xr.DataArray: + raise NotImplementedError + + def reduced_cost(self, node: ReducedCostNode) -> xr.DataArray: + raise NotImplementedError + + +def _and_mask( + a: Optional[xr.DataArray], b: Optional[xr.DataArray] +) -> Optional[xr.DataArray]: + """AND two optional boolean DataArray masks; None means 'all valid'.""" + if a is None: + return b + if b is None: + return a + return a & b # type: ignore[operator] + + +@dataclass(kw_only=True) +class ShiftValidityVisitor(ExpressionVisitor[Optional[xr.DataArray]]): + """Walk an expression AST and compute a boolean [component, time] validity mask. + + Returns ``None`` when no time-shifting nodes are encountered (all instances + are valid). Otherwise returns a boolean DataArray with at least a ``time`` + dimension (and a ``component`` dimension when shift amounts vary per + component) where ``True`` marks valid constraint instances. + + Used for ``OutOfBoundsMode.DROP``: a ``(component, time)`` entry is + ``True`` iff every time-shifted access in the expression falls within + ``[0, block_length)``. + """ + + model_id: str + param_arrays: Dict[Tuple[str, str], xr.DataArray] + block_length: int + + def _eval_as_da(self, expr: ExpressionNode) -> Optional[xr.DataArray]: + """Evaluate *expr* as a DataArray shift amount. + + Tries a pure-constant evaluation first; falls back to a parameter- + aware DataArray evaluation. Returns ``None`` if neither succeeds so + that the caller can skip the validity check for that shift. + """ + try: + val = visit(expr, EvaluationVisitor(EvaluationContext())) + if isinstance(val, (int, float)): + return xr.DataArray(float(val)) + except (KeyError, NotImplementedError): + pass + try: + return visit( + expr, + _ShiftAmountEvaluator( + model_id=self.model_id, param_arrays=self.param_arrays + ), + ) + except (_TimeDependentShiftError, _ScenarioDependentShiftError): + raise + except (KeyError, ValueError, NotImplementedError): + return None + + # ------------------------------------------------------------------ # + # Time operators — produce validity conditions # + # ------------------------------------------------------------------ # + + def time_shift(self, node: TimeShiftNode) -> Optional[xr.DataArray]: + operand_mask = visit(node.operand, self) + shift_da = self._eval_as_da(node.time_shift) + if shift_da is None: + raise ValueError( + f"Time-shift amount is not evaluable to a literal or parameter: " + f"{node.time_shift!r}. Only literals and parameter references are " + f"supported as shift amounts in OutOfBoundsMode.DROP constraints." + ) + T = self.block_length + t = xr.DataArray(np.arange(T), dims="time") + own_mask = (t + shift_da >= 0) & (t + shift_da < T) + return _and_mask(operand_mask, own_mask) + + def time_sum(self, node: TimeSumNode) -> Optional[xr.DataArray]: + operand_mask = visit(node.operand, self) + from_da = self._eval_as_da(node.from_time) + to_da = self._eval_as_da(node.to_time) + if from_da is None or to_da is None: + unevaluable = node.from_time if from_da is None else node.to_time + raise ValueError( + f"Time-sum bound is not evaluable to a literal or parameter: " + f"{unevaluable!r}. Only literals and parameter references are " + f"supported as bounds in OutOfBoundsMode.DROP constraints." + ) + T = self.block_length + t = xr.DataArray(np.arange(T), dims="time") + # Every offset in [from, to] must be in bounds; the extreme values are binding. + own_mask = (t + from_da >= 0) & (t + to_da < T) + return _and_mask(operand_mask, own_mask) + + # ------------------------------------------------------------------ # + # Structural nodes — AND-propagate children # + # ------------------------------------------------------------------ # + + def negation(self, node: NegationNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def addition(self, node: AdditionNode) -> Optional[xr.DataArray]: + return functools.reduce( + _and_mask, (visit(op, self) for op in node.operands), None + ) + + def multiplication(self, node: MultiplicationNode) -> Optional[xr.DataArray]: + return _and_mask(visit(node.left, self), visit(node.right, self)) + + def division(self, node: DivisionNode) -> Optional[xr.DataArray]: + return _and_mask(visit(node.left, self), visit(node.right, self)) + + def comparison(self, node: ComparisonNode) -> Optional[xr.DataArray]: + return _and_mask(visit(node.left, self), visit(node.right, self)) + + def floor(self, node: FloorNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def ceil(self, node: CeilNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def maximum(self, node: MaxNode) -> Optional[xr.DataArray]: + return functools.reduce( + _and_mask, (visit(op, self) for op in node.operands), None + ) + + def minimum(self, node: MinNode) -> Optional[xr.DataArray]: + return functools.reduce( + _and_mask, (visit(op, self) for op in node.operands), None + ) + + def all_time_sum(self, node: AllTimeSumNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def time_eval(self, node: TimeEvalNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def scenario_operator(self, node: ScenarioOperatorNode) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + def port_field_aggregator( + self, node: PortFieldAggregatorNode + ) -> Optional[xr.DataArray]: + return visit(node.operand, self) + + # ------------------------------------------------------------------ # + # Leaf nodes — no time-shift constraints # + # ------------------------------------------------------------------ # + + def literal(self, node: LiteralNode) -> Optional[xr.DataArray]: + return None + + def parameter(self, node: ParameterNode) -> Optional[xr.DataArray]: + return None + + def variable(self, node: VariableNode) -> Optional[xr.DataArray]: + return None + + def port_field(self, node: PortFieldNode) -> Optional[xr.DataArray]: + return None + + def dual(self, node: DualNode) -> Optional[xr.DataArray]: + return None + + def reduced_cost(self, node: ReducedCostNode) -> Optional[xr.DataArray]: + return None diff --git a/src/gems/study/__init__.py b/src/gems/study/__init__.py index 0f3ba478..f95d2c23 100644 --- a/src/gems/study/__init__.py +++ b/src/gems/study/__init__.py @@ -15,17 +15,11 @@ DataBase, ScenarioIndex, ScenarioSeriesData, - Scenarization, TimeIndex, TimeScenarioIndex, TimeScenarioSeriesData, TimeSeriesData, ) -from .network import ( - Component, - Network, - Node, - PortRef, - PortsConnection, - create_component, -) +from .scenario_builder import ScenarioBuilder +from .study import Study +from .system import Component, PortRef, PortsConnection, System, create_component diff --git a/src/gems/study/data.py b/src/gems/study/data.py index 4664ea68..a941ad7a 100644 --- a/src/gems/study/data.py +++ b/src/gems/study/data.py @@ -10,13 +10,15 @@ # # This file is part of the Antares project. from abc import ABC, abstractmethod -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from typing import Dict, Mapping, Optional +from typing import TYPE_CHECKING, Dict, List, Optional, Union +import numpy as np import pandas as pd -from gems.study.network import Network +if TYPE_CHECKING: + from gems.study.scenario_builder import ScenarioBuilder @dataclass(frozen=True) @@ -35,33 +37,19 @@ class ScenarioIndex: scenario: int -@dataclass(frozen=True) -class Scenarization: - _scenarization: Dict[int, int] - - def get_scenario_for_year(self, year: int) -> int: - return self._scenarization[year] - - def add_year(self, year: int, scenario: int) -> None: - if year in self._scenarization: - raise ValueError(f"the year {year} is already defined") - self._scenarization[year] = scenario - - @dataclass(frozen=True) class AbstractDataStructure(ABC): @abstractmethod def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" - ) -> float: + self, + timestep: Optional[List[int]], + scenario: Optional[np.ndarray], + ) -> Union[float, np.ndarray]: raise NotImplementedError() @abstractmethod def check_requirement(self, time: bool, scenario: bool) -> bool: - """ - Check if the data structure meets certain requirements. - Implement this method in subclasses as needed. - """ + """Check if the data structure meets certain requirements.""" pass @@ -70,11 +58,12 @@ class ConstantData(AbstractDataStructure): value: float def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" + self, + timestep: Optional[List[int]], + scenario: Optional[np.ndarray], ) -> float: return self.value - # ConstantData can be used for time varying or constant models def check_requirement(self, time: bool, scenario: bool) -> bool: if not isinstance(self, ConstantData): raise ValueError("Invalid data type for ConstantData") @@ -83,55 +72,82 @@ def check_requirement(self, time: bool, scenario: bool) -> bool: @dataclass(frozen=True) class TimeSeriesData(AbstractDataStructure): - """ - Container for identifiable timeseries data. - When a model is instantiated as a component, property values - can be defined by referencing one of those timeseries by its ID. - """ + """Time-only series: one value per timestep, scenario-independent.""" - time_series: Mapping[TimeIndex, float] + time_series: pd.Series def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" - ) -> float: + self, + timestep: Optional[List[int]], + scenario: Optional[np.ndarray], + ) -> np.ndarray: if timestep is None: raise KeyError("Time series data requires a time index.") - return self.time_series[TimeIndex(timestep)] + result: np.ndarray = np.asarray(self.time_series.values)[np.asarray(timestep)] + if scenario is not None: + return np.broadcast_to( + result[:, np.newaxis], (len(timestep), len(scenario)) + ) + return result def check_requirement(self, time: bool, scenario: bool) -> bool: if not isinstance(self, TimeSeriesData): raise ValueError("Invalid data type for TimeSeriesData") - return time @dataclass(frozen=True) class ScenarioSeriesData(AbstractDataStructure): - """ - Container for identifiable timeseries data. - When a model is instantiated as a component, property values - can be defined by referencing one of those timeseries by its ID. + """Scenario-only series: one value per data-series column, time-independent. + + ``scenario_series`` is a 1-D numpy array indexed by 0-based column index. """ - scenario_series: Mapping[ScenarioIndex, float] - scenarization: Optional[Scenarization] = None + scenario_series: np.ndarray def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" - ) -> float: + self, + timestep: Optional[List[int]], + scenario: Optional[np.ndarray], + ) -> np.ndarray: if scenario is None: raise KeyError("Scenario series data requires a scenario index.") - if self.scenarization: - scenario = self.scenarization.get_scenario_for_year(scenario) - return self.scenario_series[ScenarioIndex(scenario)] + result = self.scenario_series[scenario] # (S,) + if timestep is not None: + return np.broadcast_to( + result[np.newaxis, :], (len(timestep), len(scenario)) + ) + return result def check_requirement(self, time: bool, scenario: bool) -> bool: if not isinstance(self, ScenarioSeriesData): - raise ValueError("Invalid data type for TimeSeriesData") - + raise ValueError("Invalid data type for ScenarioSeriesData") return scenario +@dataclass(frozen=True) +class TimeScenarioSeriesData(AbstractDataStructure): + """Time × scenario series: values for every (timestep, column) pair.""" + + time_scenario_series: pd.DataFrame + + def get_value( + self, + timestep: Optional[List[int]], + scenario: Optional[np.ndarray], + ) -> np.ndarray: + if timestep is None: + raise KeyError("Time scenario data requires a time index.") + if scenario is None: + raise KeyError("Time scenario data requires a scenario index.") + return self.time_scenario_series.values[np.ix_(np.asarray(timestep), scenario)] + + def check_requirement(self, time: bool, scenario: bool) -> bool: + if not isinstance(self, TimeScenarioSeriesData): + raise ValueError("Invalid data type for TimeScenarioSeriesData") + return time and scenario + + def load_ts_from_file( timeseries_name: Optional[str], path_to_file: Optional[Path] ) -> pd.DataFrame: @@ -162,78 +178,21 @@ def load_ts_from_file( ) -def dataframe_to_time_series(ts_dataframe: pd.DataFrame) -> Dict[TimeIndex, float]: +def dataframe_to_time_series(ts_dataframe: pd.DataFrame) -> pd.Series: if ts_dataframe.shape[1] != 1: raise ValueError( f"Could not convert input data to time series data. Expect data series with exactly one column, got shape {ts_dataframe.shape}" ) - df_index = ts_dataframe.index.astype(int) # Only for mypy - return { - TimeIndex(index): float(value) - for index, value in zip(df_index, ts_dataframe.iloc[:, 0].values) - } + return ts_dataframe.iloc[:, 0] -def dataframe_to_scenario_series( - ts_dataframe: pd.DataFrame, -) -> Dict[ScenarioIndex, float]: +def dataframe_to_scenario_series(ts_dataframe: pd.DataFrame) -> np.ndarray: + """Return a 1-D numpy array of floats indexed by 0-based column index.""" if ts_dataframe.shape[0] != 1: raise ValueError( f"Could not convert input data to scenario series data. Expect data series with exactly one line, got shape {ts_dataframe.shape}" ) - - return { - ScenarioIndex(col_id): float(value) - for col_id, value in zip( - list(range(ts_dataframe.shape[1])), ts_dataframe.iloc[0, :].values - ) - } - - -@dataclass(frozen=True) -class TimeScenarioSeriesData(AbstractDataStructure): - """ - Container for identifiable timeseries data. - When a model is instantiated as a component, property values - can be defined by referencing one of those timeseries by its ID. - """ - - time_scenario_series: pd.DataFrame - scenarization: Optional[Scenarization] = None - - def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" - ) -> float: - if timestep is None: - raise KeyError("Time scenario data requires a time index.") - if scenario is None: - raise KeyError("Time scenario data requires a scenario index.") - if self.scenarization: - scenario = self.scenarization.get_scenario_for_year(scenario) - value = str(self.time_scenario_series.iloc[timestep, scenario]) - return float(value) - - def check_requirement(self, time: bool, scenario: bool) -> bool: - if not isinstance(self, TimeScenarioSeriesData): - raise ValueError("Invalid data type for TimeScenarioSeriesData") - - return time and scenario - - -@dataclass(frozen=True) -class TreeData(AbstractDataStructure): - data: Mapping[str, AbstractDataStructure] - - def get_value( - self, timestep: Optional[int], scenario: Optional[int], node_id: str = "" - ) -> float: - return self.data[node_id].get_value(timestep, scenario) - - def check_requirement(self, time: bool, scenario: bool) -> bool: - return all( - node_data.check_requirement(time, scenario) - for node_data in self.data.values() - ) + return ts_dataframe.iloc[0, :].to_numpy(dtype=float) @dataclass(frozen=True) @@ -243,43 +202,75 @@ class ComponentParameterIndex: class DataBase: - """ - Container for identifiable data. - When a model is instantiated as a component, property values - can be defined by referencing one of those data by its ID. - Data can have different structure : constant, varying in time or scenarios. - """ + """Container for component parameter data. - _data: Dict[ComponentParameterIndex, AbstractDataStructure] + Resolves MC scenario indices to data-series column indices at use time + via the optional ``ScenarioBuilder``. The mapping is vectorized: a + single numpy array index per call, no Python loop over scenarios. + """ - def __init__(self) -> None: + def __init__( + self, + scenario_builder: Optional["ScenarioBuilder"] = None, + ) -> None: self._data: Dict[ComponentParameterIndex, AbstractDataStructure] = {} + self._scenario_groups: Dict[ComponentParameterIndex, Optional[str]] = {} + self._scenario_builder = scenario_builder def get_data(self, component_id: str, parameter_name: str) -> AbstractDataStructure: return self._data[ComponentParameterIndex(component_id, parameter_name)] def add_data( - self, component_id: str, parameter_name: str, data: AbstractDataStructure + self, + component_id: str, + parameter_name: str, + data: AbstractDataStructure, + scenario_group: Optional[str] = None, ) -> None: - self._data[ComponentParameterIndex(component_id, parameter_name)] = data + idx = ComponentParameterIndex(component_id, parameter_name) + self._data[idx] = data + self._scenario_groups[idx] = scenario_group + + def get_values( + self, + component_id: str, + parameter_name: str, + timesteps: Optional[List[int]], + mc_scenarios: Optional[List[int]], + ) -> Union[float, np.ndarray]: + """Return parameter data for all requested timesteps and MC scenarios. + + MC scenario → col_idx resolution happens here (use-time, vectorized): + a single numpy array index, no Python loop over S. + + Returns shape ``(T, S)``, ``(T,)``, ``(S,)``, or scalar depending on + the underlying data type. + """ + idx = ComponentParameterIndex(component_id, parameter_name) + raw_data = self._data[idx] + group = self._scenario_groups.get(idx) + + cols: Optional[np.ndarray] = None + if mc_scenarios is not None: + mc_arr = np.asarray(mc_scenarios, dtype=int) + cols = ( + self._scenario_builder.resolve_vectorized(group, mc_arr) + if self._scenario_builder + else mc_arr + ) + + return raw_data.get_value(timesteps, cols) def get_value( self, index: ComponentParameterIndex, timestep: int, scenario: int - ) -> float: - if index in self._data: - return self._data[index].get_value(timestep, scenario) - else: - raise KeyError(f"Index {index} not found.") - - def requirements_consistency(self, network: Network) -> None: - for component in network.components: - for param in component.model.parameters.values(): - data_structure = self.get_data(component.id, param.name) - - if not data_structure.check_requirement( - component.model.parameters[param.name].structure.time, - component.model.parameters[param.name].structure.scenario, - ): - raise ValueError( - f"Data inconsistency for component: {component.id}, parameter: {param.name}. Requirement not met." - ) + ) -> Union[float, np.ndarray]: + """Scalar convenience wrapper used by tests.""" + result = self.get_values( + index.component_id, + index.parameter_name, + [timestep], + [scenario], + ) + if isinstance(result, np.ndarray): + return result.flat[0] + return result diff --git a/src/gems/study/folder.py b/src/gems/study/folder.py new file mode 100644 index 00000000..54e5c6bb --- /dev/null +++ b/src/gems/study/folder.py @@ -0,0 +1,64 @@ +""" +This module provides functions to load simulation studies from disk. + +A study is defined by a directory containing: +- `input/system.yml`: A file describing the system to be simulated. +- `input/model-libraries/`: A folder containing model library files in YAML format. +- `input/data-series/`: A folder containing data series files. +""" + +from pathlib import Path + +from gems.model.model import Model +from gems.model.parsing import parse_yaml_library +from gems.model.resolve_library import resolve_library +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import ( + build_data_base, + consistency_check, + resolve_system, +) +from gems.study.scenario_builder import ScenarioBuilder +from gems.study.study import Study + + +def load_study(study_dir: Path) -> Study: + """ + Loads a study from a given directory. + + This function reads the system definition, model libraries, and data series + from the study directory, resolves them, and builds the simulation system + and database. + + Args: + study_dir: The path to the study directory. + + Returns: + A Study container holding the resolved system and database. + """ + system_file = study_dir / "input" / "system.yml" + lib_folder = study_dir / "input" / "model-libraries" + series_dir = study_dir / "input" / "data-series" + + input_libraries = [] + for lib_file in lib_folder.glob("*.yml"): + with lib_file.open() as lib: + input_libraries.append(parse_yaml_library(lib)) + + with system_file.open() as c: + input_study = parse_yaml_components(c) + lib_dict = resolve_library(input_libraries) + system = resolve_system(input_study, lib_dict) + model_dict: dict[str, Model] = {} + for library in lib_dict.values(): + model_dict |= library.models + consistency_check(system, model_dict) + + scenario_builder_path = study_dir / "input" / "scenariobuilder.dat" + scenario_builder = ( + ScenarioBuilder.load(scenario_builder_path) + if scenario_builder_path.exists() + else ScenarioBuilder() + ) + database = build_data_base(input_study, series_dir, scenario_builder) + return Study(system=system, database=database, scenario_builder=scenario_builder) diff --git a/src/gems/study/parsing.py b/src/gems/study/parsing.py index 19270314..895873dc 100644 --- a/src/gems/study/parsing.py +++ b/src/gems/study/parsing.py @@ -11,51 +11,43 @@ # This file is part of the Antares project. import argparse -import os from dataclasses import dataclass from pathlib import Path from typing import List, Optional, TextIO, Union -import pandas as pd from pydantic import Field, ValidationError from yaml import safe_load from gems.utils import ModifiedBaseModel -def load_input_system(input_study: Path) -> "InputSystem": +def load_input_system(input_study: Path) -> "SystemSchema": try: with input_study.open() as f: - return InputSystem.model_validate(safe_load(f)) + return SystemSchema.model_validate(safe_load(f)) except ValidationError as e: raise ValueError(f"An error occurred during parsing: {e}") -def parse_yaml_components(input_study: TextIO) -> "InputSystem": +def parse_yaml_components(input_study: TextIO) -> "SystemSchema": tree = safe_load(input_study) - return InputSystem.model_validate(tree["system"]) + return SystemSchema.model_validate(tree["system"]) -def parse_scenario_builder(file: Path) -> pd.DataFrame: - sb = pd.read_csv(file, names=("name", "year", "scenario")) - sb.rename(columns={0: "name", 1: "year", 2: "scenario"}) - return sb - - -class InputAreaConnections(ModifiedBaseModel): +class AreaConnectionsSchema(ModifiedBaseModel): component: str port: str area: str -class InputPortConnections(ModifiedBaseModel): +class PortConnectionsSchema(ModifiedBaseModel): component1: str port1: str component2: str port2: str -class InputComponentParameter(ModifiedBaseModel): +class ComponentParameterSchema(ModifiedBaseModel): id: str time_dependent: bool = False scenario_dependent: bool = False @@ -63,75 +55,45 @@ class InputComponentParameter(ModifiedBaseModel): scenario_group: Optional[str] = None -class InputComponent(ModifiedBaseModel): +class ComponentSchema(ModifiedBaseModel): id: str model: str scenario_group: Optional[str] = None - parameters: Optional[List[InputComponentParameter]] = None + parameters: Optional[List[ComponentParameterSchema]] = None -class InputSystem(ModifiedBaseModel): +class SystemSchema(ModifiedBaseModel): id: Optional[str] = None model_libraries: Optional[str] = None # Parsed but unused for now - components: List[InputComponent] = Field(default_factory=list) - connections: Optional[List[InputPortConnections]] = None - area_connections: Optional[List[InputAreaConnections]] = None - nodes: Optional[List[InputComponent]] = [] + components: List[ComponentSchema] = Field(default_factory=list) + connections: Optional[List[PortConnectionsSchema]] = None + area_connections: Optional[List[AreaConnectionsSchema]] = None @dataclass(frozen=True) class ParsedArguments: - models_path: List[Path] - components_path: Path - timeseries_path: Path - duration: int - nb_scenarios: int + study_dir: Path + optim_config_path: Optional[Path] = None def parse_cli() -> ParsedArguments: parser = argparse.ArgumentParser() parser.add_argument( - "--study", type=Path, help="path to the root directory of the study" - ) - parser.add_argument( - "--models", nargs="+", type=Path, help="list of path to model file, *.yml" - ) - parser.add_argument( - "--component", type=Path, help="path to the component file, *.yml" - ) - parser.add_argument( - "--timeseries", type=Path, help="path to the timeseries directory" + "--study", + type=Path, + required=True, + help="path to the root directory of the study", ) parser.add_argument( - "--duration", type=int, help="duration of the simulation", default=1 - ) - parser.add_argument( - "--scenario", type=int, help="number of scenario of the simulation", default=1 + "--optim-config", + type=Path, + default=None, + dest="optim_config", + help="optional custom path to optim-config.yml (defaults to study_dir/input/optim-config.yml)", ) args = parser.parse_args() - - if args.study: - if args.models or args.component or args.timeseries: - parser.error( - "--study flag can't be use with --models, --component and --timeseries" - ) - - components_path = args.study / "input" / "components" / "components.yml" - timeseries_dir = args.study / "input" / "components" / "series" - model_paths = [ - args.study / "input" / "models" / file - for file in os.listdir(args.study / "input" / "models") - ] - - else: - if not args.models or not args.component: - parser.error("--models and --component must be entered") - - components_path = args.component - timeseries_dir = args.timeseries - model_paths = args.models - return ParsedArguments( - model_paths, components_path, timeseries_dir, args.duration, args.scenario + study_dir=args.study, + optim_config_path=args.optim_config, ) diff --git a/src/gems/study/resolve_components.py b/src/gems/study/resolve_components.py index 4be05b91..82902738 100644 --- a/src/gems/study/resolve_components.py +++ b/src/gems/study/resolve_components.py @@ -9,9 +9,8 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. -from dataclasses import dataclass from pathlib import Path -from typing import Dict, Iterable, List, Optional, Union +from typing import Dict, List, Optional, Tuple, Union import pandas as pd @@ -21,11 +20,9 @@ Component, ConstantData, DataBase, - Network, - Node, PortRef, PortsConnection, - Scenarization, + System, ) from gems.study.data import ( AbstractDataStructure, @@ -36,29 +33,11 @@ dataframe_to_time_series, load_ts_from_file, ) -from gems.study.parsing import InputComponent, InputPortConnections, InputSystem +from gems.study.parsing import ComponentSchema, PortConnectionsSchema, SystemSchema +from gems.study.scenario_builder import ScenarioBuilder -@dataclass(frozen=True) -class System: - components: Dict[str, Component] - nodes: Dict[str, Component] - connections: List[PortsConnection] - - -def system( - components_list: Iterable[Component], - nodes: Iterable[Component], - connections: Iterable[PortsConnection], -) -> System: - return System( - components=dict((m.id, m) for m in components_list), - nodes=dict((n.id, n) for n in nodes), - connections=list(connections), - ) - - -def resolve_system(input_system: InputSystem, libraries: dict[str, Library]) -> System: +def resolve_system(input_system: SystemSchema, libraries: dict[str, Library]) -> System: """ Resolves: - components to be used for study @@ -66,46 +45,42 @@ def resolve_system(input_system: InputSystem, libraries: dict[str, Library]) -> components_list = [ _resolve_component(libraries, m) for m in input_system.components ] - nodes_input = getattr(input_system, "nodes", []) or [] - nodes = [_resolve_component(libraries, n) for n in nodes_input] - all_components: List[Component] = components_list + nodes + + s = System("study") + for component in components_list: + s.add_component(component) + connections_input = getattr(input_system, "connections", []) or [] - connections = [] for cnx in connections_input: - resolved_cnx = _resolve_connections(cnx, all_components) - connections.append(resolved_cnx) + port_ref_1, port_ref_2 = _resolve_port_refs(cnx, components_list) + s.connect(port_ref_1, port_ref_2) - return system(components_list, nodes, connections) + return s def _resolve_component( - libraries: dict[str, Library], component: InputComponent + libraries: dict[str, Library], component: ComponentSchema ) -> Component: lib_id, model_id = component.model.split(".") - model = libraries[lib_id].models[model_id] + model = libraries[lib_id].models[f"{lib_id}.{model_id}"] return Component( model=model, id=component.id, + scenario_group=component.scenario_group, ) -def _resolve_connections( - connection: InputPortConnections, +def _resolve_port_refs( + connection: PortConnectionsSchema, all_components: List[Component], -) -> PortsConnection: - cnx_component1 = connection.component1 - cnx_component2 = connection.component2 - port1 = connection.port1 - port2 = connection.port2 - - component_1 = _get_component_by_id(all_components, cnx_component1) - component_2 = _get_component_by_id(all_components, cnx_component2) +) -> Tuple[PortRef, PortRef]: + component_1 = _get_component_by_id(all_components, connection.component1) + component_2 = _get_component_by_id(all_components, connection.component2) assert component_1 is not None and component_2 is not None - port_ref_1 = PortRef(component_1, port1) - port_ref_2 = PortRef(component_2, port2) - - return PortsConnection(port_ref_1, port_ref_2) + return PortRef(component_1, connection.port1), PortRef( + component_2, connection.port2 + ) def _get_component_by_id( @@ -115,55 +90,43 @@ def _get_component_by_id( return components_dict.get(component_id) -def consistency_check( - input_study: Dict[str, Component], input_models: Dict[str, Model] -) -> bool: +def consistency_check(system: System, input_models: Dict[str, Model]) -> bool: """ - Checks if all components in the Components instances have a valid model from the library. + Checks if all components in the System have a valid model from the library. Returns True if all components are consistent, raises ValueError otherwise. """ # TODO: Update this consistency check to check if each component have a valid model from the lib it refers to (and not all libs) model_ids_set = input_models.keys() - for component_id, component in input_study.items(): + for component in system.all_components: if component.model.id not in model_ids_set: raise ValueError( - f"Error: Component {component_id} has invalid model ID: {component.model.id}" + f"Error: Component {component.id} has invalid model ID: {component.model.id}" ) return True -def build_network(system: System) -> Network: - # It seems that System and Network are almost the same thing -> could be simplified ? - network = Network("study") - - for node_id, node in system.nodes.items(): - node = Node(model=node.model, id=node_id) - network.add_node(node) - - for component in system.components.values(): - network.add_component(component) - - for connection in system.connections: - network.connect(connection.port1, connection.port2) - return network - - def build_data_base( - input_system: InputSystem, timeseries_dir: Optional[Path] + input_system: SystemSchema, + timeseries_dir: Optional[Path], + scenario_builder: Optional[ScenarioBuilder] = None, ) -> DataBase: - database = DataBase() - nodes_input = getattr(input_system, "nodes", []) or [] - input_system_objects = input_system.components + nodes_input - for comp in input_system_objects: - # This idiom allows mypy to 'ignore' the fact that comp.parameter can be None + """Build a DataBase from the system description and optional ScenarioBuilder. + + When a ``ScenarioBuilder`` is provided, each parameter's ``scenario_group`` + is recorded so that ``DataBase.get_values()`` can resolve MC scenario indices + to data-series column indices at use time. + """ + database = DataBase(scenario_builder=scenario_builder) + for comp in input_system.components: for param in comp.parameters or []: + group = param.scenario_group or comp.scenario_group param_value = _build_data( param.time_dependent, param.scenario_dependent, param.value, timeseries_dir, ) - database.add_data(comp.id, param.id, param_value) + database.add_data(comp.id, param.id, param_value, scenario_group=group) return database @@ -173,67 +136,22 @@ def _build_data( scenario_dependent: bool, param_value: Union[float, str], timeseries_dir: Optional[Path], - scenarization: Optional[Scenarization] = None, ) -> AbstractDataStructure: if isinstance(param_value, str): - # Should happen only if time-dependent or scenario-dependent ts_data = load_ts_from_file(param_value, timeseries_dir) if time_dependent and scenario_dependent: - return TimeScenarioSeriesData(ts_data, scenarization) + return TimeScenarioSeriesData(ts_data) elif time_dependent: return TimeSeriesData(dataframe_to_time_series(ts_data)) elif scenario_dependent: - return ScenarioSeriesData( - dataframe_to_scenario_series(ts_data), scenarization - ) + return ScenarioSeriesData(dataframe_to_scenario_series(ts_data)) else: raise ValueError( f"A float value is expected for constant data, got {param_value}" ) - else: # param_value is a float, we should be in the constant data case + else: if time_dependent or scenario_dependent: raise ValueError( f"A timeseries name is expected for time or scenario dependent data, got {param_value}" ) return ConstantData(float(param_value)) - - -def _resolve_scenarization( - scenario_builder_data: pd.DataFrame, -) -> Dict[str, Scenarization]: - output: Dict[str, Scenarization] = {} - for i, row in scenario_builder_data.iterrows(): - if row["name"] in output: - output[row["name"]].add_year(row["year"], row["scenario"]) - else: - output[row["name"]] = Scenarization({row["year"]: row["scenario"]}) - return output - - -def build_scenarized_data_base( - input_comp: InputSystem, - scenario_builder_data: pd.DataFrame, - timeseries_dir: Optional[Path], -) -> DataBase: - database = DataBase() - scenarizations = _resolve_scenarization(scenario_builder_data) - - for comp in input_comp.components: - scenarization = None - if comp.scenario_group: - scenarization = scenarizations[comp.scenario_group] - - # This idiom allows mypy to 'ignore' the fact that comp.parameter can be None - for param in comp.parameters or []: - if param.scenario_group: - scenarization = scenarizations[param.scenario_group] - param_value = _build_data( - param.time_dependent, - param.scenario_dependent, - param.value, - timeseries_dir, - scenarization, - ) - database.add_data(comp.id, param.id, param_value) - - return database diff --git a/src/gems/study/runner.py b/src/gems/study/runner.py new file mode 100644 index 00000000..d9d7c142 --- /dev/null +++ b/src/gems/study/runner.py @@ -0,0 +1,47 @@ +from datetime import datetime +from pathlib import Path +from typing import Optional + +from gems.optim_config.parsing import ( + OptimConfig, + load_optim_config, + validate_optim_config, +) +from gems.session.session import SimulationSession +from gems.study.folder import load_study + + +def run_study( + study_dir: Path, + optim_config_path: Optional[Path] = None, +) -> None: + """ + Runs a simulation study and exports results to CSV. + + Run parameters (time scope, solver options, scenario scope) are read from + ``study_dir/input/optim-config.yml``; defaults apply when the file is absent. + Results are written to ``study_dir/output/{run_id}/``. + + Args: + study_dir: The path to the study directory. + optim_config_path: Optional custom path to an optim-config YAML file. + If not provided, defaults to ``study_dir/input/optim-config.yml``. + """ + study = load_study(study_dir) + + resolved_config_path = optim_config_path or ( + study_dir / "input" / "optim-config.yml" + ) + optim_config = load_optim_config(resolved_config_path) or OptimConfig() + validate_optim_config(optim_config, study.system) + + run_id = datetime.now().strftime("%Y%m%dT%H%M") + output_dir = study_dir / "output" / run_id + session = SimulationSession( + study=study, + optim_config=optim_config, + run_id=run_id, + output_dir=output_dir, + ) + table = session.run() + table.to_csv(output_dir) diff --git a/src/gems/study/scenario_builder.py b/src/gems/study/scenario_builder.py new file mode 100644 index 00000000..24beb324 --- /dev/null +++ b/src/gems/study/scenario_builder.py @@ -0,0 +1,107 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Dict, Optional + +import numpy as np + + +@dataclass +class ScenarioBuilder: + """Maps MC scenario indices to data-series column indices (0-based). + + Loaded from a ``scenariobuilder.dat`` file whose lines have the form:: + + scenario_group, mc_scenario = time_serie_number + + where ``time_serie_number`` is 1-based (column 1 = first column of the + data-series file). Internally the mapping is stored as one numpy array + per group so that ``resolve_vectorized`` is a pure array index — no + Python loop over scenarios. + + When no file is present the builder is empty and ``resolve_vectorized`` + returns the MC scenario indices unchanged (identity mapping) for parameters + whose ``scenario_group`` is ``None``. + """ + + _group_arrays: Dict[str, np.ndarray] = field(default_factory=dict) + + def resolve_vectorized( + self, scenario_group: Optional[str], mc_scenarios: np.ndarray + ) -> np.ndarray: + """Return 0-based col_idx array for a vector of MC scenario indices. + + When ``scenario_group`` is ``None`` the parameter carries no group and + the identity mapping (col_idx == mc_scenario) is returned. + + Raises ``ValueError`` when a non-None group is not present in the + scenario builder — this is always a misconfiguration (e.g. a typo in + ``system.yml`` or a missing entry in ``scenariobuilder.dat``). + + No Python loop — pure numpy array indexing. + """ + if scenario_group is None: + return mc_scenarios + if scenario_group not in self._group_arrays: + raise ValueError( + f"Scenario group '{scenario_group}' is not defined in the scenario builder. " + f"Known groups: {list(self._group_arrays.keys())}." + ) + arr = self._group_arrays[scenario_group] + out_of_bounds = mc_scenarios[mc_scenarios >= len(arr)] + if len(out_of_bounds): + raise ValueError( + f"MC scenario indices {list(out_of_bounds)} are not defined for group " + f"'{scenario_group}' (defined range: 0–{len(arr) - 1})." + ) + return arr[mc_scenarios] + + @classmethod + def load(cls, path: Path) -> "ScenarioBuilder": + """Parse a ``scenariobuilder.dat`` file. + + Each non-blank, non-comment line must follow:: + + group_name, mc_scenario = time_serie_number + + ``time_serie_number`` is 1-based; it is stored internally as a + 0-based column index. + """ + raw: Dict[str, Dict[int, int]] = {} + with path.open() as f: + for line in f: + line = line.strip() + if not line or line.startswith("#"): + continue + left, _, right = line.partition("=") + parts = [p.strip() for p in left.split(",")] + group, mc_scenario = parts[0], int(parts[1]) + raw.setdefault(group, {})[mc_scenario] = int(right.strip()) - 1 + + group_arrays: Dict[str, np.ndarray] = {} + for group, mapping in raw.items(): + max_mc = max(mapping) + expected = set(range(max_mc + 1)) + missing = sorted(expected - set(mapping.keys())) + if missing: + raise ValueError( + f"Scenario group '{group}' has no mapping for MC scenarios {missing}. " + f"All {max_mc + 1} MC scenarios (0..{max_mc}) must be explicitly mapped." + ) + arr = np.empty(max_mc + 1, dtype=int) + for mc, col in mapping.items(): + arr[mc] = col + group_arrays[group] = arr + + return cls(group_arrays) diff --git a/src/gems/study/study.py b/src/gems/study/study.py new file mode 100644 index 00000000..07caa2fb --- /dev/null +++ b/src/gems/study/study.py @@ -0,0 +1,77 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from collections import defaultdict +from dataclasses import dataclass, field +from functools import cached_property +from typing import Dict, List + +from gems.model.model import Model +from gems.study.data import DataBase +from gems.study.scenario_builder import ScenarioBuilder +from gems.study.system import Component, System + + +@dataclass +class Study: + """ + Container that pairs a System (component topology and connections) with a + DataBase (parameter values for those components). + + These two objects are always used together to build an optimisation + problem. ``Study`` gathers them into a single, coherent unit and + provides the cross-validation logic that was previously spread between + ``DataBase.requirements_consistency`` and the callers of + ``build_problem``. + """ + + system: System + database: DataBase + scenario_builder: ScenarioBuilder = field(default_factory=ScenarioBuilder) + + @cached_property + def model_components(self) -> Dict[str, List[Component]]: + """Components grouped by their model.id.""" + result: Dict[str, List[Component]] = defaultdict(list) + for component in self.system.all_components: + result[component.model.id].append(component) + return dict(result) + + @cached_property + def models(self) -> Dict[str, Model]: + """All unique models in the system, keyed by model.id.""" + return { + mk: components[0].model for mk, components in self.model_components.items() + } + + def check_consistency(self) -> None: + """Validate that the database supplies data for every parameter of every + component defined in the system. + + Raises + ------ + ValueError + If a required data entry is missing or its time/scenario structure + does not match what the model parameter expects. + """ + for component in self.system.components: + for param in component.model.parameters.values(): + data_structure = self.database.get_data(component.id, param.name) + + if not data_structure.check_requirement( + component.model.parameters[param.name].structure.time, + component.model.parameters[param.name].structure.scenario, + ): + raise ValueError( + f"Data inconsistency for component: {component.id}, " + f"parameter: {param.name}. Requirement not met." + ) diff --git a/src/gems/study/network.py b/src/gems/study/system.py similarity index 54% rename from src/gems/study/network.py rename to src/gems/study/system.py index 53a53687..1a808cb4 100644 --- a/src/gems/study/network.py +++ b/src/gems/study/system.py @@ -11,14 +11,15 @@ # This file is part of the Antares project. """ -The network module defines the data model for an instance of network, -including nodes, links, and components (model instantations). +The system module defines the data model for an instance of a system, +including components and connections. """ -import itertools from dataclasses import dataclass, field, replace -from typing import Any, Dict, Iterable, List, cast +from typing import Any, Dict, Iterable, List, Optional +from gems.expression.degree import contains_dual_or_reduced_cost +from gems.expression.expression import ExpressionNode, PortFieldAggregatorNode, PortFieldNode from gems.model import PortField, PortType from gems.model.model import Model from gems.model.port import PortFieldId @@ -33,6 +34,7 @@ class Component: model: Model id: str + scenario_group: Optional[str] = None def is_variable_in_model(self, var_id: str) -> bool: return var_id in self.model.variables.keys() @@ -41,27 +43,68 @@ def replicate(self, /, **changes: Any) -> "Component": return replace(self, **changes) -def create_component(model: Model, id: str) -> Component: - return Component(model=model, id=id) +def create_component( + model: Model, id: str, scenario_group: Optional[str] = None +) -> Component: + return Component(model=model, id=id, scenario_group=scenario_group) @dataclass(frozen=True) -class Node(Component): - """ - A node in the network. - """ +class PortRef: + component: Component + port_id: str - pass +def _uses_sum_connections_on(expr: ExpressionNode, port_name: str, field_name: str) -> bool: + """Return True if expr contains sum_connections(port_name.field_name).""" + if ( + isinstance(expr, PortFieldAggregatorNode) + and isinstance(expr.operand, PortFieldNode) + and expr.operand.port_name == port_name + and expr.operand.field_name == field_name + ): + return True + for child in _children(expr): + if _uses_sum_connections_on(child, port_name, field_name): + return True + return False + + +def _check_no_dual_rc_sum_connections( + master_ref: PortRef, slave_ref: PortRef, field_name: str +) -> None: + master_model = master_ref.component.model + slave_model = slave_ref.component.model + master_port_id = PortFieldId(port_name=master_ref.port_id, field_name=field_name) + master_def = master_model.port_fields_definitions.get(master_port_id) + if master_def is None or not contains_dual_or_reduced_cost(master_def.definition): + return + for bc in slave_model.binding_constraints.values(): + if _uses_sum_connections_on(bc.expression, slave_ref.port_id, field_name): + raise ValueError( + f"Port-field definition '{master_port_id}' contains " + f"dual/reduced_cost (non-linear) and cannot be aggregated " + f"via sum_connections in a binding-constraint of model " + f"'{slave_model.id}'." + ) -def create_node(model: Model, id: str) -> Node: - return Node(model=model, id=id) +def _children(expr: ExpressionNode) -> list: + from gems.expression.expression import ( + AdditionNode, + BinaryOperatorNode, + MaxNode, + MinNode, + UnaryOperatorNode, + ) -@dataclass(frozen=True) -class PortRef: - component: Component - port_id: str + if isinstance(expr, (AdditionNode, MaxNode, MinNode)): + return list(expr.operands) + if isinstance(expr, BinaryOperatorNode): + return [expr.left, expr.right] + if isinstance(expr, UnaryOperatorNode): + return [expr.operand] + return [] @dataclass() @@ -104,9 +147,10 @@ def __validate_ports(self) -> None: f"Port field {field_name} on {port_1.port_name} has 2 definitions." ) - self.master_port[PortField(name=field_name)] = ( - self.port1 if def1 else self.port2 - ) + master_ref = self.port1 if def1 else self.port2 + slave_ref = self.port2 if def1 else self.port1 + self.master_port[PortField(name=field_name)] = master_ref + _check_no_dual_rc_sum_connections(master_ref, slave_ref, field_name) def get_port_type(self) -> PortType: port_1 = self.port1.component.model.ports.get(self.port1.port_id) @@ -121,51 +165,37 @@ def replicate(self, /, **changes: Any) -> "PortsConnection": @dataclass -class Network: +class System: """ - Network model: simply nodes, links, and components. + A system model consisting of components and their connections. """ id: str - _nodes: Dict[str, Node] = field(init=False, default_factory=dict) _components: Dict[str, Component] = field(init=False, default_factory=dict) _connections: List[PortsConnection] = field(init=False, default_factory=list) - def _check_node_exists(self, node_id: str) -> None: - if node_id not in self._nodes: - raise ValueError(f"Node {node_id} does not exist in the network.") + def _check_model_id_unique(self, model: Model) -> None: + for existing in self.all_components: + if existing.model is not model and existing.model.id == model.id: + raise ValueError( + f"Model id '{model.id}' is already used by a different model object in this system." + ) def add_component(self, component: Component) -> None: require_not_none(component) + self._check_model_id_unique(component.model) self._components[component.id] = component def get_component(self, component_id: str) -> Component: - """ - Returns the component (possibly a node) corresponding to this ID. - """ - res = self._components.get(component_id, None) - return res if res else self._nodes[component_id] + return self._components[component_id] @property def components(self) -> Iterable[Component]: return self._components.values() - def add_node(self, node: Node) -> None: - self._nodes[node.id] = node - - def get_node(self, node_id: str) -> Node: - return self._nodes[node_id] - - @property - def nodes(self) -> Iterable[Node]: - return self._nodes.values() - @property def all_components(self) -> Iterable[Component]: - """ - An iterable over both nodes and components. - """ - return itertools.chain(self.nodes, self.components) + return self._components.values() def connect(self, port1: PortRef, port2: PortRef) -> None: ports_connection = PortsConnection(port1, port2) @@ -179,13 +209,10 @@ def get_connection(self, idx: int) -> PortsConnection: return self._connections[idx] def is_empty(self) -> bool: - return (not self._nodes) and (not self._components) and (not self._connections) - - def replicate(self, /, **changes: Any) -> "Network": - replica = replace(self, **changes) + return (not self._components) and (not self._connections) - for node in self.nodes: - replica.add_node(cast(Node, node.replicate())) + def replicate(self, /, **changes: Any) -> "System": + replica: System = replace(self, **changes) for component in self.components: replica.add_component(component.replicate()) diff --git a/tests/e2e/functional/conftest.py b/tests/e2e/functional/conftest.py index 39ef7b0f..057e63a7 100644 --- a/tests/e2e/functional/conftest.py +++ b/tests/e2e/functional/conftest.py @@ -13,9 +13,9 @@ import pytest -from gems.model.parsing import InputLibrary, parse_yaml_library +from gems.model.parsing import LibrarySchema, parse_yaml_library from gems.model.resolve_library import Library, resolve_library -from gems.study.parsing import InputSystem, parse_yaml_components +from gems.study.parsing import SystemSchema, parse_yaml_components @pytest.fixture(scope="session") @@ -34,7 +34,7 @@ def series_dir() -> Path: @pytest.fixture -def input_system(systems_dir: Path) -> InputSystem: +def input_system(systems_dir: Path) -> SystemSchema: compo_file = systems_dir / "system.yml" with compo_file.open() as c: @@ -42,7 +42,7 @@ def input_system(systems_dir: Path) -> InputSystem: @pytest.fixture -def input_library(libs_dir: Path) -> InputLibrary: +def input_library(libs_dir: Path) -> LibrarySchema: library = libs_dir / "lib_unittest.yml" with library.open() as lib: @@ -58,3 +58,13 @@ def lib_dict(libs_dir: Path) -> dict[str, Library]: lib_dict = resolve_library([input_lib]) return lib_dict + + +@pytest.fixture(scope="session") +def lib_dict_unittest(libs_dir: Path) -> dict[str, Library]: + lib_file = libs_dir / "lib_unittest.yml" + + with lib_file.open() as f: + input_lib = parse_yaml_library(f) + + return resolve_library([input_lib]) diff --git a/tests/e2e/functional/libs/lib_time_shift.yml b/tests/e2e/functional/libs/lib_time_shift.yml new file mode 100644 index 00000000..81fd1c41 --- /dev/null +++ b/tests/e2e/functional/libs/lib_time_shift.yml @@ -0,0 +1,124 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: time_shift_test + description: Library for component-dependent time shift functional tests + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + + - id: node + description: A basic balancing node + ports: + - id: injection_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(injection_port.flow) = 0 + + - id: demand + description: A demand model (supports time-varying and scenario-varying series) + parameters: + - id: demand + time-dependent: true + scenario-dependent: true + ports: + - id: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -demand + + - id: spillage + description: A spillage model + parameters: + - id: cost + time-dependent: false + scenario-dependent: false + variables: + - id: spillage + lower-bound: 0 + ports: + - id: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: -spillage + objective-contributions: + - id: obj + expression: expec(sum(cost * spillage)) + + - id: unsupplied + description: An unsupplied energy model + parameters: + - id: cost + time-dependent: false + scenario-dependent: false + variables: + - id: unsupplied_energy + lower-bound: 0 + ports: + - id: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: unsupplied_energy + objective-contributions: + - id: obj + expression: expec(sum(cost * unsupplied_energy)) + + - id: lagged-storage + description: > + Storage whose level equation references the state `lag` steps ago. + The `lag` parameter is constant per component, so different instances + of this model can carry different shift values, exercising the + per-component time-shift code path. + Level equation: level[t] = level[t-lag] - withdrawal[t] + inflows + Port flow: withdrawal (positive → supplies the network). + parameters: + - id: lag + time-dependent: false + scenario-dependent: false + - id: p_max + time-dependent: false + scenario-dependent: false + - id: level_max + time-dependent: false + scenario-dependent: false + - id: inflows + time-dependent: false + scenario-dependent: false + variables: + - id: level + lower-bound: 0 + upper-bound: level_max + - id: withdrawal + lower-bound: 0 + upper-bound: p_max + ports: + - id: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: withdrawal + constraints: + - id: level-equation + expression: level[t] - level[t-lag] + withdrawal = inflows \ No newline at end of file diff --git a/tests/e2e/functional/libs/standard.py b/tests/e2e/functional/libs/standard.py index a8f6cd7f..ca6a93a1 100644 --- a/tests/e2e/functional/libs/standard.py +++ b/tests/e2e/functional/libs/standard.py @@ -17,7 +17,6 @@ from gems.expression import literal, param, var from gems.expression.expression import port_field from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext from gems.model.constraint import Constraint from gems.model.model import ModelPort, model from gems.model.parameter import float_parameter, int_parameter @@ -58,12 +57,14 @@ == var("spillage") - var("unsupplied_energy"), ) ], - objective_operational_contribution=( - param("spillage_cost") * var("spillage") - + param("ens_cost") * var("unsupplied_energy") - ) - .time_sum() - .expec(), + objective_contributions={ + "operational": ( + param("spillage_cost") * var("spillage") + + param("ens_cost") * var("unsupplied_energy") + ) + .time_sum() + .expec() + }, ) """ @@ -130,13 +131,13 @@ name="Max generation", expression=var("generation") <= param("p_max") ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) GENERATOR_MODEL_WITH_PMIN = model( - id="GEN", + id="GEN_WITH_PMIN", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("p_min", CONSTANT), @@ -160,9 +161,9 @@ lower_bound=literal(0), ), # To test both ways of setting constraints ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) """ @@ -170,7 +171,7 @@ and total generation in whole period. It considers a full storage with no replenishing """ GENERATOR_MODEL_WITH_STORAGE = model( - id="GEN", + id="GEN_WITH_STORAGE", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", CONSTANT), @@ -193,14 +194,14 @@ expression=var("generation").time_sum() <= param("full_storage"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # For now, no starting cost THERMAL_CLUSTER_MODEL_HD = model( - id="GEN", + id="THERMAL_CLUSTER_HD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -265,14 +266,14 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # Same model as previous one, except that starting/stopping variables are now non anticipative THERMAL_CLUSTER_MODEL_DHD = model( - id="GEN", + id="THERMAL_CLUSTER_DHD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -337,9 +338,9 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) SPILLAGE_MODEL = model( @@ -353,9 +354,9 @@ definition=-var("spillage"), ) ], - objective_operational_contribution=(param("cost") * var("spillage")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("spillage")).time_sum().expec() + }, ) UNSUPPLIED_ENERGY_MODEL = model( @@ -369,9 +370,9 @@ definition=var("unsupplied_energy"), ) ], - objective_operational_contribution=(param("cost") * var("unsupplied_energy")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("unsupplied_energy")).time_sum().expec() + }, ) # Simplified model @@ -418,12 +419,12 @@ == param("inflows"), ), ], - objective_operational_contribution=literal(0), # Implcitement nul ? + objective_contributions={"operational": literal(0)}, # Implcitement nul ? ) """ Simple thermal unit that can be invested on""" THERMAL_CANDIDATE = model( - id="GEN", + id="THERMAL_CANDIDATE", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -436,7 +437,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -449,15 +449,15 @@ constraints=[ Constraint(name="Max generation", expression=var("generation") <= var("p_max")) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("p_max"), + }, ) """ Simple thermal unit that can be invested on and with already installed capacity""" THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA = model( - id="GEN", + id="THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -471,7 +471,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -488,8 +487,8 @@ <= param("already_installed_capa") + var("invested_capa"), ) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("invested_capa"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("invested_capa"), + }, ) diff --git a/tests/e2e/functional/perf_pypsa.py b/tests/e2e/functional/perf_pypsa.py new file mode 100644 index 00000000..e20934dd --- /dev/null +++ b/tests/e2e/functional/perf_pypsa.py @@ -0,0 +1,64 @@ +import time +from pathlib import Path +from typing import Tuple + +import numpy as np +import pandas as pd + +from gems.model.parsing import parse_yaml_library +from gems.model.resolve_library import resolve_library +from gems.simulation import TimeBlock, build_problem +from gems.study import Study +from gems.study.data import DataBase +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import ( + build_data_base, + consistency_check, + resolve_system, +) +from gems.study.system import System + + +def setup_data(pypsa_dir: Path) -> Tuple[System, DataBase]: + study_file = pypsa_dir / "input" / "system.yml" + lib_file = pypsa_dir / "input" / "model-libraries" / "pypsa_models.yml" + series_dir = pypsa_dir / "input" / "data-series" + with lib_file.open() as lib: + input_library = parse_yaml_library(lib) + + with study_file.open() as c: + input_study = parse_yaml_components(c) + lib_dict = resolve_library([input_library]) + system = resolve_system(input_study, lib_dict) + consistency_check(system, lib_dict["pypsa_models"].models) + + database = build_data_base(input_study, series_dir) + return system, database + + +def build_pypsa_problem(system: System, database: DataBase, time_horizon: int) -> float: + scenarios = 1 + time_block = TimeBlock(1, list(range(time_horizon))) + start = time.time() + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + end = time.time() + print(f"Time elapsed for horizon {time_horizon}: {end - start:.4f}") + return end - start + + +def run_pypsa_performance_scalability(pypsa_dir: Path) -> None: + system, database = setup_data(pypsa_dir) + durations = {} + + for horizon in np.linspace(1, 21, num=4): + durations[int(horizon)] = build_pypsa_problem(system, database, int(horizon)) + + duration_df = pd.DataFrame.from_dict(durations, orient="index") + duration_df.columns = pd.Index(["build time"]) + print(duration_df) + duration_df.to_csv(pypsa_dir / "pypsa_build_time_scalability.csv") + + +if __name__ == "__main__": + pypsa_dir = Path(__file__).parent / "data_pypsa" + run_pypsa_performance_scalability(pypsa_dir) diff --git a/tests/e2e/functional/series/demand_shift_test.txt b/tests/e2e/functional/series/demand_shift_test.txt new file mode 100644 index 00000000..b61c1f0e --- /dev/null +++ b/tests/e2e/functional/series/demand_shift_test.txt @@ -0,0 +1,4 @@ +20 +0 +20 +0 \ No newline at end of file diff --git a/tests/e2e/functional/series/demand_var_down_time.txt b/tests/e2e/functional/series/demand_var_down_time.txt new file mode 100644 index 00000000..2da1f51e --- /dev/null +++ b/tests/e2e/functional/series/demand_var_down_time.txt @@ -0,0 +1,10 @@ +100 +100 +100 +0 +0 +0 +100 +100 +100 +100 \ No newline at end of file diff --git a/tests/e2e/functional/series/scenariobuilder.dat b/tests/e2e/functional/series/scenariobuilder.dat new file mode 100644 index 00000000..5eefa376 --- /dev/null +++ b/tests/e2e/functional/series/scenariobuilder.dat @@ -0,0 +1,8 @@ +load, 0 = 1 +load, 1 = 2 +load, 2 = 1 +load, 3 = 2 +cost-group, 0 = 1 +cost-group, 1 = 1 +cost-group, 2 = 2 +cost-group, 3 = 2 diff --git a/tests/e2e/functional/studies/10_5/case_description.txt b/tests/e2e/functional/studies/10_5/case_description.txt new file mode 100644 index 00000000..40509d32 --- /dev/null +++ b/tests/e2e/functional/studies/10_5/case_description.txt @@ -0,0 +1,15 @@ +Test 10_5 : one time step + +One area (base_zone) + +One load : 90 MW + +Two generators, with max_power = 100 MW. One generator has got a marginal cost = 10, the other equals to 50. + + +Expected results: + +objective = 900 +base_zone.price = 10 +gas_base_zone.generation_reduced_cost = 0 +oil_base_zone.generation_reduced_cost = 40 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5/input/model-libraries/library-eo.yml b/tests/e2e/functional/studies/10_5/input/model-libraries/library-eo.yml new file mode 100644 index 00000000..7ea50986 --- /dev/null +++ b/tests/e2e/functional/studies/10_5/input/model-libraries/library-eo.yml @@ -0,0 +1,95 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: library-eo + description: Library for test cases on extra-outputs + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: sum(spillage_cost * spillage + ens_cost * unsupplied_energy) + extra-outputs: + - id: spill_cost_contribution + expression: spillage_cost * spillage + - id: ens_cost_contribution + expression: ens_cost * unsupplied_energy + - id: price + expression: dual(balance) + + - id: load + parameters: + - id: load_value + time-dependent: true + scenario-dependent: false + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load_value + + + - id: simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0.0 + upper-bound: p_max_cluster + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + + objective-contributions: + - id: operational_objective + expression: sum(marginal_generation_cost * generation) + extra-outputs: + - id: generation_reduced_cost + expression: reduced_cost(generation) + diff --git a/tests/e2e/functional/studies/10_5/input/optim-config.yml b/tests/e2e/functional/studies/10_5/input/optim-config.yml new file mode 100644 index 00000000..d6b6556c --- /dev/null +++ b/tests/e2e/functional/studies/10_5/input/optim-config.yml @@ -0,0 +1,11 @@ +time-scope: + first-time-step: 0 + last-time-step: 0 + +solver-options: + name: highs + logs: true + parameters: "THREADS 1" + +scenario-scope: + nb-scenarios: 1 diff --git a/tests/e2e/functional/studies/10_5/input/system.yml b/tests/e2e/functional/studies/10_5/input/system.yml new file mode 100644 index 00000000..db179d4d --- /dev/null +++ b/tests/e2e/functional/studies/10_5/input/system.yml @@ -0,0 +1,82 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + # Study case 10_5 : to test extra-output features + # Small test case with one node (1 thermal cluster, 1 one load) + +system: + + id: case_10_5 + components: + - id: base_zone + model: library-eo.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 10000 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 20000 + + + - id: load_base_zone + model: library-eo.load + parameters: + - id: load_value + time-dependent: false + scenario-dependent: false + value : 90 + + + - id: gas_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + value: 10 + + - id: oil_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + value: 50 + + + connections: + - component1: base_zone + port1: balance_port + component2: load_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: gas_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: oil_base_zone + port2: balance_port + diff --git a/tests/e2e/functional/studies/10_5_1/case_description.txt b/tests/e2e/functional/studies/10_5_1/case_description.txt new file mode 100644 index 00000000..dfea30f3 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_1/case_description.txt @@ -0,0 +1,15 @@ +Test 10_5 : one time step + +One area (base_zone) + +One load : 110 MW + +Two generators, with max_power = 100 MW. One generator has got a marginal cost = 10, the other equals to 50. + + +Expected results: + +objective = 1500 +base_zone.price = 50 +gas_base_zone.generation_reduced_cost = -40 +oil_base_zone.generation_reduced_cost = 0 diff --git a/tests/e2e/functional/studies/10_5_1/input/model-libraries/library-eo.yml b/tests/e2e/functional/studies/10_5_1/input/model-libraries/library-eo.yml new file mode 100644 index 00000000..7ea50986 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_1/input/model-libraries/library-eo.yml @@ -0,0 +1,95 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: library-eo + description: Library for test cases on extra-outputs + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: sum(spillage_cost * spillage + ens_cost * unsupplied_energy) + extra-outputs: + - id: spill_cost_contribution + expression: spillage_cost * spillage + - id: ens_cost_contribution + expression: ens_cost * unsupplied_energy + - id: price + expression: dual(balance) + + - id: load + parameters: + - id: load_value + time-dependent: true + scenario-dependent: false + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load_value + + + - id: simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0.0 + upper-bound: p_max_cluster + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + + objective-contributions: + - id: operational_objective + expression: sum(marginal_generation_cost * generation) + extra-outputs: + - id: generation_reduced_cost + expression: reduced_cost(generation) + diff --git a/tests/e2e/functional/studies/10_5_1/input/optim-config.yml b/tests/e2e/functional/studies/10_5_1/input/optim-config.yml new file mode 100644 index 00000000..d6b6556c --- /dev/null +++ b/tests/e2e/functional/studies/10_5_1/input/optim-config.yml @@ -0,0 +1,11 @@ +time-scope: + first-time-step: 0 + last-time-step: 0 + +solver-options: + name: highs + logs: true + parameters: "THREADS 1" + +scenario-scope: + nb-scenarios: 1 diff --git a/tests/e2e/functional/studies/10_5_1/input/system.yml b/tests/e2e/functional/studies/10_5_1/input/system.yml new file mode 100644 index 00000000..ff888815 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_1/input/system.yml @@ -0,0 +1,82 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + # Study case 10_5 : to test extra-output features + # Small test case with one node (1 thermal cluster, 1 one load) + +system: + + id: case_10_5 + components: + - id: base_zone + model: library-eo.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 10000 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 20000 + + + - id: load_base_zone + model: library-eo.load + parameters: + - id: load_value + time-dependent: false + scenario-dependent: false + value : 110 + + + - id: gas_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + value: 10 + + - id: oil_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: false + value: 50 + + + connections: + - component1: base_zone + port1: balance_port + component2: load_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: gas_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: oil_base_zone + port2: balance_port + diff --git a/tests/e2e/functional/studies/10_5_2/case_description.txt b/tests/e2e/functional/studies/10_5_2/case_description.txt new file mode 100644 index 00000000..e19a88e7 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/case_description.txt @@ -0,0 +1,32 @@ +Test 10_5_2 : three time steps + +One area (base_zone). WIth unsp_cost = 20000 + +One load : +- t= 1 : 80 MW +- t= 2 : 150 MW +- t= 3 : 201 MW + +One generator (gas_base_zone) has got max_power = 100 MW and following marginal cost +- t = 1 : 10 €/MWh, +- t = 2 : 15 €/MWh, +- t = 3 : 40 €/MWh, + +One generator (oil_base_zone) has got max_power = 100 MW and following marginal cost +- t = 1 : 30 €/MWh, +- t = 2 : 10 €/MWh, +- t = 3 : 10 €/MWh, + + +Expected results: + +objective = 27550 +base_zone.price_1 = 10 +base_zone.price_2 = 15 +base_zone.price_3 = 20000 +gas_base_zone.generation_reduced_cost_1 = 0 +gas_base_zone.generation_reduced_cost_2 = 0 +gas_base_zone.generation_reduced_cost_3 = - 19960 +oil_base_zone.generation_reduced_cost_1 = 20 +oil_base_zone.generation_reduced_cost_2 = - 5 +oil_base_zone.generation_reduced_cost_3 = - 19990 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/data-series/gas_cost_serie.tsv b/tests/e2e/functional/studies/10_5_2/input/data-series/gas_cost_serie.tsv new file mode 100644 index 00000000..0a9c618a --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/data-series/gas_cost_serie.tsv @@ -0,0 +1,3 @@ +10 +15 +40 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/data-series/load_serie.tsv b/tests/e2e/functional/studies/10_5_2/input/data-series/load_serie.tsv new file mode 100644 index 00000000..35759cd6 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/data-series/load_serie.tsv @@ -0,0 +1,3 @@ +80 +150 +201 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/data-series/load_ts.tsv b/tests/e2e/functional/studies/10_5_2/input/data-series/load_ts.tsv new file mode 100644 index 00000000..0179cf77 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/data-series/load_ts.tsv @@ -0,0 +1,5 @@ +20 +40 +60 +80 +110 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/data-series/oil_cost_serie.tsv b/tests/e2e/functional/studies/10_5_2/input/data-series/oil_cost_serie.tsv new file mode 100644 index 00000000..bafa962d --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/data-series/oil_cost_serie.tsv @@ -0,0 +1,3 @@ +30 +10 +10 \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/model-libraries/library-eo.yml b/tests/e2e/functional/studies/10_5_2/input/model-libraries/library-eo.yml new file mode 100644 index 00000000..4808c5d3 --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/model-libraries/library-eo.yml @@ -0,0 +1,97 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: library-eo + description: Library for test cases on extra-outputs + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: sum(spillage_cost * spillage + ens_cost * unsupplied_energy) + extra-outputs: + - id: spill_cost_contribution + expression: spillage_cost * spillage + - id: ens_cost_contribution + expression: ens_cost * unsupplied_energy + - id: price + expression: dual(balance) + + - id: load + parameters: + - id: load_value + time-dependent: true + scenario-dependent: false + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load_value + + + - id: simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: true + variables: + - id: generation + lower-bound: 0.0 + upper-bound: p_max_cluster + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + + objective-contributions: + - id: operational_objective + expression: sum(marginal_generation_cost * generation) + extra-outputs: + - id: generation_reduced_cost + expression: reduced_cost(generation) + + + \ No newline at end of file diff --git a/tests/e2e/functional/studies/10_5_2/input/optim-config.yml b/tests/e2e/functional/studies/10_5_2/input/optim-config.yml new file mode 100644 index 00000000..10f6408d --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/optim-config.yml @@ -0,0 +1,11 @@ +time-scope: + first-time-step: 0 + last-time-step: 2 + +solver-options: + name: highs + logs: true + parameters: "THREADS 1" + +scenario-scope: + nb-scenarios: 1 diff --git a/tests/e2e/functional/studies/10_5_2/input/system.yml b/tests/e2e/functional/studies/10_5_2/input/system.yml new file mode 100644 index 00000000..ce65a37f --- /dev/null +++ b/tests/e2e/functional/studies/10_5_2/input/system.yml @@ -0,0 +1,87 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study case 10_4 : to test extra-output features +# Small test case with one node (1 thermal cluster, 1 one load) + +system: + + id: case_10_5 + components: + - id: base_zone + model: library-eo.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 1000 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 20000 + + + - id: load_base_zone + model: library-eo.load + parameters: + - id: load_value + time-dependent: true + scenario-dependent: false + value: load_serie + + + - id: gas_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: true + value: gas_cost_serie + + - id: oil_base_zone + model: library-eo.simple_gen + parameters: + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 100 + - id: marginal_generation_cost + scenario-dependent: false + time-dependent: true + value: oil_cost_serie + + + connections: + - component1: base_zone + port1: balance_port + component2: load_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: gas_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: oil_base_zone + port2: balance_port + + + + + + \ No newline at end of file diff --git a/tests/e2e/functional/studies/13_1/expected_outputs/master.mps b/tests/e2e/functional/studies/13_1/expected_outputs/master.mps new file mode 100644 index 00000000..492adf6b --- /dev/null +++ b/tests/e2e/functional/studies/13_1/expected_outputs/master.mps @@ -0,0 +1,9 @@ +NAME +ROWS + N Obj +COLUMNS + x0 Obj 400 +RHS +BOUNDS + UP BOUND x0 1000 +ENDATA diff --git a/tests/e2e/functional/studies/13_1/expected_outputs/structure.txt b/tests/e2e/functional/studies/13_1/expected_outputs/structure.txt new file mode 100644 index 00000000..bdaa608f --- /dev/null +++ b/tests/e2e/functional/studies/13_1/expected_outputs/structure.txt @@ -0,0 +1,2 @@ + master continuous_generator_candidate 0 + subproblem continuous_generator_candidate 3 diff --git a/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps b/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps new file mode 100644 index 00000000..48698416 --- /dev/null +++ b/tests/e2e/functional/studies/13_1/expected_outputs/subproblem.mps @@ -0,0 +1,33 @@ +NAME +ROWS + N Obj + G c0 + L c1 + L c2 + L c3 +COLUMNS + x0 Obj 1 + x0 c0 -1 + x0 c1 -1 + x1 Obj 501 + x1 c0 1 + x1 c1 1 + x2 Obj 45 + x2 c0 1 + x2 c1 1 + x2 c2 1 + x3 c3 -1 + x4 Obj 10 + x4 c0 1 + x4 c1 1 + x4 c3 1 +RHS + RHS_V c0 400 + RHS_V c1 400 + RHS_V c2 200 +BOUNDS + UP BOUND x0 1000000 + UP BOUND x1 1000000 + UP BOUND x2 200 + UP BOUND x3 1000 +ENDATA diff --git a/tests/e2e/functional/studies/13_1/input/model-libraries/lib_example_13_1.yml b/tests/e2e/functional/studies/13_1/input/model-libraries/lib_example_13_1.yml new file mode 100644 index 00000000..a47120e4 --- /dev/null +++ b/tests/e2e/functional/studies/13_1/input/model-libraries/lib_example_13_1.yml @@ -0,0 +1,118 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: lib_example_13_1 + description: Library for test cases with investment + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: expec(sum(spillage_cost * spillage + ens_cost * unsupplied_energy)) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + - id: cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0 + upper-bound: p_max + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: expec(sum(cost * generation)) + + - id: generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 1000 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: expec(sum(op_cost * generation)) + - id: invest_objective + expression: invest_cost * p_max diff --git a/tests/e2e/functional/studies/13_1/input/optim-config.yml b/tests/e2e/functional/studies/13_1/input/optim-config.yml new file mode 100644 index 00000000..47312721 --- /dev/null +++ b/tests/e2e/functional/studies/13_1/input/optim-config.yml @@ -0,0 +1,36 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 0 + +solver-options: + name: highs + logs: true + parameters: "THREADS 1" + +scenario-scope: + nb-scenarios: 1 + +models: + - id: lib_example_13_1.generator_with_continuous_invest + model-decomposition: + variables: + - id: p_max + location: master-and-subproblems + + # Variable generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems diff --git a/tests/e2e/functional/studies/13_1/input/system.yml b/tests/e2e/functional/studies/13_1/input/system.yml new file mode 100644 index 00000000..400b8012 --- /dev/null +++ b/tests/e2e/functional/studies/13_1/input/system.yml @@ -0,0 +1,93 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study case 13_1 : to test investment problems +# Simple generation expansion problem on one node, one timestep and one scenario with one candidate. + +# Demand = 400 +# Generator : P_max : 200, Cost : 45 +# Unsupplied energy : Cost : 501 + +# -> 200 of unsupplied energy +# -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + +# Continuous candidate : Invest cost : 400 / MW; Prod cost : 10 + +# Optimal investment : 200 MW + +# -> Optimal cost = 400 * 200 + 10 * 200 (Invest cost + prod cost of new generator) +# + 45 * 200 (Generator) +# = 80_000 + 11_000 +# = 91_000 + +system: + id: case_13_1 + components: + - id: my_node + model: lib_example_13_1.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 1 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 501 + + - id: load + model: lib_example_13_1.load + parameters: + - id: load + time-dependent: false + scenario-dependent: false + value: 400 + + - id: already_installed_generator + model: lib_example_13_1.generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + value: 200 + - id: cost + scenario-dependent: false + time-dependent: false + value: 45 + + - id: continuous_generator_candidate + model: lib_example_13_1.generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 400 + + connections: + - component1: my_node + port1: balance_port + component2: load + port2: balance_port + + - component1: my_node + port1: balance_port + component2: already_installed_generator + port2: balance_port + + - component1: my_node + port1: balance_port + component2: continuous_generator_candidate + port2: balance_port diff --git a/tests/e2e/functional/studies/13_2/expected_outputs/master.mps b/tests/e2e/functional/studies/13_2/expected_outputs/master.mps new file mode 100644 index 00000000..697a9563 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/expected_outputs/master.mps @@ -0,0 +1,19 @@ +NAME +ROWS + N Obj + G c0 + L c1 +COLUMNS + x0 Obj 490 + x1 Obj 200 + x1 c0 1 + x1 c1 1 + MARK0000 'MARKER' 'INTORG' + x2 c0 -10 + x2 c1 -10 + MARK0001 'MARKER' 'INTEND' +RHS +BOUNDS + UP BOUND x0 1000 + UI BOUND x2 10 +ENDATA diff --git a/tests/e2e/functional/studies/13_2/expected_outputs/structure.txt b/tests/e2e/functional/studies/13_2/expected_outputs/structure.txt new file mode 100644 index 00000000..b8f64fd1 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/expected_outputs/structure.txt @@ -0,0 +1,4 @@ + master discrete_generator_candidate 1 + subproblem discrete_generator_candidate 5 + master continuous_generator_candidate 0 + subproblem continuous_generator_candidate 3 diff --git a/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps b/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps new file mode 100644 index 00000000..7d5ce1f2 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/expected_outputs/subproblem.mps @@ -0,0 +1,39 @@ +NAME +ROWS + N Obj + G c0 + L c1 + L c2 + L c3 + L c4 +COLUMNS + x0 Obj 1 + x0 c0 -1 + x0 c1 -1 + x1 Obj 501 + x1 c0 1 + x1 c1 1 + x2 Obj 45 + x2 c0 1 + x2 c1 1 + x2 c2 1 + x3 c3 -1 + x4 Obj 10 + x4 c0 1 + x4 c1 1 + x4 c3 1 + x5 c4 -1 + x6 Obj 10 + x6 c0 1 + x6 c1 1 + x6 c4 1 +RHS + RHS_V c0 400 + RHS_V c1 400 + RHS_V c2 200 +BOUNDS + UP BOUND x0 1000000 + UP BOUND x1 1000000 + UP BOUND x2 200 + UP BOUND x3 1000 +ENDATA diff --git a/tests/e2e/functional/studies/13_2/input/model-libraries/lib_example_13_2.yml b/tests/e2e/functional/studies/13_2/input/model-libraries/lib_example_13_2.yml new file mode 100644 index 00000000..5133c4e0 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/input/model-libraries/lib_example_13_2.yml @@ -0,0 +1,163 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: lib_example_13_2 + description: Library for test cases with investment + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + upper-bound: 1000000 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: operational_objective + expression: expec(sum(spillage_cost * spillage + ens_cost * unsupplied_energy)) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + - id: cost + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: 0 + upper-bound: p_max + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: expec(sum(cost * generation)) + + - id: generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 1000 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + constraints: + - id: max_generation + expression: generation <= p_max + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: expec(sum(op_cost * generation)) + - id: invest_objective + expression: invest_cost * p_max + + - id: generator_with_discrete_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + - id: invest_cost + scenario-dependent: false + time-dependent: false + - id: p_max_per_unit + scenario-dependent: false + time-dependent: false + variables: + - id: p_max + scenario-dependent: false + time-dependent: false + lower-bound: 0 + variable-type: continuous + - id: generation + lower-bound: 0 + variable-type: continuous + - id: nb_units + scenario-dependent: false + time-dependent: false + lower-bound: 0 + upper-bound: 10 + variable-type: integer + constraints: + - id: max_generation + expression: generation <= p_max + - id: p_max_nb_units_relation + expression: p_max = p_max_per_unit * nb_units + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: operational_objective + expression: expec(sum(op_cost * generation)) + - id: invest_objective + expression: invest_cost * p_max + diff --git a/tests/e2e/functional/studies/13_2/input/optim-config.yml b/tests/e2e/functional/studies/13_2/input/optim-config.yml new file mode 100644 index 00000000..78711de9 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/input/optim-config.yml @@ -0,0 +1,53 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 0 + +solver-options: + name: highs + logs: true + parameters: "THREADS 1" + +scenario-scope: + nb-scenarios: 1 + +models: + - id: lib_example_13_2.generator_with_discrete_invest + model-decomposition: + variables: + - id: nb_units + location: master + - id: p_max + location: master-and-subproblems + # Variable generation has default settings, no need to make it appear here + constraints: + - id: p_max_nb_units_relation + location: master + # Constraint max_generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems + + - id: lib_example_13_2.generator_with_continuous_invest + model-decomposition: + variables: + - id: p_max + location: master-and-subproblems + # Variable generation has default settings, no need to make it appear here + objective-contributions: + - id: invest_objective + location: master + - id: operational_objective + location: subproblems \ No newline at end of file diff --git a/tests/e2e/functional/studies/13_2/input/system.yml b/tests/e2e/functional/studies/13_2/input/system.yml new file mode 100644 index 00000000..46447365 --- /dev/null +++ b/tests/e2e/functional/studies/13_2/input/system.yml @@ -0,0 +1,116 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study case 13_2 : to test investment problems +# Simple generation expansion problem on one node, one timestep and one scenario with two candidates: one continuous and one discrete. + +# Demand = 400 +# Generator : P_max : 200, Cost : 45 +# Unsupplied energy : Cost : 501 + +# -> 200 of unsupplied energy +# -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 + +# Continuous candidate : Invest cost : 490 / MW; Prod cost : 10 +# Discrete candidate : Invest cost : 200 / MW; Prod cost : 10; Nb of units: 10; Prod per unit: 10 + +# Optimal investment : 100 MW (Discrete) + 100 MW (Continuous) + +# -> Optimal cost = 490 * 100 + 10 * 100 (Continuous) +# + 200 * 100 + 10 * 100 (Discrete) +# + 45 * 200 (Generator) +# = 69_000 + 11_000 +# = 80_000 + +system: + id: case_13_2 + components: + - id: my_node + model: lib_example_13_2.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 1 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 501 + + - id: load + model: lib_example_13_2.load + parameters: + - id: load + time-dependent: false + scenario-dependent: false + value: 400 + + - id: already_installed_generator + model: lib_example_13_2.generator + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + value: 200 + - id: cost + scenario-dependent: false + time-dependent: false + value: 45 + + - id: continuous_generator_candidate + model: lib_example_13_2.generator_with_continuous_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 490 + + - id: discrete_generator_candidate + model: lib_example_13_2.generator_with_discrete_invest + parameters: + - id: op_cost + scenario-dependent: false + time-dependent: false + value: 10 + - id: invest_cost + scenario-dependent: false + time-dependent: false + value: 200 + - id: p_max_per_unit + scenario-dependent: false + time-dependent: false + value: 10 + + connections: + - component1: my_node + port1: balance_port + component2: load + port2: balance_port + + - component1: my_node + port1: balance_port + component2: already_installed_generator + port2: balance_port + + - component1: my_node + port1: balance_port + component2: continuous_generator_candidate + port2: balance_port + + - component1: my_node + port1: balance_port + component2: discrete_generator_candidate + port2: balance_port diff --git a/tests/e2e/functional/studies/7_4/input/data-series/load_ts_base028.tsv b/tests/e2e/functional/studies/7_4/input/data-series/load_ts_base028.tsv new file mode 100644 index 00000000..7ef4fb93 --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/data-series/load_ts_base028.tsv @@ -0,0 +1,168 @@ +5993 +5794 +5753 +6020 +6397 +7095 +7326 +7383 +7407 +7450 +7426 +7392 +7273 +7122 +7142 +7519 +7804 +7724 +7356 +6884 +6910 +6790 +6772 +6705 +6642 +6431 +6373 +6594 +6914 +7571 +7746 +7717 +7673 +7653 +7536 +7467 +7326 +7150 +7140 +7522 +7830 +7742 +7381 +6961 +6982 +6859 +6845 +6785 +6731 +6526 +6474 +6701 +7024 +7684 +7871 +7849 +7811 +7794 +7680 +7609 +7462 +7284 +7270 +7652 +7958 +7871 +7509 +7091 +7111 +6985 +6975 +6917 +6869 +6666 +6618 +6844 +7166 +7820 +8005 +7982 +7936 +7907 +7782 +7705 +7550 +7365 +7348 +7724 +8030 +7934 +7574 +7159 +7177 +7049 +7036 +6976 +6926 +6718 +6663 +6885 +7201 +7851 +8029 +8003 +7954 +7922 +7794 +7715 +7560 +7369 +7362 +7746 +8061 +7950 +7598 +7188 +7212 +7088 +6953 +6874 +6774 +6619 +6505 +6554 +6696 +6924 +7181 +7164 +7147 +7101 +7130 +6972 +6920 +6760 +6742 +7032 +7211 +7201 +6953 +6865 +7005 +6968 +6725 +6688 +6581 +6412 +6264 +6243 +6270 +6365 +6487 +6762 +6824 +6859 +6910 +6752 +6421 +6170 +6267 +6562 +6851 +6958 +6924 +6738 +6738 +6673 +6519 +6512 \ No newline at end of file diff --git a/tests/e2e/functional/studies/7_4/input/data-series/wind_ts_base028.tsv b/tests/e2e/functional/studies/7_4/input/data-series/wind_ts_base028.tsv new file mode 100644 index 00000000..24ef59c2 --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/data-series/wind_ts_base028.tsv @@ -0,0 +1,168 @@ +3273 +4863 +5509 +5261 +5867 +6057 +7583 +8078 +8349 +8958 +8660 +8536 +8207 +7838 +8405 +8270 +7826 +7801 +7833 +6334 +5560 +4464 +3903 +5482 +5246 +4601 +3813 +2460 +2843 +2803 +2608 +2300 +1807 +2056 +761 +1056 +111 +190 +204 +36 +144 +389 +393 +1584 +2497 +3201 +4484 +3423 +2348 +2908 +5144 +3287 +2918 +4109 +2093 +2850 +956 +175 +369 +1366 +1890 +3004 +3304 +3561 +2764 +1764 +2328 +4052 +3289 +2712 +3395 +4050 +4717 +5311 +3655 +2512 +3198 +5405 +4675 +4315 +1703 +4740 +5201 +3245 +6341 +6810 +7471 +7994 +8085 +8675 +8783 +8267 +8789 +6580 +6623 +5199 +5052 +5579 +3251 +6750 +7011 +5411 +4790 +4337 +4094 +2927 +2583 +483 +667 +1259 +4686 +4163 +5588 +4359 +4312 +3237 +4417 +4112 +5024 +4439 +4815 +4814 +5529 +4738 +4394 +5249 +5722 +6602 +6199 +6897 +6883 +6704 +5995 +6840 +6823 +6826 +7649 +7701 +8579 +8740 +9000 +9000 +8473 +9000 +8665 +7520 +7966 +8123 +7392 +7039 +7417 +7252 +5970 +5460 +5050 +6769 +6053 +7832 +8285 +7883 +8270 +7419 +6876 +8080 +7434 +8022 +8730 +9000 \ No newline at end of file diff --git a/tests/e2e/functional/studies/7_4/input/model-libraries/andromede_v1_models.yml b/tests/e2e/functional/studies/7_4/input/model-libraries/andromede_v1_models.yml new file mode 100644 index 00000000..9512d5ce --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/model-libraries/andromede_v1_models.yml @@ -0,0 +1,72 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: andromede-v1-models-weo + description: Andromede V1 model library - without expectation operators + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + models: + - id: dsr + parameters: + - id: max_load + time-dependent: true + scenario-dependent: true + - id: curtailment_price + time-dependent: false + scenario-dependent: false + variables: + - id: curtailment + lower-bound: 0 + upper-bound: max_load + variable-type: continuous + objective-contributions: + - id: objective + expression: expec(sum(curtailment * curtailment_price)) + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -max_load + curtailment + + + - id: electrolyser + parameters: + - id: efficiency + scenario-dependent: false + time-dependent: false + - id: p_max + scenario-dependent: true + time-dependent: true + variables: + - id: power + lower-bound: 0 + upper-bound: p_max + variable-type: continuous + ports: + - id: power_port + type: flow + - id: hydrogen_port + type: flow + port-field-definitions: + - port: power_port + field: flow + definition: -power + - port: hydrogen_port + field: flow + definition: efficiency * power diff --git a/tests/e2e/functional/studies/7_4/input/model-libraries/antares_historic.yml b/tests/e2e/functional/studies/7_4/input/model-libraries/antares_historic.yml new file mode 100644 index 00000000..e9c6aad2 --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/model-libraries/antares_historic.yml @@ -0,0 +1,243 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: antares-historic-weo + description: Antares historic model library - without expectation operators + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: objective + expression: expec(sum(spillage_cost * spillage + ens_cost * unsupplied_energy)) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: link + parameters: + - id: capacity_direct + time-dependent: true + scenario-dependent: true + - id: capacity_indirect + time-dependent: true + scenario-dependent: true + variables: + - id: flow_direct + lower-bound: 0 + upper-bound: capacity_direct + variable-type: continuous + - id: flow_indirect + lower-bound: 0 + upper-bound: capacity_indirect + variable-type: continuous + - id: flow + lower-bound: -capacity_indirect + upper-bound: capacity_indirect + variable-type: continuous + ports: + - id: out_port + type: flow + - id: in_port + type: flow + port-field-definitions: + - port: out_port + field: flow + definition: flow + - port: in_port + field: flow + definition: -flow + constraints: + - id: flow_direct_indirect + expression: flow = flow_direct - flow_indirect + + - id: renewable + parameters: + - id: nominal_capacity + - id: unit_count + - id: generation + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + + - id: thermal + parameters: + - id: p_min_cluster + scenario-dependent: true + time-dependent: true + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: true + time-dependent: true + - id: p_min_unit + - id: p_max_unit + - id: generation_cost + - id: startup_cost + - id: fixed_cost + - id: d_min_up + - id: d_min_down + - id: nb_units_min # Equals to ceil(p_min_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: true + time-dependent: true + - id: nb_units_max # Equals to ceil(p_max_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: true + time-dependent: true + - id: nb_units_max_variation_forward + scenario-dependent: true + time-dependent: true + - id: nb_units_max_variation_backward + scenario-dependent: true + time-dependent: true + variables: + - id: generation + lower-bound: p_min_cluster + upper-bound: p_max_cluster + variable-type: continuous + - id: nb_units_on + lower-bound: nb_units_min + upper-bound: nb_units_max + variable-type: integer + - id: nb_starting + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + - id: nb_stopping + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + - id: nb_failing + lower-bound: 0 + upper-bound: nb_units_max + variable-type: integer + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + constraints: + - id: max_generation + expression: generation <= nb_units_on * p_max_unit + - id: min_generation + expression: generation >= nb_units_on * p_min_unit + - id: on_units_dynamics + expression: nb_units_on = nb_units_on[t-1] + nb_starting - nb_stopping + - id: nb_failing_lower_than_stopping + expression: nb_failing <= nb_stopping + - id: nb_failing_lower_than_max_variation + expression: nb_failing <= nb_units_max_variation_forward + - id: min_up_duration + expression: sum(t-d_min_up + 1 .. t, nb_starting - nb_failing) <= nb_units_on + - id: min_down_duration + expression: sum(t-d_min_down + 1 .. t, nb_stopping) <= nb_units_max[t-d_min_down] - nb_units_on + sum(t-d_min_down + 1 .. t, nb_units_max_variation_backward) + objective-contributions: + - id: objective + expression: expec(sum(generation_cost * generation + startup_cost * nb_starting + fixed_cost * nb_units_on)) + + - id: short-term-storage + parameters: + - id: reservoir_capacity + time-dependent: false + scenario-dependent: false + - id: injection_nominal_capacity + time-dependent: false + scenario-dependent: false + - id: withdrawal_nominal_capacity + time-dependent: false + scenario-dependent: false + - id: efficiency_injection + time-dependent: false + scenario-dependent: false + - id: efficiency_withdrawal + time-dependent: false + scenario-dependent: false + - id: lower_rule_curve + time-dependent: true + scenario-dependent: true + - id: upper_rule_curve + time-dependent: true + scenario-dependent: true + - id: p_max_injection_modulation # Read in p_max_injection + time-dependent: true + scenario-dependent: true + - id: p_max_withdrawal_modulation # Read in p_max_withdrawal + time-dependent: true + scenario-dependent: true + - id: inflows + time-dependent: true + scenario-dependent: true + - id: initial_level + time-dependent: false + scenario-dependent: true + variables: + - id: p_injection + lower-bound: 0 + upper-bound: p_max_injection_modulation * injection_nominal_capacity # p_max_injection_modulation is a timeseries with adimensional values between 0 and 1 + variable-type: continuous + - id: p_withdrawal + lower-bound: 0 + upper-bound: p_max_withdrawal_modulation * withdrawal_nominal_capacity # p_max_withdrawal_modulation is a timeseries with adimensional values between 0 and 1 + variable-type: continuous + - id: level + lower-bound: lower_rule_curve * reservoir_capacity + upper-bound: upper_rule_curve * reservoir_capacity + variable-type: continuous + ports: + - id: injection_port + type: flow + port-field-definitions: + - port: injection_port + field: flow + definition: p_withdrawal - p_injection + constraints: + - id: initial_level_constraint + expression: level[0] = initial_level * reservoir_capacity + - id: Level equation + expression: level[t+1] = level + efficiency_injection * p_injection - efficiency_withdrawal * p_withdrawal + inflows diff --git a/tests/e2e/functional/studies/7_4/input/optim-config.yml b/tests/e2e/functional/studies/7_4/input/optim-config.yml new file mode 100644 index 00000000..3df4189d --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/optim-config.yml @@ -0,0 +1,11 @@ +time-scope: + first-time-step: 0 + last-time-step: 167 + +solver-options: + name: highs + logs: false + parameters: "" + +scenario-scope: + nb-scenarios: 1 diff --git a/tests/e2e/functional/studies/7_4/input/system.yml b/tests/e2e/functional/studies/7_4/input/system.yml new file mode 100644 index 00000000..238a566e --- /dev/null +++ b/tests/e2e/functional/studies/7_4/input/system.yml @@ -0,0 +1,457 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study 028b B-D-E - V8.6 +# Small test case with two nodes +#### 1 one electric node : 3 thermal clusters, 1 load, 1 wind, 1 short term storage, 1 demand-side response +#### 1 H2 node : 1 electrolyser, 1 H2 load, 1 H2 back-up production + +system: + id: system + components: + + ############################# H2 zone ############################# + + - id: hydrogen_zone + model: antares-historic-weo.area + + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 0 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 1000 + + - id: hydrogen_backup + + model: antares-historic-weo.thermal + parameters: + - id: p_min_cluster + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 200 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 200 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 61 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_min # Equals to ceil(p_min_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max # Equals to ceil(p_max_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_max_variation_forward + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_max_variation_backward + scenario-dependent: false + time-dependent: false + value: 1 + + - id: electrolyser + + model: andromede-v1-models-weo.electrolyser + parameters: + - id: p_max + scenario-dependent: false + time-dependent: false + value: 300 + - id: efficiency + scenario-dependent: false + time-dependent: false + value: 0.7 + + - id: hydrogen_load + model: antares-historic-weo.load + + parameters: + - id: load + time-dependent: false + scenario-dependent: false + value: 220 + ############################# Electricity zone ############################# + - id: base_zone + model: antares-historic-weo.area + + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 0 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 20000 + + - id: load_base_zone + model: antares-historic-weo.load + + parameters: + - id: load + time-dependent: true + scenario-dependent: true + value: load_ts_base028 + + - id: dsr_base_zone + + model: andromede-v1-models-weo.dsr + parameters: + + - id: max_load + time-dependent: false + scenario-dependent: false + value: 300 + - id: curtailment_price + time-dependent: false + scenario-dependent: false + value: 42.7 + + - id: wind_base_zone + model: antares-historic-weo.renewable + + parameters: + - id: nominal_capacity + time-dependent: false + scenario-dependent: false + value: 9000 + - id: unit_count + time-dependent: false + scenario-dependent: false + value: 1 + - id: generation + time-dependent: true + scenario-dependent: true + value: wind_ts_base028 + + - id: gas_base_zone + + model: antares-historic-weo.thermal + parameters: + - id: p_min_cluster + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 2500 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 500 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 50 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_min # Equals to ceil(p_min_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max # Equals to ceil(p_max_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 5 + - id: nb_units_max_variation_forward + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max_variation_backward + scenario-dependent: false + time-dependent: false + value: 0 + + - id: oil_base_zone + + model: antares-historic-weo.thermal + parameters: + - id: p_min_cluster + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 1000 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 200 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 80 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_min # Equals to ceil(p_min_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max # Equals to ceil(p_max_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 5 + - id: nb_units_max_variation_forward + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max_variation_backward + scenario-dependent: false + time-dependent: false + value: 0 + + - id: storage_base_zone + + model: antares-historic-weo.short-term-storage + parameters: + - id: reservoir_capacity + time-dependent: false + scenario-dependent: false + value: 1200 + - id: injection_nominal_capacity + time-dependent: false + scenario-dependent: false + value: 300 + - id: withdrawal_nominal_capacity + time-dependent: false + scenario-dependent: false + value: 300 + - id: efficiency_injection + time-dependent: false + scenario-dependent: false + value: 0.9 + - id: efficiency_withdrawal + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: lower_rule_curve + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: upper_rule_curve + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: p_max_injection_modulation # Read in p_max_injection + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: p_max_withdrawal_modulation # Read in p_max_withdrawal + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: inflows + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: initial_level + time-dependent: false + scenario-dependent: false + value: 0.5 + + - id: coal_base_zone + + model: antares-historic-weo.thermal + parameters: + - id: p_min_cluster + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: false + time-dependent: false + value: 6000 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 1000 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 30 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1 + - id: nb_units_min # Equals to ceil(p_min_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max # Equals to ceil(p_max_cluster/p_max_unit), to be done in preprocessing + scenario-dependent: false + time-dependent: false + value: 6 + - id: nb_units_max_variation_forward + scenario-dependent: false + time-dependent: false + value: 0 + - id: nb_units_max_variation_backward + scenario-dependent: false + time-dependent: false + value: 0 + + + connections: + + ############################# H2 zone ############################# + + - component1: hydrogen_zone + port1: balance_port + component2: hydrogen_load + port2: balance_port + + - component1: hydrogen_zone + port1: balance_port + component2: hydrogen_backup + port2: balance_port + + - component1: hydrogen_zone + port1: balance_port + component2: electrolyser + port2: hydrogen_port + + ############################# Electricity zone ############################# + - component1: base_zone + port1: balance_port + component2: load_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: wind_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: gas_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: oil_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: coal_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: dsr_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: storage_base_zone + port2: injection_port + + + + + ############################# H2-Electricity connection ############################# + + - component1: base_zone + port1: balance_port + component2: electrolyser + port2: power_port + + + + diff --git a/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/load_ts_base028.txt b/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/load_ts_base028.txt new file mode 100644 index 00000000..45d7be9c --- /dev/null +++ b/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/load_ts_base028.txt @@ -0,0 +1,504 @@ +5993 +5794 +5753 +6020 +6397 +7095 +7326 +7383 +7407 +7450 +7426 +7392 +7273 +7122 +7142 +7519 +7804 +7724 +7356 +6884 +6910 +6790 +6772 +6705 +6642 +6431 +6373 +6594 +6914 +7571 +7746 +7717 +7673 +7653 +7536 +7467 +7326 +7150 +7140 +7522 +7830 +7742 +7381 +6961 +6982 +6859 +6845 +6785 +6731 +6526 +6474 +6701 +7024 +7684 +7871 +7849 +7811 +7794 +7680 +7609 +7462 +7284 +7270 +7652 +7958 +7871 +7509 +7091 +7111 +6985 +6975 +6917 +6869 +6666 +6618 +6844 +7166 +7820 +8005 +7982 +7936 +7907 +7782 +7705 +7550 +7365 +7348 +7724 +8030 +7934 +7574 +7159 +7177 +7049 +7036 +6976 +6926 +6718 +6663 +6885 +7201 +7851 +8029 +8003 +7954 +7922 +7794 +7715 +7560 +7369 +7362 +7746 +8061 +7950 +7598 +7188 +7212 +7088 +6953 +6874 +6774 +6619 +6505 +6554 +6696 +6924 +7181 +7164 +7147 +7101 +7130 +6972 +6920 +6760 +6742 +7032 +7211 +7201 +6953 +6865 +7005 +6968 +6725 +6688 +6581 +6412 +6264 +6243 +6270 +6365 +6487 +6762 +6824 +6859 +6910 +6752 +6421 +6170 +6267 +6562 +6851 +6958 +6924 +6738 +6738 +6673 +6519 +6512 +6472 +6278 +6242 +6516 +6894 +7568 +7782 +7804 +7774 +7755 +7679 +7590 +7409 +7190 +7192 +7558 +7841 +7711 +7379 +6934 +6965 +6841 +6825 +6760 +6700 +6486 +6428 +6650 +6972 +7630 +7809 +7786 +7741 +7714 +7591 +7513 +7362 +7173 +7143 +7503 +7790 +7650 +7331 +6937 +6948 +6814 +6791 +6721 +6651 +6430 +6361 +6575 +6888 +7547 +7731 +7720 +7691 +7685 +7578 +7519 +7386 +7219 +7192 +7555 +7841 +7721 +7393 +6989 +7000 +6870 +6848 +6779 +6711 +6489 +6418 +6631 +6939 +7592 +7769 +7752 +7717 +7705 +7595 +7531 +7396 +7225 +7200 +7564 +7852 +7749 +7412 +7000 +7019 +6890 +6872 +6805 +6741 +6519 +6450 +6661 +6971 +7625 +7803 +7790 +7763 +7756 +7653 +7596 +7465 +7301 +7275 +7639 +7928 +7827 +7481 +7065 +7081 +6953 +6807 +6710 +6584 +6402 +6259 +6282 +6402 +6621 +6854 +6827 +6815 +6793 +6851 +6716 +6693 +6562 +6534 +6813 +6972 +6954 +6711 +6621 +6758 +6725 +6477 +6431 +6306 +6124 +5964 +5931 +5952 +6059 +6187 +6486 +6585 +6669 +6763 +6650 +6366 +6168 +6259 +6546 +6818 +6935 +6893 +6693 +6684 +6613 +6455 +6438 +6383 +6173 +6119 +6377 +6739 +7421 +7646 +7701 +7724 +7765 +7742 +7709 +7588 +7436 +7441 +7804 +8083 +7983 +7618 +7147 +7170 +7044 +7026 +6959 +6895 +6673 +6598 +6807 +7111 +7752 +7916 +7878 +7831 +7811 +7698 +7630 +7488 +7312 +7294 +7670 +7974 +7863 +7510 +7097 +7119 +6996 +6986 +6929 +6876 +6670 +6615 +6839 +7159 +7811 +7995 +7973 +7931 +7909 +7789 +7711 +7558 +7371 +7338 +7698 +7988 +7886 +7536 +7119 +7136 +7005 +6984 +6917 +6849 +6625 +6551 +6756 +7058 +7701 +7871 +7841 +7801 +7789 +7683 +7624 +7491 +7326 +7300 +7665 +7957 +7823 +7478 +7071 +7087 +6958 +6941 +6879 +6818 +6601 +6537 +6750 +7061 +7714 +7892 +7874 +7837 +7821 +7710 +7645 +7506 +7335 +7311 +7681 +7975 +7859 +7515 +7103 +7121 +6995 +6854 +6763 +6646 +6472 +6342 +6375 +6502 +6724 +6961 +6930 +6911 +6874 +6917 +6766 +6725 +6575 +6553 +6834 +6999 +6976 +6735 +6647 +6787 +6759 +6513 +6468 +6347 +6170 +6017 +5990 +6016 +6125 +6255 +6553 +6646 +6720 +6807 +6683 +6392 +6181 +6269 +6556 +6828 +6937 +6896 +6700 +6691 +6622 +6462 +6443 diff --git a/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/wind_ts_base028.txt b/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/wind_ts_base028.txt new file mode 100644 index 00000000..d34836c0 --- /dev/null +++ b/tests/e2e/functional/studies/dsr_3_blocks/input/data-series/wind_ts_base028.txt @@ -0,0 +1,504 @@ +3273 +4863 +5509 +5261 +5867 +6057 +7583 +8078 +8349 +8958 +8660 +8536 +8207 +7838 +8405 +8270 +7826 +7801 +7833 +6334 +5560 +4464 +3903 +5482 +5246 +4601 +3813 +2460 +2843 +2803 +2608 +2300 +1807 +2056 +761 +1056 +111 +190 +204 +36 +144 +389 +393 +1584 +2497 +3201 +4484 +3423 +2348 +2908 +5144 +3287 +2918 +4109 +2093 +2850 +956 +175 +369 +1366 +1890 +3004 +3304 +3561 +2764 +1764 +2328 +4052 +3289 +2712 +3395 +4050 +4717 +5311 +3655 +2512 +3198 +5405 +4675 +4315 +1703 +4740 +5201 +3245 +6341 +6810 +7471 +7994 +8085 +8675 +8783 +8267 +8789 +6580 +6623 +5199 +5052 +5579 +3251 +6750 +7011 +5411 +4790 +4337 +4094 +2927 +2583 +483 +667 +1259 +4686 +4163 +5588 +4359 +4312 +3237 +4417 +4112 +5024 +4439 +4815 +4814 +5529 +4738 +4394 +5249 +5722 +6602 +6199 +6897 +6883 +6704 +5995 +6840 +6823 +6826 +7649 +7701 +8579 +8740 +9000 +9000 +8473 +9000 +8665 +7520 +7966 +8123 +7392 +7039 +7417 +7252 +5970 +5460 +5050 +6769 +6053 +7832 +8285 +7883 +8270 +7419 +6876 +8080 +7434 +8022 +8730 +9000 +8420 +7819 +8180 +8039 +7726 +8338 +8445 +8783 +8650 +8639 +8527 +9000 +8861 +8516 +7966 +7155 +6307 +6750 +6441 +7704 +8829 +8702 +8376 +7298 +6620 +6568 +6143 +6907 +7007 +8504 +8056 +6493 +7090 +6063 +5421 +5314 +6516 +6519 +7267 +8114 +7662 +8698 +8164 +8113 +9000 +8815 +7987 +8373 +7538 +6730 +5484 +5741 +5511 +5026 +5511 +6799 +6801 +5590 +4827 +4458 +5348 +6725 +5776 +4760 +6320 +5476 +6299 +7079 +7255 +6062 +6807 +5394 +4569 +4190 +6297 +5063 +6448 +6294 +6929 +7025 +7790 +5535 +8158 +6644 +7421 +7681 +6119 +5403 +6976 +7957 +8833 +8832 +9000 +8965 +8995 +8706 +7232 +7007 +5765 +5379 +4416 +3004 +2575 +3090 +4861 +2608 +4337 +4445 +1920 +1246 +2373 +3225 +1547 +1722 +1212 +553 +610 +210 +404 +35 +378 +945 +1108 +945 +2295 +3421 +1253 +477 +565 +276 +779 +3097 +3845 +4470 +5920 +3915 +4576 +4970 +5284 +3507 +4222 +1159 +566 +492 +2005 +3884 +3344 +2556 +5109 +5854 +6154 +8254 +7654 +7045 +7017 +7640 +7347 +7659 +6859 +5482 +5524 +4116 +3904 +3442 +3389 +3791 +2586 +1815 +26 +28 +488 +11 +446 +1168 +1107 +403 +143 +347 +106 +53 +959 +1293 +1827 +3212 +4430 +4390 +4998 +6147 +7992 +6988 +5810 +3515 +4866 +5316 +6868 +5021 +3716 +3682 +3729 +3394 +4154 +3569 +3075 +4530 +4703 +5140 +5869 +4881 +4695 +5010 +4461 +3117 +1870 +3561 +3133 +2318 +1451 +676 +570 +1149 +1823 +3447 +3376 +1285 +1699 +1157 +1163 +623 +439 +1720 +1446 +557 +0 +404 +797 +1247 +796 +975 +1183 +553 +1310 +1512 +977 +2290 +4177 +4985 +4519 +4242 +4846 +4909 +5722 +5607 +5702 +7518 +8398 +8369 +8592 +8615 +6498 +6716 +6004 +4440 +4675 +2391 +2030 +1551 +1117 +1536 +2212 +3286 +3127 +3495 +3623 +1903 +1106 +99 +54 +0 +311 +183 +629 +2891 +4132 +4587 +6891 +7573 +7002 +7133 +6918 +7485 +7608 +6317 +6292 +5224 +5078 +6601 +5286 +3849 +2779 +3161 +2230 +2562 +3249 +3939 +3283 +1957 +3729 +5180 +4949 +4164 +3626 +3103 +3758 +3704 +4529 +4924 +5117 +4789 +4952 +5763 +6728 +6782 +6767 +7505 +7054 +7226 +6360 +5868 +2899 +3267 +4300 +3554 +6042 +6542 +7661 +8353 diff --git a/tests/e2e/functional/studies/dsr_3_blocks/input/model-libraries/test_lib.yml b/tests/e2e/functional/studies/dsr_3_blocks/input/model-libraries/test_lib.yml new file mode 100644 index 00000000..1c9ccaad --- /dev/null +++ b/tests/e2e/functional/studies/dsr_3_blocks/input/model-libraries/test_lib.yml @@ -0,0 +1,122 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: test-lib + description: Minimal model library for the dsr_3_blocks e2e test + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + - id: unsupplied_energy + lower-bound: 0 + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: obj + expression: expec(sum(spillage_cost * spillage + ens_cost * unsupplied_energy)) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + - id: renewable + parameters: + - id: nominal_capacity + - id: unit_count + - id: generation + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + + - id: dsr + parameters: + - id: max_load + time-dependent: true + scenario-dependent: true + - id: curtailment_price + time-dependent: false + scenario-dependent: false + variables: + - id: curtailment + lower-bound: 0 + upper-bound: max_load + objective-contributions: + - id: obj + expression: expec(sum(curtailment * curtailment_price)) + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -max_load + curtailment + + - id: simple_generator + parameters: + - id: p_min + scenario-dependent: true + time-dependent: true + - id: p_max + scenario-dependent: true + time-dependent: true + - id: generation_cost + scenario-dependent: false + time-dependent: false + - id: co2_emission_factor + scenario-dependent: false + time-dependent: false + variables: + - id: generation + lower-bound: p_min + upper-bound: p_max + variable-type: continuous + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + objective-contributions: + - id: objective + expression: expec(sum(generation_cost * generation)) diff --git a/tests/e2e/functional/studies/dsr_3_blocks/input/system.yml b/tests/e2e/functional/studies/dsr_3_blocks/input/system.yml new file mode 100644 index 00000000..eb7225a1 --- /dev/null +++ b/tests/e2e/functional/studies/dsr_3_blocks/input/system.yml @@ -0,0 +1,165 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# LP-only variant of the andromede_v1 DSR study (base028). +# +# Generators (gas, oil, coal) use the simple_generator LP model instead of +# the antares-historic thermal MIP model. MIP thermal clusters with zero +# startup/fixed costs have multiple equivalent integer solutions across block +# boundaries, making per-timestep comparison between resolution modes +# unreliable. The simple_generator model is a pure continuous LP, so the +# unique optimal dispatch is determined entirely by merit order at each +# timestep. Frontal, parallel-subproblems, and sequential-subproblems modes +# must therefore produce identical per-timestep results, which is exactly +# what this test verifies. + +system: + + components: + - id: base_zone + model: test-lib.area + parameters: + - id: spillage_cost + time-dependent: false + scenario-dependent: false + value: 0 + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 20000 + + - id: load_base_zone + model: test-lib.load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + value: load_ts_base028 + + - id: wind_base_zone + model: test-lib.renewable + parameters: + - id: nominal_capacity + time-dependent: false + scenario-dependent: false + value: 9000 + - id: unit_count + time-dependent: false + scenario-dependent: false + value: 1 + - id: generation + time-dependent: true + scenario-dependent: true + value: wind_ts_base028 + + - id: dsr_base_zone + model: test-lib.dsr + parameters: + - id: max_load + time-dependent: false + scenario-dependent: false + value: 300 + - id: curtailment_price + time-dependent: false + scenario-dependent: false + value: 42.7 + + - id: gas_base_zone + model: test-lib.simple_generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 2500 + - id: generation_cost + time-dependent: false + scenario-dependent: false + value: 50 + - id: co2_emission_factor + time-dependent: false + scenario-dependent: false + value: 0 + + - id: oil_base_zone + model: test-lib.simple_generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 1000 + - id: generation_cost + time-dependent: false + scenario-dependent: false + value: 80 + - id: co2_emission_factor + time-dependent: false + scenario-dependent: false + value: 0 + + - id: coal_base_zone + model: test-lib.simple_generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 6000 + - id: generation_cost + time-dependent: false + scenario-dependent: false + value: 30 + - id: co2_emission_factor + time-dependent: false + scenario-dependent: false + value: 0 + + connections: + - component1: base_zone + port1: balance_port + component2: load_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: wind_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: dsr_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: gas_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: oil_base_zone + port2: balance_port + + - component1: base_zone + port1: balance_port + component2: coal_base_zone + port2: balance_port diff --git a/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/data-series/demand.txt b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/data-series/demand.txt new file mode 100644 index 00000000..b47162ad --- /dev/null +++ b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/data-series/demand.txt @@ -0,0 +1,6 @@ +0 +4 +0 +4 +0 +4 diff --git a/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/model-libraries/rolling_horizon_lib.yml b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/model-libraries/rolling_horizon_lib.yml new file mode 100644 index 00000000..9626e2c1 --- /dev/null +++ b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/model-libraries/rolling_horizon_lib.yml @@ -0,0 +1,121 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Minimal model library for the rolling_horizon_suboptimality e2e test. +# +# System: 1 generator (capacity-capped, constant cost) + 1 load (oscillating) +# + 1 storage (drop-mode SoC balance) + 1 bus (power balance + unserved). +# +# The bus carries an `unsupplied` variable (energy not served) penalised at a +# large cost. This allows the sequential mode to remain feasible when the +# storage is empty at a peak-demand step while still incurring a high objective +# penalty compared with the frontal mode. +# +# The storage soc_balance constraint uses soc[t-1] and must be declared with +# mode: drop in the optim-config so that the carry-over equality constraint +# (injected in Phase 5 of build_problem) provides the initial state for each +# sequential block. + +library: + id: rolling-horizon-lib + description: Minimal 4-model library for rolling-horizon suboptimality test + + port-types: + - id: flow + description: Single-field power flow port + fields: + - id: flow + + models: + + - id: generator + parameters: + - id: gen_cost + time-dependent: false + scenario-dependent: false + - id: p_max + time-dependent: false + scenario-dependent: false + variables: + - id: p + lower-bound: 0 + upper-bound: p_max + ports: + - id: balance + type: flow + port-field-definitions: + - port: balance + field: flow + definition: p + objective-contributions: + - id: generation_cost + expression: expec(sum(gen_cost * p)) + + - id: load + parameters: + - id: demand + time-dependent: true + scenario-dependent: false + ports: + - id: balance + type: flow + port-field-definitions: + - port: balance + field: flow + definition: -demand + + - id: storage + parameters: + - id: capacity + time-dependent: false + scenario-dependent: false + - id: max_rate + time-dependent: false + scenario-dependent: false + variables: + - id: charge + lower-bound: 0 + upper-bound: max_rate + - id: discharge + lower-bound: 0 + upper-bound: max_rate + - id: soc + lower-bound: 0 + upper-bound: capacity + ports: + - id: balance + type: flow + port-field-definitions: + - port: balance + field: flow + definition: discharge - charge + constraints: + - id: soc_balance + expression: soc = soc[t-1] + charge - discharge + + - id: bus + parameters: + - id: ens_cost + time-dependent: false + scenario-dependent: false + variables: + - id: unsupplied + lower-bound: 0 + ports: + - id: balance + type: flow + binding-constraints: + - id: flow_balance + expression: sum_connections(balance.flow) + unsupplied = 0 + objective-contributions: + - id: ens_penalty + expression: expec(sum(ens_cost * unsupplied)) diff --git a/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/system.yml b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/system.yml new file mode 100644 index 00000000..cb401149 --- /dev/null +++ b/tests/e2e/functional/studies/rolling_horizon_suboptimality/input/system.yml @@ -0,0 +1,78 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Minimal 4-component system for the rolling-horizon suboptimality test. +# +# Time series (6 steps, t=0..5): +# demand = [0, 4, 0, 4, 0, 4] (oscillating; peaks at odd steps) +# +# Parameters: +# generator: p_max=2 (cannot cover peak demand=4 alone), gen_cost=1 +# bus: ens_cost=100 (penalty for unserved energy) +# storage: capacity=2, max_rate=2, η=1 + +system: + components: + + - id: gen + model: rolling-horizon-lib.generator + parameters: + - id: gen_cost + time-dependent: false + scenario-dependent: false + value: 1 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 2 + + - id: load_node + model: rolling-horizon-lib.load + parameters: + - id: demand + time-dependent: true + scenario-dependent: false + value: demand + + - id: storage + model: rolling-horizon-lib.storage + parameters: + - id: capacity + time-dependent: false + scenario-dependent: false + value: 2 + - id: max_rate + time-dependent: false + scenario-dependent: false + value: 2 + + - id: bus + model: rolling-horizon-lib.bus + parameters: + - id: ens_cost + time-dependent: false + scenario-dependent: false + value: 100 + + connections: + - component1: bus + port1: balance + component2: gen + port2: balance + - component1: bus + port1: balance + component2: load_node + port2: balance + - component1: bus + port1: balance + component2: storage + port2: balance diff --git a/tests/e2e/functional/studies/simple_system_cyclic/input/model-libraries/simple_models.yml b/tests/e2e/functional/studies/simple_system_cyclic/input/model-libraries/simple_models.yml new file mode 100644 index 00000000..f3450fb3 --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_cyclic/input/model-libraries/simple_models.yml @@ -0,0 +1,254 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + + +library: + id: simple_models + description: Simple model library + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + - id: emission + description: A port which accounts for CO2 emissions + fields: + - id: emission + + models: + - id: bus + + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + - id: x + time-dependent: false + scenario-dependent: false + - id: y + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_set + time-dependent: true + scenario-dependent: false + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + binding-constraints: + - id: p_balance + expression: sum_connections(p_balance_port.flow) = 0 + - id: q_balance + expression: sum_connections(q_balance_port.flow) = 0 + + - id: load + + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + - id: q_set + time-dependent: true + scenario-dependent: true + - id: sign + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + port-field-definitions: + - port: p_balance_port + field: flow + definition: sign * p_set + - port: q_balance_port + field: flow + definition: sign * q_set + + - id: generator + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: e_sum_min + time-dependent: false + scenario-dependent: true + - id: e_sum_max + time-dependent: false + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: efficiency + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + + variables: + - id: p_nom + lower-bound: p_nom_min + upper-bound: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p + #- id: q + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p*sign + - port: emission_port + field: emission + definition: p*emission_factor/efficiency + constraints: + - id: min_dispatch + expression: p >=p_nom * p_min_pu + - id: max_dispatch + expression: p <=p_nom * p_max_pu + - id: min_production + expression: sum(p) >= e_sum_min + - id: max_production + expression: sum(p) <= e_sum_max + objective-contributions: + - id: capital_objective + expression: p_nom * capital_cost + - id: operational_objective + expression: expec(sum(marginal_cost * p)) + + + + - id: storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: spill_cost #Parameter not instantiated for now + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: marginal_cost_storage + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: max_hours + time-dependent: false + scenario-dependent: false + - id: efficiency_store + time-dependent: true + scenario-dependent: true + - id: efficiency_dispatch + time-dependent: true + scenario-dependent: true + - id: standing_loss + time-dependent: true + scenario-dependent: true + - id: inflow + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + variables: + - id: p_nom + time-dependent: false + scenario-dependent: false + lower-bound: p_nom_min + upper-bound: p_nom_max + - id: p_store + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: p_dispatch + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: state_of_charge + lower-bound: 0 + upper-bound: max_hours * p_nom_max + - id: spill + lower-bound: 0 + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p_dispatch - p_store + - port: emission_port + field: emission + definition: emission_factor * 0 #Since we assume here cyclity of StorageUnits. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor + constraints: + - id: p_store_upper + expression: p_store <= p_max_pu * p_nom + - id: p_dispatch_upper + expression: p_dispatch <= p_max_pu * p_nom + - id: state_of_charge_upper + expression: state_of_charge <= max_hours * p_nom + - id: state_of_charge_balance + expression: state_of_charge = (1- standing_loss) * state_of_charge[t-1] + efficiency_store * p_store - p_dispatch / efficiency_dispatch + inflow - spill + objective-contributions: + - id: capital_objective + expression: capital_cost * p_nom + - id: operational_objective + expression: expec(sum(marginal_cost * p_dispatch + spill_cost*spill + marginal_cost_storage * state_of_charge)) + + \ No newline at end of file diff --git a/tests/e2e/functional/studies/simple_system_cyclic/input/optim-config.yml b/tests/e2e/functional/studies/simple_system_cyclic/input/optim-config.yml new file mode 100644 index 00000000..2660d4fb --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_cyclic/input/optim-config.yml @@ -0,0 +1,43 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 2 + +models: + - id: simple_models.generator + model-decomposition: + variables: + - id: p_nom + location: master-and-subproblems + objective-contributions: + - id: capital_objective + location: master + - id: operational_objective + location: subproblems + + + + - id: simple_models.storage_unit + model-decomposition: + variables: + - id: p_nom + location: master-and-subproblems + objective-contributions: + - id: capital_objective + location: master + - id: operational_objective + location: subproblems + out-of-bounds-processing: + constraints: + - id: state_of_charge_balance + mode: cyclic diff --git a/tests/e2e/functional/studies/simple_system_cyclic/input/system.yml b/tests/e2e/functional/studies/simple_system_cyclic/input/system.yml new file mode 100644 index 00000000..bda416d7 --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_cyclic/input/system.yml @@ -0,0 +1,170 @@ +system: + id: simple_battery + model_libraries: simple_models + components: + - id: generator_generator + model: simple_models.generator + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: p_nom_max + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: p_min_pu + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: p_max_pu + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: capital_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: e_sum_min + time-dependent: false + scenario-dependent: false + value: -1.0e+20 + - id: e_sum_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + - id: sign + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: emission_factor + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: load_load + model: simple_models.load + parameters: + - id: p_set + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: q_set + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: -1.0 + - id: bus + model: simple_models.bus + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: x + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: y + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_set + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + - id: storage_unit_battery + model: simple_models.storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + value: 40.0 + - id: p_nom_max + time-dependent: false + scenario-dependent: false + value: 40.0 + - id: p_min_pu + time-dependent: false + scenario-dependent: false + value: -1.0 + - id: p_max_pu + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency_store + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency_dispatch + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: standing_loss + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: max_hours + time-dependent: false + scenario-dependent: false + value: 5.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: capital_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: marginal_cost_storage + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: spill_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: inflow + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: emission_factor + time-dependent: false + scenario-dependent: false + value: 0.0 + connections: + - component1: bus + port1: p_balance_port + component2: generator_generator + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: load_load + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: storage_unit_battery + port2: p_balance_port + diff --git a/tests/e2e/functional/studies/simple_system_drop/input/model-libraries/simple_models.yml b/tests/e2e/functional/studies/simple_system_drop/input/model-libraries/simple_models.yml new file mode 100644 index 00000000..3bf2a445 --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_drop/input/model-libraries/simple_models.yml @@ -0,0 +1,254 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + + + +library: + id: simple_models + description: Simple model library + + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + - id: emission + description: A port which accounts for CO2 emissions + fields: + - id: emission + + models: + - id: bus + + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + - id: x + time-dependent: false + scenario-dependent: false + - id: y + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_set + time-dependent: true + scenario-dependent: false + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + binding-constraints: + - id: p_balance + expression: sum_connections(p_balance_port.flow) = 0 + - id: q_balance + expression: sum_connections(q_balance_port.flow) = 0 + + - id: load + + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + - id: q_set + time-dependent: true + scenario-dependent: true + - id: sign + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + port-field-definitions: + - port: p_balance_port + field: flow + definition: sign * p_set + - port: q_balance_port + field: flow + definition: sign * q_set + + - id: generator + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: e_sum_min + time-dependent: false + scenario-dependent: true + - id: e_sum_max + time-dependent: false + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: efficiency + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + + variables: + - id: p_nom + lower-bound: p_nom_min + upper-bound: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p + #- id: q + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p*sign + - port: emission_port + field: emission + definition: p*emission_factor/efficiency + constraints: + - id: min_dispatch + expression: p >=p_nom * p_min_pu + - id: max_dispatch + expression: p <=p_nom * p_max_pu + - id: min_production + expression: sum(p) >= e_sum_min + - id: max_production + expression: sum(p) <= e_sum_max + objective-contributions: + - id: capital_objective + expression: p_nom * capital_cost + - id: operational_objective + expression: expec(sum(marginal_cost * p)) + + + + - id: storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: spill_cost #Parameter not instantiated for now + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: marginal_cost_storage + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: max_hours + time-dependent: false + scenario-dependent: false + - id: efficiency_store + time-dependent: true + scenario-dependent: true + - id: efficiency_dispatch + time-dependent: true + scenario-dependent: true + - id: standing_loss + time-dependent: true + scenario-dependent: true + - id: inflow + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + variables: + - id: p_nom + time-dependent: false + scenario-dependent: false + lower-bound: p_nom_min + upper-bound: p_nom_max + - id: p_store + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: p_dispatch + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: state_of_charge + lower-bound: 0 + upper-bound: max_hours * p_nom_max + - id: spill + lower-bound: 0 + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p_dispatch - p_store + - port: emission_port + field: emission + definition: emission_factor * 0 #Since we assume here cyclity of StorageUnits. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor + constraints: + - id: p_store_upper + expression: p_store <= p_max_pu * p_nom + - id: p_dispatch_upper + expression: p_dispatch <= p_max_pu * p_nom + - id: state_of_charge_upper + expression: state_of_charge <= max_hours * p_nom + - id: state_of_charge_balance + expression: state_of_charge = (1- standing_loss) * state_of_charge[t-1] + efficiency_store * p_store - p_dispatch / efficiency_dispatch + inflow - spill + objective-contributions: + - id: capital_objective + expression: capital_cost * p_nom + - id: operational_objective + expression: expec(sum(marginal_cost * p_dispatch + spill_cost*spill + marginal_cost_storage * state_of_charge)) + + \ No newline at end of file diff --git a/tests/e2e/functional/studies/simple_system_drop/input/optim-config.yml b/tests/e2e/functional/studies/simple_system_drop/input/optim-config.yml new file mode 100644 index 00000000..add13ea6 --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_drop/input/optim-config.yml @@ -0,0 +1,43 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 2 + +models: + - id: simple_models.generator + model-decomposition: + variables: + - id: p_nom + location: master-and-subproblems + objective-contributions: + - id: capital_objective + location: master + - id: operational_objective + location: subproblems + + + + - id: simple_models.storage_unit + model-decomposition: + variables: + - id: p_nom + location: master-and-subproblems + objective-contributions: + - id: capital_objective + location: master + - id: operational_objective + location: subproblems + out-of-bounds-processing: + constraints: + - id: state_of_charge_balance + mode: drop diff --git a/tests/e2e/functional/studies/simple_system_drop/input/system.yml b/tests/e2e/functional/studies/simple_system_drop/input/system.yml new file mode 100644 index 00000000..bda416d7 --- /dev/null +++ b/tests/e2e/functional/studies/simple_system_drop/input/system.yml @@ -0,0 +1,170 @@ +system: + id: simple_battery + model_libraries: simple_models + components: + - id: generator_generator + model: simple_models.generator + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: p_nom_max + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: p_min_pu + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: p_max_pu + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: capital_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: e_sum_min + time-dependent: false + scenario-dependent: false + value: -1.0e+20 + - id: e_sum_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + - id: sign + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: emission_factor + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: load_load + model: simple_models.load + parameters: + - id: p_set + time-dependent: false + scenario-dependent: false + value: 50.0 + - id: q_set + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: -1.0 + - id: bus + model: simple_models.bus + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: x + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: y + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_set + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + - id: storage_unit_battery + model: simple_models.storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + value: 40.0 + - id: p_nom_max + time-dependent: false + scenario-dependent: false + value: 40.0 + - id: p_min_pu + time-dependent: false + scenario-dependent: false + value: -1.0 + - id: p_max_pu + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency_store + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: efficiency_dispatch + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: standing_loss + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: max_hours + time-dependent: false + scenario-dependent: false + value: 5.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: capital_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: marginal_cost_storage + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: spill_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: inflow + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: emission_factor + time-dependent: false + scenario-dependent: false + value: 0.0 + connections: + - component1: bus + port1: p_balance_port + component2: generator_generator + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: load_load + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: storage_unit_battery + port2: p_balance_port + diff --git a/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/data-series/load.tsv b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/data-series/load.tsv new file mode 100644 index 00000000..e83cfaec --- /dev/null +++ b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/data-series/load.tsv @@ -0,0 +1,3 @@ +10 +10 +100 \ No newline at end of file diff --git a/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/model-libraries/simple_models.yml b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/model-libraries/simple_models.yml new file mode 100644 index 00000000..28620ebd --- /dev/null +++ b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/model-libraries/simple_models.yml @@ -0,0 +1,238 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: simple_models + description: Simple model library + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + - id: emission + description: A port which accounts for CO2 emissions + fields: + - id: emission + + models: + - id: bus + + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + - id: x + time-dependent: false + scenario-dependent: false + - id: y + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_set + time-dependent: true + scenario-dependent: false + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + binding-constraints: + - id: p_balance + expression: sum_connections(p_balance_port.flow) = 0 + - id: q_balance + expression: sum_connections(q_balance_port.flow) = 0 + + - id: load + + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + - id: q_set + time-dependent: true + scenario-dependent: true + - id: sign + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + port-field-definitions: + - port: p_balance_port + field: flow + definition: sign * p_set + - port: q_balance_port + field: flow + definition: sign * q_set + + - id: generator + parameters: + - id: p_min + time-dependent: true + scenario-dependent: true + - id: p_max + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: startup_cost + time-dependent: false + scenario-dependent: false + - id: d_min_up + time-dependent: false + scenario-dependent: false + - id: d_min_down + time-dependent: false + scenario-dependent: false + + variables: + - id: p + - id: start + lower-bound: 0 + upper-bound: 1 + variable-type: integer + - id: stop + lower-bound: 0 + upper-bound: 1 + variable-type: integer + - id: is_on + lower-bound: 0 + upper-bound: 1 + variable-type: integer + #- id: q + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p + constraints: + - id: min_dispatch + expression: p >=p_min * is_on + - id: max_dispatch + expression: p <=p_max * is_on + - id: is_on_dynamics + expression: is_on = is_on[t-1] + start - stop + - id: min_up_duration + expression: sum(t-d_min_up + 1 .. t, start) <= is_on + - id: min_down_duration + expression: sum(t-d_min_down + 1 .. t, stop) <= 1 - is_on + objective-contributions: + - id: operational_objective + expression: expec(sum(marginal_cost * p + startup_cost * start)) + + - id: storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: spill_cost #Parameter not instantiated for now + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: marginal_cost_storage + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: max_hours + time-dependent: false + scenario-dependent: false + - id: efficiency_store + time-dependent: true + scenario-dependent: true + - id: efficiency_dispatch + time-dependent: true + scenario-dependent: true + - id: standing_loss + time-dependent: true + scenario-dependent: true + - id: inflow + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + variables: + - id: p_nom + time-dependent: false + scenario-dependent: false + lower-bound: p_nom_min + upper-bound: p_nom_max + - id: p_store + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: p_dispatch + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: state_of_charge + lower-bound: 0 + upper-bound: max_hours * p_nom_max + - id: spill + lower-bound: 0 + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p_dispatch - p_store + - port: emission_port + field: emission + definition: emission_factor * 0 #Since we assume here cyclity of StorageUnits. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor + constraints: + - id: p_store_upper + expression: p_store <= p_max_pu * p_nom + - id: p_dispatch_upper + expression: p_dispatch <= p_max_pu * p_nom + - id: state_of_charge_upper + expression: state_of_charge <= max_hours * p_nom + - id: state_of_charge_balance + expression: state_of_charge = (1- standing_loss) * state_of_charge[t-1] + + efficiency_store * p_store - p_dispatch / efficiency_dispatch + + inflow - spill + objective-contributions: + - id: capital_objective + expression: capital_cost * p_nom + - id: operational_objective + expression: expec(sum(marginal_cost * p_dispatch + spill_cost*spill + + marginal_cost_storage * state_of_charge)) diff --git a/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/optim-config.yml b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/optim-config.yml new file mode 100644 index 00000000..aaa078ed --- /dev/null +++ b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/optim-config.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 2 + +models: + + - id: simple_models.generator + out-of-bounds-processing: + constraints: + - id: is_on_dynamics + mode: cyclic + - id: min_up_duration + mode: cyclic + - id: min_down_duration + mode: cyclic diff --git a/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/system.yml b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/system.yml new file mode 100644 index 00000000..249f89ff --- /dev/null +++ b/tests/e2e/functional/studies/system_cyclic_with_param_in_shift/input/system.yml @@ -0,0 +1,159 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study: system_cyclic_with_param_in_shift +# +# 3 time steps, 1 bus, 1 load [10, 10, 100], 2 generators: +# - gen_1: expensive (marginal_cost=100, startup_cost=500), p_min=10, p_max=90, +# d_min_up=2, d_min_down=1 +# - gen_2: cheap (marginal_cost=10, no startup cost), p_min=0, p_max=10, +# d_min_up=1, d_min_down=1 +# +# The optim-config enables "cyclic" out-of-bounds processing for +# is_on_dynamics, min_up_duration, and min_down_duration on the generator +# model. Out-of-bounds references wrap around to the other end of the block +# instead of being dropped: +# - is_on_dynamics at t=0: is_on[0] = is_on[2] + start[0] - stop[0] +# - min_up_duration at t=0 (d_min_up=2): start[2] + start[0] <= is_on[0] +# - min_down_duration at t=0 (d_min_down=1): range [0,0], always in-bounds +# +# Optimal solution: +# gen_1 is off at t=0, starts at t=1, and runs at t=1 and t=2. +# The cyclic is_on_dynamics at t=0 is satisfied with stop[0]=1 (gen_1 "stops" +# at t=0, balancing the carry-over from is_on[2]=1). gen_2 covers the load at +# t=0. gen_1 runs at its minimum (10) at t=1 and its maximum (90) at t=2; +# gen_2 covers the remaining 10 at t=2. +# +# gen_2 at t=0: marginal_cost * p = 10*10 = 100 +# gen_1 at t=1: marginal_cost * p = 100*10 = 1000 +# gen_1 at t=2: marginal_cost * p = 100*90 = 9000 +# gen_2 at t=2: marginal_cost * p = 10*10 = 100 +# gen_1 startup = 500 +# Total optimal cost = 10700 + +system: + id: system_cyclic_with_param_in_shift + model_libraries: simple_models + components: + - id: bus + model: simple_models.bus + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: x + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: y + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_set + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + + - id: load + model: simple_models.load + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + value: load + - id: q_set + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: -1.0 + + - id: gen_1 + model: simple_models.generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 90.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 100.0 + - id: startup_cost + time-dependent: false + scenario-dependent: false + value: 500.0 + - id: d_min_up + time-dependent: false + scenario-dependent: false + value: 2 + - id: d_min_down + time-dependent: false + scenario-dependent: false + value: 1 + + - id: gen_2 + model: simple_models.generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: startup_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: d_min_up + time-dependent: false + scenario-dependent: false + value: 1 + - id: d_min_down + time-dependent: false + scenario-dependent: false + value: 1 + + connections: + - component1: bus + port1: p_balance_port + component2: load + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: gen_1 + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: gen_2 + port2: p_balance_port diff --git a/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/data-series/load.tsv b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/data-series/load.tsv new file mode 100644 index 00000000..e83cfaec --- /dev/null +++ b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/data-series/load.tsv @@ -0,0 +1,3 @@ +10 +10 +100 \ No newline at end of file diff --git a/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/model-libraries/simple_models.yml b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/model-libraries/simple_models.yml new file mode 100644 index 00000000..28620ebd --- /dev/null +++ b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/model-libraries/simple_models.yml @@ -0,0 +1,238 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +library: + id: simple_models + description: Simple model library + + port-types: + - id: flow + description: A port which transfers power flow + fields: + - id: flow + - id: emission + description: A port which accounts for CO2 emissions + fields: + - id: emission + + models: + - id: bus + + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + - id: x + time-dependent: false + scenario-dependent: false + - id: y + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_set + time-dependent: true + scenario-dependent: false + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + binding-constraints: + - id: p_balance + expression: sum_connections(p_balance_port.flow) = 0 + - id: q_balance + expression: sum_connections(q_balance_port.flow) = 0 + + - id: load + + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + - id: q_set + time-dependent: true + scenario-dependent: true + - id: sign + time-dependent: false + scenario-dependent: false + ports: + - id: p_balance_port + type: flow + - id: q_balance_port + type: flow + port-field-definitions: + - port: p_balance_port + field: flow + definition: sign * p_set + - port: q_balance_port + field: flow + definition: sign * q_set + + - id: generator + parameters: + - id: p_min + time-dependent: true + scenario-dependent: true + - id: p_max + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: startup_cost + time-dependent: false + scenario-dependent: false + - id: d_min_up + time-dependent: false + scenario-dependent: false + - id: d_min_down + time-dependent: false + scenario-dependent: false + + variables: + - id: p + - id: start + lower-bound: 0 + upper-bound: 1 + variable-type: integer + - id: stop + lower-bound: 0 + upper-bound: 1 + variable-type: integer + - id: is_on + lower-bound: 0 + upper-bound: 1 + variable-type: integer + #- id: q + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p + constraints: + - id: min_dispatch + expression: p >=p_min * is_on + - id: max_dispatch + expression: p <=p_max * is_on + - id: is_on_dynamics + expression: is_on = is_on[t-1] + start - stop + - id: min_up_duration + expression: sum(t-d_min_up + 1 .. t, start) <= is_on + - id: min_down_duration + expression: sum(t-d_min_down + 1 .. t, stop) <= 1 - is_on + objective-contributions: + - id: operational_objective + expression: expec(sum(marginal_cost * p + startup_cost * start)) + + - id: storage_unit + parameters: + - id: p_nom_min + time-dependent: false + scenario-dependent: false + - id: p_nom_max + time-dependent: false + scenario-dependent: false + - id: p_min_pu + time-dependent: true + scenario-dependent: true + - id: p_max_pu + time-dependent: true + scenario-dependent: true + - id: sign #default value = 1 + time-dependent: false + scenario-dependent: false + #- id: carrier # This parameter is not used in the Gems model + - id: spill_cost #Parameter not instantiated for now + time-dependent: true + scenario-dependent: true + - id: marginal_cost + time-dependent: true + scenario-dependent: true + - id: marginal_cost_storage + time-dependent: true + scenario-dependent: true + - id: capital_cost + time-dependent: false + scenario-dependent: false + - id: max_hours + time-dependent: false + scenario-dependent: false + - id: efficiency_store + time-dependent: true + scenario-dependent: true + - id: efficiency_dispatch + time-dependent: true + scenario-dependent: true + - id: standing_loss + time-dependent: true + scenario-dependent: true + - id: inflow + time-dependent: true + scenario-dependent: true + - id: emission_factor + time-dependent: false + scenario-dependent: false + variables: + - id: p_nom + time-dependent: false + scenario-dependent: false + lower-bound: p_nom_min + upper-bound: p_nom_max + - id: p_store + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: p_dispatch + lower-bound: 0 + upper-bound: p_max_pu * p_nom_max + - id: state_of_charge + lower-bound: 0 + upper-bound: max_hours * p_nom_max + - id: spill + lower-bound: 0 + ports: + - id: p_balance_port + type: flow + - id: emission_port + type: emission + port-field-definitions: + - port: p_balance_port + field: flow + definition: p_dispatch - p_store + - port: emission_port + field: emission + definition: emission_factor * 0 #Since we assume here cyclity of StorageUnits. In the future, for non-cyclic Store: (e[-1] - e[T-1])*emission_factor + constraints: + - id: p_store_upper + expression: p_store <= p_max_pu * p_nom + - id: p_dispatch_upper + expression: p_dispatch <= p_max_pu * p_nom + - id: state_of_charge_upper + expression: state_of_charge <= max_hours * p_nom + - id: state_of_charge_balance + expression: state_of_charge = (1- standing_loss) * state_of_charge[t-1] + + efficiency_store * p_store - p_dispatch / efficiency_dispatch + + inflow - spill + objective-contributions: + - id: capital_objective + expression: capital_cost * p_nom + - id: operational_objective + expression: expec(sum(marginal_cost * p_dispatch + spill_cost*spill + + marginal_cost_storage * state_of_charge)) diff --git a/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/optim-config.yml b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/optim-config.yml new file mode 100644 index 00000000..60cce4a0 --- /dev/null +++ b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/optim-config.yml @@ -0,0 +1,26 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +time-scope: + first-time-step: 0 + last-time-step: 2 + +models: + + - id: simple_models.generator + out-of-bounds-processing: + constraints: + - id: is_on_dynamics + mode: drop + - id: min_up_duration + mode: drop + - id: min_down_duration + mode: drop diff --git a/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/system.yml b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/system.yml new file mode 100644 index 00000000..89b91d53 --- /dev/null +++ b/tests/e2e/functional/studies/system_drop_with_param_in_shift/input/system.yml @@ -0,0 +1,160 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# Study: system_drop_with_param_in_shift +# +# 3 time steps, 1 bus, 1 load [10, 10, 100], 2 generators: +# - gen_1: expensive (marginal_cost=100, startup_cost=500), p_min=10, p_max=90, +# d_min_up=2, d_min_down=1 +# - gen_2: cheap (marginal_cost=10, no startup cost), p_min=0, p_max=10, +# d_min_up=1, d_min_down=1 +# +# The optim-config enables "drop" out-of-bounds processing for +# is_on_dynamics, min_up_duration, and min_down_duration on the generator +# model. Out-of-bounds references are dropped (constraint not instantiated) +# instead of wrapping around: +# - is_on_dynamics (shift -1): dropped at t=0 for both gen_1 and gen_2 → 4 instances +# - min_up_duration (d_min_up=2): sum range [-1,0] → dropped at t=0 for gen_1; +# gen_2 (d_min_up=1, range [0,0]) is always in-bounds → 5 instances total +# - min_down_duration (d_min_down=1): range [0,0] → never dropped → 6 instances +# +# Optimal solution: +# gen_1 starts at t=2 only; gen_2 covers t=0 and t=1. +# With is_on_dynamics dropped at t=0, gen_1's initial state is unconstrained. +# With min_up_duration dropped at t=0 for gen_1, the start at t=2 does not +# force gen_1 on at t=0. gen_1 produces its maximum (90) at t=2; gen_2 +# covers the remaining 10. +# +# gen_2 at t=0: marginal_cost * p = 10*10 = 100 +# gen_2 at t=1: marginal_cost * p = 10*10 = 100 +# gen_1 at t=2: marginal_cost * p = 100*90 = 9000 +# gen_2 at t=2: marginal_cost * p = 10*10 = 100 +# gen_1 startup = 500 +# Total optimal cost = 9800 + +system: + id: system_drop_with_param_in_shift + model_libraries: simple_models + components: + - id: bus + model: simple_models.bus + parameters: + - id: v_nom + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: x + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: y + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_set + time-dependent: false + scenario-dependent: false + value: 1.0 + - id: v_mag_pu_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: v_mag_pu_max + time-dependent: false + scenario-dependent: false + value: 1.0e+20 + + - id: load + model: simple_models.load + parameters: + - id: p_set + time-dependent: true + scenario-dependent: true + value: load + - id: q_set + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: sign + time-dependent: false + scenario-dependent: false + value: -1.0 + + - id: gen_1 + model: simple_models.generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 90.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 100.0 + - id: startup_cost + time-dependent: false + scenario-dependent: false + value: 500.0 + - id: d_min_up + time-dependent: false + scenario-dependent: false + value: 2 + - id: d_min_down + time-dependent: false + scenario-dependent: false + value: 1 + + - id: gen_2 + model: simple_models.generator + parameters: + - id: p_min + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: p_max + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: marginal_cost + time-dependent: false + scenario-dependent: false + value: 10.0 + - id: startup_cost + time-dependent: false + scenario-dependent: false + value: 0.0 + - id: d_min_up + time-dependent: false + scenario-dependent: false + value: 1 + - id: d_min_down + time-dependent: false + scenario-dependent: false + value: 1 + + connections: + - component1: bus + port1: p_balance_port + component2: load + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: gen_1 + port2: p_balance_port + - component1: bus + port1: p_balance_port + component2: gen_2 + port2: p_balance_port diff --git a/tests/e2e/functional/systems/components_for_short_term_storage.yml b/tests/e2e/functional/systems/components_for_short_term_storage.yml index fe9ec2f6..821048e8 100644 --- a/tests/e2e/functional/systems/components_for_short_term_storage.yml +++ b/tests/e2e/functional/systems/components_for_short_term_storage.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: D model: basic.demand parameters: diff --git a/tests/e2e/functional/systems/study_scenario_only_series.yml b/tests/e2e/functional/systems/study_scenario_only_series.yml index 4d9bcfd6..093efab0 100644 --- a/tests/e2e/functional/systems/study_scenario_only_series.yml +++ b/tests/e2e/functional/systems/study_scenario_only_series.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/e2e/functional/systems/study_time_only_series.yml b/tests/e2e/functional/systems/study_time_only_series.yml index c16e1b37..6b281451 100644 --- a/tests/e2e/functional/systems/study_time_only_series.yml +++ b/tests/e2e/functional/systems/study_time_only_series.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/e2e/functional/systems/system.yml b/tests/e2e/functional/systems/system.yml index d48c5cc0..5dbbbb7e 100644 --- a/tests/e2e/functional/systems/system.yml +++ b/tests/e2e/functional/systems/system.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/e2e/functional/systems/system_time_shift_per_component.yml b/tests/e2e/functional/systems/system_time_shift_per_component.yml new file mode 100644 index 00000000..311af23e --- /dev/null +++ b/tests/e2e/functional/systems/system_time_shift_per_component.yml @@ -0,0 +1,103 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# System used to test component-dependent time shift. +# +# Two lagged-storage components (STS_lag1, STS_lag2) connected to the same +# balance node, with lag=1 and lag=2 respectively. +# +# Demand is time-varying: [20, 0, 20, 0] — concentrated at even time steps. +# +# With CYCLE border management over H=4 steps and inflows=5/step: +# - lag=1 (STS_lag1): all four steps are linked; the storage can +# concentrate its 20 units of forced withdrawal at the even demand +# steps (10 per step) → contributes 20 to even-step supply. +# - lag=2 (STS_lag2): even {0,2} and odd {1,3} groups are decoupled; +# even-group sum=10 and odd-group sum=10. The 10 odd-group units +# must be discharged at t=1 or t=3 where demand=0 → forced spillage=10. +# Even-group provides at most 10 to even demand steps. +# +# Total even-step supply: 20 (lag1) + 10 (lag2) = 30 < 40 (demand) → 10 unsupplied. +# Odd-step spillage: 10 (forced by lag=2 group structure). +# +# Expected objective = 10 * unsupplied_cost(100) + 10 * spillage_cost(1) = 1010. +system: + model-libraries: time_shift_test + + components: + - id: N + model: time_shift_test.node + - id: D + model: time_shift_test.demand + parameters: + - id: demand + time-dependent: true + scenario-dependent: false + value: demand_shift_test + + - id: S + model: time_shift_test.spillage + parameters: + - id: cost + value: 1 + + - id: U + model: time_shift_test.unsupplied + parameters: + - id: cost + value: 100 + + - id: STS_lag1 + model: time_shift_test.lagged-storage + parameters: + - id: lag + value: 1 + - id: p_max + value: 10 + - id: level_max + value: 100 + - id: inflows + value: 5 + + - id: STS_lag2 + model: time_shift_test.lagged-storage + parameters: + - id: lag + value: 2 + - id: p_max + value: 10 + - id: level_max + value: 100 + - id: inflows + value: 5 + + connections: + - component1: N + port1: injection_port + component2: D + port2: injection_port + - component1: N + port1: injection_port + component2: S + port2: injection_port + - component1: N + port1: injection_port + component2: U + port2: injection_port + - component1: N + port1: injection_port + component2: STS_lag1 + port2: injection_port + - component1: N + port1: injection_port + component2: STS_lag2 + port2: injection_port \ No newline at end of file diff --git a/tests/e2e/functional/systems/system_with_varying_down_time.yml b/tests/e2e/functional/systems/system_with_varying_down_time.yml new file mode 100644 index 00000000..419d2fdc --- /dev/null +++ b/tests/e2e/functional/systems/system_with_varying_down_time.yml @@ -0,0 +1,114 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +# System used to test thermal clusters with component-dependent d_min_down. +# +# Two thermal clusters (G_0 and G_1) use the `thermal-cluster-dhd` model, +# whose Min-down-time constraint contains both a component-dependent time sum +# and a component-dependent time shift: +# +# sum(t - d_min_down + 1 .. t, nb_stop) <= nb_units_max[t - d_min_down] - nb_on +# +# G_0 has d_min_down=3 and G_1 has d_min_down=5, so the constraint's shift +# values differ per component, exercising the per-component aggregation path. +# +# Demand pattern: [100,100,100,0,0,0,100,100,100,100] +# Both G_0 and G_1 have p_min=50 and p_max=50, so they MUST be off when demand=0. +# They stop at t=3 and cannot restart until their min-down-time is satisfied: +# G_0 (d_min_down=3): off at t=3,4,5 → restarts at t=6 +# G_1 (d_min_down=5): off at t=3,4,5,6,7 → restarts at t=8 +# +# At t=6 and t=7, demand=100 but G_1 cannot restart yet → G_2 covers 50 each step. +# The fact that objective=12250 (not lower) proves d_min_down=5 is binding for G_1. +# +# Expected objective: +# G_0: 7 steps × 50 × cost_10 = 3500 +# G_1: 5 steps × 50 × cost_15 = 3750 +# G_2: 2 steps × 50 × cost_50 = 5000 +# Total = 12250 +system: + model-libraries: basic + components: + - id: N + model: basic.node + - id: G_0 + model: basic.thermal-cluster-dhd + parameters: + - id: cost + value: 10 + - id: p_max + value: 50 + - id: p_min + value: 50 + - id: d_min_up + value: 2 + - id: d_min_down + value: 3 + - id: nb_units_max + value: 1 + - id: nb_failures + value: 0 + + - id: G_1 + model: basic.thermal-cluster-dhd + parameters: + - id: cost + value: 15 + - id: p_max + value: 50 + - id: p_min + value: 50 + - id: d_min_up + value: 5 + - id: d_min_down + value: 5 + - id: nb_units_max + value: 1 + - id: nb_failures + value: 0 + + - id: G_2 + model: basic.generator + parameters: + - id: cost + value: 50 + - id: p_max + value: 100 + + - id: D + model: basic.demand + parameters: + - id: demand + time-dependent: true + scenario-dependent: false + value: demand_var_down_time + + connections: + - component1: N + port1: injection_port + component2: D + port2: injection_port + + - component1: N + port1: injection_port + component2: G_0 + port2: injection_port + + - component1: N + port1: injection_port + component2: G_1 + port2: injection_port + + - component1: N + port1: injection_port + component2: G_2 + port2: injection_port \ No newline at end of file diff --git a/tests/e2e/functional/systems/with_scenarization.yml b/tests/e2e/functional/systems/with_scenarization.yml index 0f0e558e..594a9192 100644 --- a/tests/e2e/functional/systems/with_scenarization.yml +++ b/tests/e2e/functional/systems/with_scenarization.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/e2e/functional/test_build_decomposed_problem.py b/tests/e2e/functional/test_build_decomposed_problem.py new file mode 100644 index 00000000..0ecca041 --- /dev/null +++ b/tests/e2e/functional/test_build_decomposed_problem.py @@ -0,0 +1,119 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Tests for decomposed problems build. + +Each study directory contains: + - input/system.yml : network and component definitions + - input/model-libraries/ : model library YAML files + - input/optim-config.yml : decomposition configuration + - expected_outputs/master.mps : expected MPS for the master problem + - expected_outputs/subproblem.mps : expected MPS for the subproblem + - expected_outputs/structure.txt : expected Benders structure file + +The test builds the decomposed problems from the input files, writes MPS +files via linopy's to_file(), generates structure.txt, and asserts that +the produced files match expected_outputs byte-for-byte. +""" + +from pathlib import Path + +import pytest + +from gems.main.main import ( + _write_structure_txt, + input_database, + input_libs, + input_system, +) +from gems.optim_config.parsing import load_optim_config, validate_optim_config +from gems.simulation import TimeBlock, build_decomposed_problems +from gems.study import Study + +STUDIES_DIR = Path(__file__).parent / "studies" +STUDY_IDS = ["13_1", "13_2"] + + +@pytest.mark.parametrize("study_id", STUDY_IDS) +def test_study_mps_matches_expected(study_id: str, tmp_path: Path) -> None: + study_dir = STUDIES_DIR / study_id + input_dir = study_dir / "input" + expected_dir = study_dir / "expected_outputs" + + # --- Load model libraries --- + lib_paths = sorted((input_dir / "model-libraries").glob("*.yml")) + lib_dict = input_libs(lib_paths) + + # --- Load system and database --- + system_path = input_dir / "system.yml" + system = input_system(system_path, lib_dict) + database = input_database(system_path, timeseries_path=None) + + # --- Load and validate optim-config --- + config_path = input_dir / "optim-config.yml" + optim_config = load_optim_config(config_path) + assert optim_config is not None, f"optim-config.yml not found in {input_dir}" + validate_optim_config(optim_config, system) + + # --- Build decomposed problems (1 timestep, 1 scenario) --- + time_block = TimeBlock(1, [0]) + scenarios = 1 + decomposed = build_decomposed_problems( + Study(system, database), time_block, list(range(scenarios)), optim_config + ) + + # --- Write MPS files --- + decomposed.subproblem.linopy_model.to_file( + tmp_path / f"{decomposed.subproblem.name}.mps" + ) + if decomposed.master is not None: + decomposed.master.linopy_model.to_file( + tmp_path / f"{decomposed.master.name}.mps" + ) + + # --- Write structure.txt --- + _write_structure_txt( + decomposed, + optim_config, + output_dir=tmp_path, + ) + + # --- Assert subproblem MPS matches expected --- + sub_name = decomposed.subproblem.name + generated_sub = (tmp_path / f"{sub_name}.mps").read_text() + expected_sub = (expected_dir / f"{sub_name}.mps").read_text() + assert generated_sub == expected_sub, ( + f"[{study_id}] {sub_name}.mps mismatch.\n" + f"Generated:\n{generated_sub}\n" + f"Expected:\n{expected_sub}" + ) + + # --- Assert master MPS matches expected (if present) --- + if decomposed.master is not None: + master_name = decomposed.master.name + generated_master = (tmp_path / f"{master_name}.mps").read_text() + expected_master = (expected_dir / f"{master_name}.mps").read_text() + assert generated_master == expected_master, ( + f"[{study_id}] {master_name}.mps mismatch.\n" + f"Generated:\n{generated_master}\n" + f"Expected:\n{expected_master}" + ) + + # --- Assert structure.txt matches expected --- + generated_struct = (tmp_path / "structure.txt").read_text() + expected_struct = (expected_dir / "structure.txt").read_text() + assert generated_struct == expected_struct, ( + f"[{study_id}] structure.txt mismatch.\n" + f"Generated:\n{generated_struct}\n" + f"Expected:\n{expected_struct}" + ) diff --git a/tests/e2e/functional/test_component_dependent_time_shift.py b/tests/e2e/functional/test_component_dependent_time_shift.py new file mode 100644 index 00000000..17735656 --- /dev/null +++ b/tests/e2e/functional/test_component_dependent_time_shift.py @@ -0,0 +1,429 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Functional tests for component-dependent time shift in expressions. + +All tests use a ``lagged-storage`` model whose level equation references +the state ``lag`` steps ago under CYCLE border management: + + level[t] − level[t − lag] + withdrawal[t] = inflows + +where ``lag`` is a **per-component constant parameter**. When two +component instances carry different ``lag`` values, the problem builder +must emit different shifted sub-expressions for each component — the +"per-component slow path" in ``linearize._time_shift``. + +Group-partition property (the key insight) +------------------------------------------ +With CYCLE over H steps and a given lag k, summing the level equation +over each orbit of the cyclic permutation σ: t ↦ t − k (mod H) gives: + + sum_{t ∈ orbit} withdrawal[t] = |orbit| × inflows + +where |orbit| = H / gcd(H, k). Timesteps in *different* orbits are +completely decoupled. + +Consequences tested below +-------------------------- +* lag=1, H=4: gcd=1, one orbit of size 4. All four steps are linked; + withdrawal can be freely redistributed across them. + +* lag=2, H=4: gcd=2, two orbits {0,2} and {1,3} each of size 2. + Even and odd steps are decoupled: each group is forced to withdraw + exactly 2 × inflows in total. If demand is concentrated at even + steps, the odd group produces unavoidable spillage. + +* lag=3, H=6: gcd=3, three orbits {0,3}, {1,4}, {2,5} each of size 2. + Each pair sums to 2 × inflows. If demand falls only on {0,3}, the + other four steps generate forced spillage. + +Test scenarios +-------------- +1. **Single component, lag=2, H=4** (scalar DataArray path): + demand=[5, 0, 5, 0] — exactly matches even-group supply (10). + Odd group must discharge 10 units where demand=0 → spillage=10. + Objective = 10 × spillage_cost(1) = 10. + +2. **Two components, lag=1 and lag=2, H=4** (per-component slow path): + demand=[20, 0, 20, 0]. + lag=1 contributes up to 20 to even demand steps (10+10). + lag=2 even-group contributes 10 to even steps; odd-group forces + spillage=10. + Total even supply = 30 < 40 → unsupplied=10. + Objective = 10×100 + 10×1 = 1010. + +3. **Three components, lag=1,2,3, H=6** (slow path, three unique shifts): + demand=[30, 0, 0, 30, 0, 0]. + lag=1: freely provides 30 at demand steps. + lag=2: even/odd split aligns perfectly with demand at t=0/t=3. + lag=3: orbits {0,3},{1,4},{2,5}; demand-orbit sum=10, but demand + already covered → 10 excess spillage there; 20 forced spillage + from the other two orbits. + Total spillage = 30. Objective = 30×1 = 30. + +4. **YAML-based version of scenario 2** (end-to-end YAML path): + Same two-component setup from files; expected objective = 1010. +""" + +from pathlib import Path + +import pandas as pd +import pytest + +from gems.expression import literal, param, var +from gems.expression.indexing_structure import IndexingStructure +from gems.model import ModelPort, float_parameter, float_variable, model +from gems.model.constraint import Constraint +from gems.model.parsing import parse_yaml_library +from gems.model.port import PortFieldDefinition, PortFieldId +from gems.model.resolve_library import resolve_library +from gems.simulation import TimeBlock, build_problem +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + TimeScenarioSeriesData, + create_component, +) +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import ( + build_data_base, + consistency_check, + resolve_system, +) +from tests.e2e.functional.libs.standard import ( + BALANCE_PORT_TYPE, + DEMAND_MODEL, + NODE_BALANCE_MODEL, + SPILLAGE_MODEL, + UNSUPPLIED_ENERGY_MODEL, +) + +CONSTANT = IndexingStructure(False, False) + +# --------------------------------------------------------------------------- +# Shared model definition +# --------------------------------------------------------------------------- + +# Storage whose level equation uses a *parametrised* lag. +# Constraint: level[t] - level[t - lag] + withdrawal[t] = inflows +# Port: flow = withdrawal (positive → supplies the network) +LAGGED_STORAGE_MODEL = model( + id="LAGGED_STORAGE", + parameters=[ + float_parameter("lag", CONSTANT), + float_parameter("p_max", CONSTANT), + float_parameter("level_max", CONSTANT), + float_parameter("inflows", CONSTANT), + ], + variables=[ + float_variable("level", lower_bound=literal(0), upper_bound=param("level_max")), + float_variable( + "withdrawal", lower_bound=literal(0), upper_bound=param("p_max") + ), + ], + ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], + port_fields_definitions=[ + PortFieldDefinition( + port_field=PortFieldId("balance_port", "flow"), + definition=var("withdrawal"), + ) + ], + constraints=[ + Constraint( + name="Level equation", + expression=( + var("level") - var("level").shift(-param("lag")) + var("withdrawal") + == param("inflows") + ), + ) + ], +) + +SPILLAGE_COST = 1.0 +UNSUPPLIED_COST = 100.0 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_demand_series(values: list[float]) -> TimeScenarioSeriesData: + """Build a single-scenario time-series DataArray from a list of values.""" + df = pd.DataFrame(values, columns=[0]) + return TimeScenarioSeriesData(df) + + +def _base_database( + demand_data: TimeScenarioSeriesData | ConstantData, +) -> DataBase: + database = DataBase() + database.add_data("D", "demand", demand_data) + database.add_data("S", "cost", ConstantData(SPILLAGE_COST)) + database.add_data("U", "cost", ConstantData(UNSUPPLIED_COST)) + return database + + +def _add_storage( + database: DataBase, + component_id: str, + *, + lag: int, + inflows: float, + p_max: float, + level_max: float = 1000.0, +) -> None: + database.add_data(component_id, "lag", ConstantData(lag)) + database.add_data(component_id, "p_max", ConstantData(p_max)) + database.add_data(component_id, "level_max", ConstantData(level_max)) + database.add_data(component_id, "inflows", ConstantData(inflows)) + + +def _build_system(*storage_ids: str) -> System: + node = Component(model=NODE_BALANCE_MODEL, id="N") + demand_comp = create_component(model=DEMAND_MODEL, id="D") + spillage_comp = create_component(model=SPILLAGE_MODEL, id="S") + unsupplied_comp = create_component(model=UNSUPPLIED_ENERGY_MODEL, id="U") + storage_comps = [ + create_component(model=LAGGED_STORAGE_MODEL, id=sid) for sid in storage_ids + ] + + system = System("test") + system.add_component(node) + for comp in [demand_comp, spillage_comp, unsupplied_comp] + storage_comps: + system.add_component(comp) + system.connect(PortRef(comp, "balance_port"), PortRef(node, "balance_port")) + return system + + +# --------------------------------------------------------------------------- +# Test 1 — single component, lag=2, H=4 (scalar DataArray path) +# --------------------------------------------------------------------------- + + +def test_single_lag2_forced_odd_spillage() -> None: + """ + Single lagged-storage with lag=2 over H=4 time steps. + + Demand = [5, 0, 5, 0] — concentrated at even time steps (t=0, t=2). + + Group-partition (lag=2, H=4, gcd=2): + even orbit {0, 2}: withdrawal sums to 2 × inflows = 10 → exactly covers + even demand (5 + 5 = 10). No unsupplied energy. + odd orbit {1, 3}: withdrawal sums to 10 but demand = 0 there → + spillage = 10 (unavoidable regardless of odd split). + + With spillage_cost=1 and unsupplied_cost=100: + objective = 10 × 1 = 10. + + The lag parameter is a scalar DataArray (one component) — exercises the + scalar code path in ``_time_shift``, not the per-component slow path. + """ + horizon = 4 + inflows = 5.0 + + database = _base_database(_make_demand_series([5.0, 0.0, 5.0, 0.0])) + _add_storage(database, "STS", lag=2, inflows=inflows, p_max=10.0) + + system = _build_system("STS") + problem = build_problem( + Study(system, database), + TimeBlock(1, list(range(horizon))), + scenario_ids=list(range(1)), + ) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + # Odd orbit forces 10 units of spillage; no unsupplied energy. + assert problem.objective_value == pytest.approx(10.0 * SPILLAGE_COST) + + +# --------------------------------------------------------------------------- +# Test 2 — two components, lag=1 and lag=2, H=4 (per-component slow path) +# --------------------------------------------------------------------------- + + +def test_two_components_different_lags_asymmetric_demand() -> None: + """ + Two lagged-storage instances: STS_lag1 (lag=1) and STS_lag2 (lag=2). + Both have inflows=5/step, p_max=10, over H=4 steps. + + Demand = [20, 0, 20, 0] — all demand at even time steps. + + Analysis: + STS_lag1 (lag=1, gcd=1, one orbit of size 4): + The storage can freely concentrate its 20 forced units at even + steps → contributes 10 at t=0 and 10 at t=2. + No spillage from STS_lag1. + + STS_lag2 (lag=2, gcd=2, two orbits of size 2): + Even orbit {0,2}: sum = 10 → at most 10 units to even demand steps. + Odd orbit {1,3}: sum = 10 → 10 units of forced spillage (demand=0). + + Balance: + Even-step supply = 20 (lag1) + 10 (lag2) = 30 < 40 (demand) → unsupplied = 10. + Odd-step spillage = 10. + + With spillage_cost=1 and unsupplied_cost=100: + objective = 10 × 100 + 10 × 1 = 1010. + + The ``lag`` parameter DataArray has a component dimension with two + distinct values ([−1, −2] after negation), triggering the per-component + slow path in ``linearize._time_shift``. + """ + horizon = 4 + inflows = 5.0 + + database = _base_database(_make_demand_series([20.0, 0.0, 20.0, 0.0])) + _add_storage(database, "STS1", lag=1, inflows=inflows, p_max=10.0) + _add_storage(database, "STS2", lag=2, inflows=inflows, p_max=10.0) + + system = _build_system("STS1", "STS2") + problem = build_problem( + Study(system, database), + TimeBlock(1, list(range(horizon))), + scenario_ids=list(range(1)), + ) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + # 10 unsupplied (even demand not covered) + 10 spillage (odd forced). + expected = 10.0 * UNSUPPLIED_COST + 10.0 * SPILLAGE_COST + assert problem.objective_value == pytest.approx(expected) + + +# --------------------------------------------------------------------------- +# Test 3 — three components, lag=1,2,3, H=6 (slow path, three unique shifts) +# --------------------------------------------------------------------------- + + +def test_three_components_distinct_lags_orbit_spillage() -> None: + """ + Three lagged-storage instances: lag=1, lag=2, lag=3 over H=6 steps. + All have inflows=5/step, p_max=15. + + Demand = [30, 0, 0, 30, 0, 0] — at t=0 and t=3 only. + + Group-partition analysis: + lag=1, gcd(6,1)=1 → one orbit of size 6. + Freely provides 30 at demand steps (15 at t=0, 15 at t=3). ✓ + + lag=2, gcd(6,2)=2 → even orbit {0,2,4} sum=15, odd orbit {1,3,5} sum=15. + Even orbit: up to 15 at t=0. Odd orbit: up to 15 at t=3. + Provides 30 at demand steps with no forced off-demand production. ✓ + + lag=3, gcd(6,3)=3 → orbits {0,3}, {1,4}, {2,5} each sum=10. + Orbit {0,3}: forced total = 10 ≡ excess at demand steps (already covered). + Orbits {1,4} and {2,5}: forced total = 10 + 10 = 20 at non-demand steps + → 20 units of unavoidable spillage. + The 10 units from orbit {0,3} are also excess (demand covered by lag1+lag2) + → 10 more units of spillage. + + Balance: + Demand steps covered: lag1(30) + lag2(30) = 60 = demand(60). ✓ + lag3 adds 10 excess at demand steps → spillage = 10 there. + lag3 forces 20 spillage at non-demand steps. + Total spillage = 30. Unsupplied = 0. + + With spillage_cost=1: + objective = 30 × 1 = 30. + + The ``lag`` parameter DataArray carries three distinct values, so the + slow-path masking loop runs three iterations. + """ + horizon = 6 + inflows = 5.0 + + database = _base_database(_make_demand_series([30.0, 0.0, 0.0, 30.0, 0.0, 0.0])) + for lag, sid in [(1, "STS1"), (2, "STS2"), (3, "STS3")]: + _add_storage(database, sid, lag=lag, inflows=inflows, p_max=15.0) + + system = _build_system("STS1", "STS2", "STS3") + problem = build_problem( + Study(system, database), + TimeBlock(1, list(range(horizon))), + scenario_ids=list(range(1)), + ) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + # lag=3 forces 30 total spillage; no unsupplied. + assert problem.objective_value == pytest.approx(30.0 * SPILLAGE_COST) + + +# --------------------------------------------------------------------------- +# Test 4 — YAML-based end-to-end: two components, lag=1 and lag=2, H=4 +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def _libs_dir() -> Path: + return Path(__file__).parent / "libs" + + +@pytest.fixture(scope="module") +def _systems_dir() -> Path: + return Path(__file__).parent / "systems" + + +@pytest.fixture(scope="module") +def _series_dir() -> Path: + return Path(__file__).parent / "series" + + +def test_two_components_different_lags_yaml( + _libs_dir: Path, _systems_dir: Path, _series_dir: Path +) -> None: + """ + YAML-based end-to-end counterpart of + ``test_two_components_different_lags_asymmetric_demand``. + + Reads the ``time_shift_test`` library from ``lib_time_shift.yml`` and + the system from ``system_time_shift_per_component.yml``. + Demand is loaded from the ``demand_shift_test`` time series file + ([20, 0, 20, 0]) from the series directory. + + Same two-component setup (STS_lag1: lag=1, STS_lag2: lag=2, both + inflows=5, p_max=10) over H=4 steps with CYCLE. + + Expected: same forced unsupplied (10) and spillage (10) as the + Python test → objective = 10 × 100 + 10 × 1 = 1010. + """ + lib_file = _libs_dir / "lib_time_shift.yml" + system_file = _systems_dir / "system_time_shift_per_component.yml" + + with lib_file.open() as f: + input_library = parse_yaml_library(f) + with system_file.open() as f: + input_system = parse_yaml_components(f) + + lib_dict = resolve_library([input_library]) + system = resolve_system(input_system, lib_dict) + consistency_check(system, lib_dict["time_shift_test"].models) + + database = build_data_base(input_system, timeseries_dir=_series_dir) + + problem = build_problem( + Study(system, database), + TimeBlock(1, list(range(4))), + scenario_ids=list(range(1)), + ) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + expected = 10.0 * UNSUPPLIED_COST + 10.0 * SPILLAGE_COST + assert problem.objective_value == pytest.approx(expected) diff --git a/tests/e2e/functional/test_dual_reduced_cost.py b/tests/e2e/functional/test_dual_reduced_cost.py new file mode 100644 index 00000000..2e47675e --- /dev/null +++ b/tests/e2e/functional/test_dual_reduced_cost.py @@ -0,0 +1,154 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +End-to-end tests for the dual() and reduced_cost() extra-output operators. + +Studies 10_5 and 10_5_1: single timestep, two generators, one load. +Study 10_5_2: three timesteps, time-varying costs and loads. + +Negative tests verify that dual/reduced_cost are rejected when used in +contexts where they are not allowed (constraints, objectives). +""" + +from pathlib import Path + +import pytest + +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study.folder import load_study + +STUDIES_DIR = Path(__file__).parent / "studies" + + +@pytest.mark.parametrize( + "study_id, expected", + [ + ( + "10_5", + { + "objective": 900.0, + "base_zone.price": 10.0, + "gas_base_zone.generation_reduced_cost": 0.0, + "oil_base_zone.generation_reduced_cost": 40.0, + }, + ), + ( + "10_5_1", + { + "objective": 1500.0, + "base_zone.price": 50.0, + "gas_base_zone.generation_reduced_cost": -40.0, + "oil_base_zone.generation_reduced_cost": 0.0, + }, + ), + ], +) +def test_dual_reduced_cost_single_timestep( + study_id: str, expected: dict +) -> None: + """Verify nodal price (dual) and reduced costs for single-timestep studies.""" + study = load_study(STUDIES_DIR / study_id) + time_block = TimeBlock(1, [0]) + problem = build_problem(study, time_block, [0]) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(expected["objective"]) + + st = SimulationTableBuilder().build(problem) + + price = st.component("base_zone").output("price").value( + time_index=0, scenario_index=0 + ) + assert price == pytest.approx(expected["base_zone.price"]) + + gas_rc = st.component("gas_base_zone").output("generation_reduced_cost").value( + time_index=0, scenario_index=0 + ) + assert gas_rc == pytest.approx(expected["gas_base_zone.generation_reduced_cost"]) + + oil_rc = st.component("oil_base_zone").output("generation_reduced_cost").value( + time_index=0, scenario_index=0 + ) + assert oil_rc == pytest.approx(expected["oil_base_zone.generation_reduced_cost"]) + + +def test_dual_reduced_cost_multi_timestep() -> None: + """Verify nodal prices and reduced costs for a 3-timestep study (10_5_2).""" + study = load_study(STUDIES_DIR / "10_5_2") + time_block = TimeBlock(1, [0, 1, 2]) + problem = build_problem(study, time_block, [0]) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(27550.0) + + st = SimulationTableBuilder().build(problem) + + # Nodal prices at t=0,1,2 + for t, expected_price in enumerate([10.0, 15.0, 20000.0]): + price = st.component("base_zone").output("price").value( + time_index=t, scenario_index=0 + ) + assert price == pytest.approx(expected_price), ( + f"price at t={t}: expected {expected_price}, got {price}" + ) + + # Gas generator reduced costs at t=0,1,2 + for t, expected_rc in enumerate([0.0, 0.0, -19960.0]): + rc = st.component("gas_base_zone").output("generation_reduced_cost").value( + time_index=t, scenario_index=0 + ) + assert rc == pytest.approx(expected_rc, abs=1e-3), ( + f"gas RC at t={t}: expected {expected_rc}, got {rc}" + ) + + # Oil generator reduced costs at t=0,1,2 + for t, expected_rc in enumerate([20.0, -5.0, -19990.0]): + rc = st.component("oil_base_zone").output("generation_reduced_cost").value( + time_index=t, scenario_index=0 + ) + assert rc == pytest.approx(expected_rc, abs=1e-3), ( + f"oil RC at t={t}: expected {expected_rc}, got {rc}" + ) + + +def test_dual_in_constraint_is_rejected() -> None: + """dual() in a constraint expression must be caught by the library resolver.""" + from gems.expression.parsing.parse_expression import ModelIdentifiers, parse_expression + from gems.model.resolve_library import _forbid_dual_or_rc + + ids = ModelIdentifiers( + variables={"x"}, + parameters=set(), + constraints={"balance"}, + ) + expr = parse_expression("dual(balance) + x", ids) + with pytest.raises(ValueError, match="Operators dual/reduced_cost are not allowed"): + _forbid_dual_or_rc(expr, "constraint 'bad'") + + +def test_reduced_cost_in_objective_is_rejected() -> None: + """reduced_cost() in an objective contribution must be caught by the library resolver.""" + from gems.expression.parsing.parse_expression import ModelIdentifiers, parse_expression + from gems.model.resolve_library import _forbid_dual_or_rc + + ids = ModelIdentifiers( + variables={"x"}, + parameters=set(), + constraints=set(), + ) + expr = parse_expression("reduced_cost(x)", ids) + with pytest.raises(ValueError, match="Operators dual/reduced_cost are not allowed"): + _forbid_dual_or_rc(expr, "objective contribution 'obj'") diff --git a/tests/e2e/functional/test_investment.py b/tests/e2e/functional/test_investment.py index 92c47ecc..03c4d2ef 100644 --- a/tests/e2e/functional/test_investment.py +++ b/tests/e2e/functional/test_investment.py @@ -23,26 +23,29 @@ Constraint, Model, ModelPort, - ProblemContext, float_parameter, float_variable, int_variable, model, ) from gems.model.port import PortFieldDefinition, PortFieldId -from gems.simulation import ( - MergedProblemStrategy, - OutputValues, - TimeBlock, - build_problem, +from gems.optim_config.parsing import ( + ElementLocation, + ElementLocationConfig, + ModelDecompositionConfig, + ModelOptimConfig, + OptimConfig, + validate_optim_config, ) +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder from gems.study import ( Component, ConstantData, DataBase, - Network, - Node, PortRef, + Study, + System, TimeScenarioSeriesData, create_component, ) @@ -56,15 +59,11 @@ FREE = IndexingStructure(True, True) -INVESTMENT = ProblemContext.INVESTMENT -OPERATIONAL = ProblemContext.OPERATIONAL -COUPLING = ProblemContext.COUPLING - @pytest.fixture def thermal_candidate() -> Model: THERMAL_CANDIDATE = model( - id="GEN", + id="THERMAL_CANDIDATE", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -76,7 +75,6 @@ def thermal_candidate() -> Model: lower_bound=literal(0), upper_bound=literal(1000), structure=CONSTANT, - context=COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -91,14 +89,41 @@ def thermal_candidate() -> Model: name="Max generation", expression=var("generation") <= var("p_max") ) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("p_max"), + }, ) return THERMAL_CANDIDATE +@pytest.fixture +def thermal_candidate_optim_config() -> OptimConfig: + return OptimConfig( + models=[ + ModelOptimConfig( + id="THERMAL_CANDIDATE", + model_decomposition=ModelDecompositionConfig( + variables=[ + ElementLocationConfig( + id="p_max", + location=ElementLocation.MASTER_AND_SUBPROBLEMS, + ), + ], + objective_contributions=[ + ElementLocationConfig( + id="investment", location=ElementLocation.MASTER + ), + ElementLocationConfig( + id="operational", location=ElementLocation.SUBPROBLEMS + ), + ], + ), + ) + ] + ) + + @pytest.fixture def discrete_candidate() -> Model: DISCRETE_CANDIDATE = model( @@ -114,14 +139,12 @@ def discrete_candidate() -> Model: "p_max", lower_bound=literal(0), structure=CONSTANT, - context=COUPLING, ), int_variable( "nb_units", lower_bound=literal(0), upper_bound=literal(10), structure=CONSTANT, - context=INVESTMENT, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -138,17 +161,51 @@ def discrete_candidate() -> Model: Constraint( name="Max investment", expression=var("p_max") == param("p_max_per_unit") * var("nb_units"), - context=INVESTMENT, ), ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("p_max"), + }, ) return DISCRETE_CANDIDATE +@pytest.fixture +def discrete_candidate_optim_config() -> OptimConfig: + return OptimConfig( + models=[ + ModelOptimConfig( + id="DISCRETE", + model_decomposition=ModelDecompositionConfig( + variables=[ + ElementLocationConfig( + id="p_max", + location=ElementLocation.MASTER_AND_SUBPROBLEMS, + ), + ElementLocationConfig( + id="nb_units", location=ElementLocation.MASTER + ), + ], + constraints=[ + ElementLocationConfig( + id="Max investment", location=ElementLocation.MASTER + ), + ], + objective_contributions=[ + ElementLocationConfig( + id="investment", location=ElementLocation.MASTER + ), + ElementLocationConfig( + id="operational", location=ElementLocation.SUBPROBLEMS + ), + ], + ), + ) + ] + ) + + @pytest.fixture def generator() -> Component: generator = create_component( @@ -180,6 +237,7 @@ def test_generation_xpansion_single_time_step_single_scenario( generator: Component, candidate: Component, demand: Component, + thermal_candidate_optim_config: OptimConfig, ) -> None: """ Simple generation expansion problem on one node. One timestep, one scenario, one thermal cluster candidate. @@ -209,40 +267,38 @@ def test_generation_xpansion_single_time_step_single_scenario( database.add_data("CAND", "invest_cost", ConstantData(490)) database.add_data("CAND", "max_invest", ConstantData(1000)) - node = Node(model=NODE_BALANCE_MODEL, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) + node = Component(model=NODE_BALANCE_MODEL, id="N") + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(generator) + system.add_component(candidate) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) + + validate_optim_config(thermal_candidate_optim_config, system) scenarios = 1 problem = build_problem( - network, - database, + Study(system, database), TimeBlock(1, [0]), - scenarios, - build_strategy=MergedProblemStrategy(), + list(range(scenarios)), ) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx( - 490 * 100 + 100 * 10 + 200 * 40 - ) - - output = OutputValues(problem) - expected_output = OutputValues() - expected_output.component("G1").var("generation").value = 200.0 - expected_output.component("CAND").var("generation").value = 100.0 - expected_output.component("CAND").var("p_max").value = 100.0 - expected_output.component("N") - expected_output.component("D") - - assert output == expected_output, f"Output differs from expected: {output}" + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(490 * 100 + 100 * 10 + 200 * 40) + + df = SimulationTableBuilder().build(problem) + assert df.component("G1").output("generation").value( + time_index=0, scenario_index=0 + ) == pytest.approx(200.0) + assert df.component("CAND").output("generation").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) + assert df.component("CAND").output("p_max").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) def test_two_candidates_xpansion_single_time_step_single_scenario( @@ -250,6 +306,8 @@ def test_two_candidates_xpansion_single_time_step_single_scenario( candidate: Component, cluster_candidate: Component, demand: Component, + thermal_candidate_optim_config: OptimConfig, + discrete_candidate_optim_config: OptimConfig, ) -> None: """ As before, simple generation expansion problem on one node, one timestep and one scenario @@ -286,47 +344,60 @@ def test_two_candidates_xpansion_single_time_step_single_scenario( database.add_data("DISCRETE", "invest_cost", ConstantData(200)) database.add_data("DISCRETE", "p_max_per_unit", ConstantData(10)) - node = Node(model=NODE_BALANCE_MODEL, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.add_component(cluster_candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - network.connect( + node = Component(model=NODE_BALANCE_MODEL, id="N") + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(generator) + system.add_component(candidate) + system.add_component(cluster_candidate) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) + system.connect( PortRef(cluster_candidate, "balance_port"), PortRef(node, "balance_port") ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) + validate_optim_config(thermal_candidate_optim_config, system) + validate_optim_config(discrete_candidate_optim_config, system) - status = problem.solver.Solve() + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx( + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx( (45 * 200) + (490 * 100 + 10 * 100) + (200 * 100 + 10 * 100) ) - output = OutputValues(problem) - expected_output = OutputValues() - expected_output.component("G1").var("generation").value = 200.0 - expected_output.component("CAND").var("generation").value = 100.0 - expected_output.component("CAND").var("p_max").value = 100.0 - expected_output.component("DISCRETE").var("generation").value = 100.0 - expected_output.component("DISCRETE").var("p_max").value = 100.0 - expected_output.component("DISCRETE").var("nb_units").value = 10.0 - expected_output.component("D") - expected_output.component("N") - assert output == expected_output, f"Output differs from expected: {output}" + df = SimulationTableBuilder().build(problem) + assert df.component("G1").output("generation").value( + time_index=0, scenario_index=0 + ) == pytest.approx(200.0) + assert df.component("CAND").output("generation").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) + assert df.component("CAND").output("p_max").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) + assert df.component("DISCRETE").output("generation").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) + assert df.component("DISCRETE").output("p_max").value( + time_index=0, scenario_index=0 + ) == pytest.approx(100.0) + assert df.component("DISCRETE").output("nb_units").value( + time_index=0, scenario_index=0 + ) == pytest.approx(10.0) def test_generation_xpansion_two_time_steps_two_scenarios( generator: Component, candidate: Component, demand: Component, + thermal_candidate_optim_config: OptimConfig, ) -> None: """ Same as previous example but with two timesteps and two scenarios, in order to test the correct instantiation of the objective function @@ -369,43 +440,45 @@ def test_generation_xpansion_two_time_steps_two_scenarios( database.add_data("CAND", "invest_cost", ConstantData(490)) database.add_data("CAND", "max_invest", ConstantData(1000)) - node = Node(model=NODE_BALANCE_MODEL, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - - problem = build_problem(network, database, time_block, scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL + node = Component(model=NODE_BALANCE_MODEL, id="N") + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(generator) + system.add_component(candidate) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) + + validate_optim_config(thermal_candidate_optim_config, system) + + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" # assert problem.solver.NumVariables() == 2 * scenarios * horizon + 1 # assert ( # problem.solver.NumConstraints() == 3 * scenarios * horizon # ) # Flow balance, Max generation for each cluster - assert problem.solver.Objective().Value() == pytest.approx( + assert problem.objective_value == pytest.approx( 490 * 300 + 0.5 * (10 * 300 + 10 * 300 + 40 * 200) + 0.5 * (10 * 200 + 10 * 300 + 40 * 100) ) - output = OutputValues(problem) - expected_output = OutputValues() - expected_output.component("G1").var("generation").value = [ - [0.0, 200.0], - [0.0, 100.0], - ] - expected_output.component("CAND").var("generation").value = [ - [300.0, 300.0], - [200.0, 300.0], - ] - expected_output.component("CAND").var("p_max").value = 300.0 - - expected_output.component("N") - expected_output.component("D") - - assert output == expected_output, f"Output differs from expected: {output}" + df = SimulationTableBuilder().build(problem) + + g1_view = df.component("G1").output("generation") + assert g1_view.value(time_index=0, scenario_index=0) == pytest.approx(0.0) + assert g1_view.value(time_index=1, scenario_index=0) == pytest.approx(200.0) + assert g1_view.value(time_index=0, scenario_index=1) == pytest.approx(0.0) + assert g1_view.value(time_index=1, scenario_index=1) == pytest.approx(100.0) + + cand_view = df.component("CAND").output("generation") + assert cand_view.value(time_index=0, scenario_index=0) == pytest.approx(300.0) + assert cand_view.value(time_index=1, scenario_index=0) == pytest.approx(300.0) + assert cand_view.value(time_index=0, scenario_index=1) == pytest.approx(200.0) + assert cand_view.value(time_index=1, scenario_index=1) == pytest.approx(300.0) + + assert df.component("CAND").output("p_max").value( + time_index=0, scenario_index=0 + ) == pytest.approx(300.0) diff --git a/tests/e2e/functional/test_investment_pathway.py b/tests/e2e/functional/test_investment_pathway.py deleted file mode 100644 index 80aa44c6..00000000 --- a/tests/e2e/functional/test_investment_pathway.py +++ /dev/null @@ -1,484 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -import pytest - -from gems.expression import literal, param, var -from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext -from gems.model.constraint import Constraint -from gems.model.model import ModelPort, model -from gems.model.parameter import float_parameter -from gems.model.port import PortField, PortFieldDefinition, PortFieldId, PortType -from gems.model.variable import float_variable -from gems.simulation import BendersSolution, TimeBlock, build_benders_decomposed_problem -from gems.simulation.decision_tree import ( - DecisionTreeNode, - InterDecisionTimeScenarioConfig, -) -from gems.study.data import ConstantData, DataBase, TreeData -from gems.study.network import Component, Network, Node, PortRef, create_component -from tests.e2e.functional.libs.standard import ( - DEMAND_MODEL, - GENERATOR_MODEL, - NODE_WITH_SPILL_AND_ENS, -) - - -@pytest.fixture -def generator() -> Component: - generator = create_component( - model=GENERATOR_MODEL, - id="BASE", - ) - return generator - - -CONSTANT = IndexingStructure(False, False) -BALANCE_PORT_TYPE = PortType(id="balance", fields=[PortField("flow")]) - - -@pytest.fixture -def candidate() -> Component: - candidate = create_component( - model=model( - id="GEN_WITH_INSTALLED_CAPA", - parameters=[ - float_parameter("op_cost", CONSTANT), - float_parameter("invest_cost", CONSTANT), - float_parameter("max_invest", CONSTANT), - float_parameter("installed_capa", CONSTANT), - ], - variables=[ - float_variable("generation", lower_bound=literal(0)), - float_variable( - "invested_capa", - lower_bound=literal(0), - structure=CONSTANT, - context=ProblemContext.COUPLING, - ), - float_variable( - "delta_invest", - lower_bound=literal(0), - upper_bound=param("max_invest"), - structure=CONSTANT, - context=ProblemContext.INVESTMENT, - ), - ], - ports=[ - ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port"), - ], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ), - ], - constraints=[ - Constraint( - name="Max generation", - expression=var("generation") - <= param("installed_capa") + var("invested_capa"), - ) - ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") - * var("delta_invest"), - ), - id="CAND", - ) - return candidate - - -@pytest.fixture -def demand() -> Component: - demand = create_component(model=DEMAND_MODEL, id="D") - return demand - - -@pytest.fixture -def node() -> Node: - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - return node - - -def test_investment_pathway_on_sequential_nodes( - node: Node, - demand: Component, - candidate: Component, -) -> None: - """ - A first simple test on the investment pathway - Here, only two nodes are represented, a parent and a child nodes - with probability one of going from parent to child - - The goal here is to show that, in the parent node, the demand is already met - by the existing fixed production. However, for the child node without any new - investment, it would create some unsupplied energy, which is very expensive. - - The investment on the child node, even though enough for the demand, is also - more expensive than on the parent (this should represent a late investment fee). - - To minimize the expected cost in this 2-node tree, one should expect the maximum - investment on the parent node, and the rest on the child node. - - Here below the values used: - - PARENT | CHILD - Demand (MW): 100 200 - Fixed prod (MW): 100 100 - Max invest (MW): 80 100 - Op cost ($): 10 10 - Invest cost ($): 100 300 - ENS cost ($): 10000 10000 - - The solution should be: - - prob | investment | operational - parent: 1 x [ 100 x 80 + 10 x 100 ] - child : + 1 x [ 300 x 20 + 10 x 200 ] - = 17 000 - - """ - # === Populating Database === - database = DataBase() - database.add_data("N", "spillage_cost", ConstantData(10)) - database.add_data("N", "ens_cost", ConstantData(10000)) - - database.add_data( - "D", - "demand", - TreeData( - { - "parent": ConstantData(100), - "child": ConstantData(200), - } - ), - ) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "installed_capa", ConstantData(100)) - - database.add_data( - "CAND", - "invest_cost", - TreeData( - { - "parent": ConstantData(100), - "child": ConstantData(300), - } - ), - ) - - database.add_data( - "CAND", - "max_invest", - TreeData( - { - "parent": ConstantData(80), - "child": ConstantData(100), - } - ), - ) - - # === Network === - demand_par = demand.replicate() - candidate_par = candidate.replicate() - - network_par = Network("parent_test") - network_par.add_node(node) - network_par.add_component(demand_par) - network_par.add_component(candidate_par) - network_par.connect( - PortRef(demand_par, "balance_port"), PortRef(node, "balance_port") - ) - network_par.connect( - PortRef(candidate_par, "balance_port"), PortRef(node, "balance_port") - ) - - demand_chd = demand.replicate() - candidate_chd = candidate.replicate() - - network_chd = Network("child_test") - network_chd.add_node(node) - network_chd.add_component(demand_chd) - network_chd.add_component(candidate_chd) - network_chd.connect( - PortRef(demand_chd, "balance_port"), PortRef(node, "balance_port") - ) - network_chd.connect( - PortRef(candidate_chd, "balance_port"), PortRef(node, "balance_port") - ) - - # === Decision tree creation === - config = InterDecisionTimeScenarioConfig([TimeBlock(0, [0])], 1) - - decision_tree_par = DecisionTreeNode("parent", config, network_par) - decision_tree_chd = DecisionTreeNode( - "child", config, network_chd, parent=decision_tree_par - ) - - # === Coupling model === - decision_tree_par.define_coupling_constraint( - candidate_par, - "invested_capa", - candidate_par, - "invested_capa", - "delta_invest", - ) - decision_tree_chd.define_coupling_constraint( - candidate_par, - "invested_capa", - candidate_chd, - "invested_capa", - "delta_invest", - ) - - # === Build problem === - xpansion = build_benders_decomposed_problem(decision_tree_par, database) - - data = { - "solution": { - "overall_cost": 17_000, - "values": { - "parent.CAND.delta_invest": 80, - "child.CAND.delta_invest": 20, - "parent.CAND.invested_capa": 80, - "child.CAND.invested_capa": 100, - }, - } - } - solution = BendersSolution(data) - - # === Run === - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" - - -def test_investment_pathway_on_a_tree_with_one_root_two_children( - generator: Component, - candidate: Component, - demand: Component, - node: Node, -) -> None: - """ - This use case aims at representing the situation where investment decisions are to be made at different, say "planning times". - An actualization rate can be taken into account. - - The novelty compared the actual usage of planning tools, is that the planning decisions at a given time - are taken without knowing exactly which "macro-scenario" / hypothesis on the system that will eventually happen - (only knowing the probability distribution of these hypothesis). - - This example models a case where investment decisions have to be made in 2030 and 2040. - - In 2030, we have full knowledge of the existing assets - - In 2040, two possible hypothesis are possible : - - P=0.2 => A case where there is no change in the generation assets since 2030 (except the potential investment in 2030) - - P=0.8 => A case where a base generation unit is present - - When taking the decision in 2030, we do not know which case will occur in 2040 - and we seek the best decision given a risk criterion (the expectation here). - The value of these models lies in the output for the first decision rather than - the decisions at the later stages as the first decisions are related to "what we have to do today" ? - More specifically, to define the use case, we define the following tree representing the system at the different decision times and hypothesis - - 2030 (root node) : - Demand = 300 - Generator : - P_max = 200, - Production cost = 10, - Max investment = 400, - Investment cost = 100 - Unsupplied energy : - Cost = 10 000 - - 2040 with new base (child A) : - Demand = 600 - Generator : - P_max = 200, - Production cost = 10, - Max investment = 100, - Investment cost = 50 - Base : - P_max = 200, - Production cost = 5 - Unsupplied energy : - Cost = 10 000 - - 2040 no base (child B) : - Demand = 600 - Generator : - P_max = 200, - Production cost = 10, - Max investment = 100, - Investment cost = 50 - Unsupplied energy : - Cost = 10 000 - - In the second decision time, demand increases from 300 to 600 in both scenarios. - However, investment capacity in the candidate is limited to 100 in the second stage. - Investment cost decreases to reflect the effect of a discount rate. - - In case 1, a base unit of capacity 100 has arrived and can produce at smaller cost than the candidate. - As it is more interesting to invest the latest possible, the optimal solution for this scenario is to invest [100, 100]. - - In case 2, there is no base unit and the max investment is 100 in the second stage, - therefore if we consider scenario 2 only, as unsupplied energy is very expensive, the best investment is [300, 100] - - But here as we solve on the tree, we need to find the best solution in expectation on the set of paths in the tree. - - Case 1 : prob | investment | operational - root: 1 x [ 100 x 100 + 10 x 300 ] - child A: + 0.8 x [ 50 x 100 + 10 x 400 (generator) + 5 x 200 (base)] - child B: + 0.2 x [ 50 x 100 + 10 x 400 (generator) + 10 000 x 200 (unsupplied energy)] - = 422 800 - - Case 2 : prob | investment | operational - root: 1 x [ 100 x 300 + 10 x 300 ] - child A: + 0.8 x [ 50 x 0 + 10 x 400 (generator) + 5 x 200 (base)] - child B: + 0.2 x [ 50 x 100 + 10 x 600 (generator)] - = 39 200 - - As investing less than 300 in the first stage would increase the unsupplied energy and lead to an increase in overall cost - (-1 MW invested in 1st stage => + 1 MW unsupplied energy => +900/MW cost increase more or less), the optimal solution is to invest : - - 300 at first stage - - 0 in child A - - 100 in child B - """ - - # === Populating Database === - database = DataBase() - database.add_data("N", "spillage_cost", ConstantData(10)) - database.add_data("N", "ens_cost", ConstantData(10_000)) - - database.add_data( - "D", - "demand", - TreeData( - { - "root": ConstantData(300), - "childA": ConstantData(600), - "childB": ConstantData(600), - } - ), - ) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "installed_capa", ConstantData(200)) - database.add_data( - "CAND", - "invest_cost", - TreeData( - { - "root": ConstantData(100), - "childA": ConstantData(50), - "childB": ConstantData(50), - } - ), - ) - database.add_data( - "CAND", - "max_invest", - TreeData( - { - "root": ConstantData(400), - "childA": ConstantData(100), - "childB": ConstantData(100), - } - ), - ) - - database.add_data("BASE", "p_max", ConstantData(200)) - database.add_data("BASE", "cost", ConstantData(5)) - - # === Network === - network_root = Network("root_network") - network_root.add_node(node) - network_root.add_component(demand) - network_root.add_component(candidate) - network_root.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network_root.connect( - PortRef(candidate, "balance_port"), PortRef(node, "balance_port") - ) - - network_childA = network_root.replicate(id="childA_network") - network_childA.add_component(generator) - network_childA.connect( - PortRef(generator, "balance_port"), PortRef(node, "balance_port") - ) - - network_childB = network_root.replicate(id="childB_network") - - # === Decision tree creation === - time_scenario_config = InterDecisionTimeScenarioConfig([TimeBlock(0, [0])], 1) - - dt_root = DecisionTreeNode("root", time_scenario_config, network_root) - dt_child_A = DecisionTreeNode( - "childA", time_scenario_config, network_childA, parent=dt_root, prob=0.8 - ) - dt_child_B = DecisionTreeNode( - "childB", time_scenario_config, network_childB, parent=dt_root, prob=0.2 - ) - - # === Coupling model === - dt_root.define_coupling_constraint( - candidate, - "invested_capa", - candidate, - "invested_capa", - "delta_invest", - ) - dt_child_A.define_coupling_constraint( - candidate, - "invested_capa", - candidate, - "invested_capa", - "delta_invest", - ) - dt_child_B.define_coupling_constraint( - candidate, - "invested_capa", - candidate, - "invested_capa", - "delta_invest", - ) - - # === Build problem === - xpansion = build_benders_decomposed_problem(dt_root, database) - - data = { - "solution": { - "overall_cost": 39_200, - "values": { - "root.CAND.delta_invest": 300, - "childA.CAND.delta_invest": 0, - "childB.CAND.delta_invest": 100, - "root.CAND.invested_capa": 300, - "childA.CAND.invested_capa": 300, - "childB.CAND.invested_capa": 400, - }, - } - } - solution = BendersSolution(data) - - # === Run === - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" diff --git a/tests/e2e/functional/test_libs_python_system_python.py b/tests/e2e/functional/test_libs_python_system_python.py index 01a7bebe..df6a1bf7 100644 --- a/tests/e2e/functional/test_libs_python_system_python.py +++ b/tests/e2e/functional/test_libs_python_system_python.py @@ -13,7 +13,7 @@ """ This module contains end-to-end functional tests for systems built by: - Using Python models, -- Building the network object in Python. +- Building the system object in Python. Several cases are tested: @@ -41,13 +41,14 @@ from gems.expression.indexing_structure import IndexingStructure from gems.model import Model, ModelPort, float_parameter, float_variable, model from gems.model.port import PortFieldDefinition, PortFieldId -from gems.simulation import BlockBorderManagement, TimeBlock, build_problem +from gems.simulation import TimeBlock, build_problem from gems.study import ( + Component, ConstantData, DataBase, - Network, - Node, PortRef, + Study, + System, TimeScenarioIndex, TimeScenarioSeriesData, create_component, @@ -74,7 +75,7 @@ def test_basic_balance() -> None: database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component( model=DEMAND_MODEL, id="D", @@ -85,19 +86,20 @@ def test_basic_balance() -> None: id="G", ) - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3000 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3000 def test_timeseries() -> None: @@ -121,7 +123,7 @@ def test_timeseries() -> None: demand_time_scenario_series = TimeScenarioSeriesData(demand_data) database.add_data("D", "demand", demand_time_scenario_series) - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") demand = create_component( model=DEMAND_MODEL, id="D", @@ -132,25 +134,24 @@ def test_timeseries() -> None: id="G", ) - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) time_block = TimeBlock(1, [0, 1]) scenarios = 1 - problem = build_problem(network, database, time_block, scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 100 * 30 + 50 * 30 + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 100 * 30 + 50 * 30 -def create_one_node_network(generator_model: Model) -> Network: - node = Node(model=NODE_BALANCE_MODEL, id="1") +def create_one_node_network(generator_model: Model) -> System: + node = Component(model=NODE_BALANCE_MODEL, id="1") demand = create_component( model=DEMAND_MODEL, id="D", @@ -161,13 +162,13 @@ def create_one_node_network(generator_model: Model) -> Network: id="G", ) - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) - return network + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + return system def create_simple_database(max_generation: float = 100) -> DataBase: @@ -181,14 +182,14 @@ def create_simple_database(max_generation: float = 100) -> DataBase: def test_variable_bound() -> None: """ - Create a network with one node, one demand and one generator on this node. + Create a system with one node, one demand and one generator on this node. Demand is constant 100, cost of generation is constant 30. Max generation can be chosen to make it infeasible or not. Variation of generator model using variable bound instead of constraint. """ generator_model = model( - id="GEN", + id="GEN_WITH_VARIABLE_BOUND", parameters=[ float_parameter("p_max", IndexingStructure(False, False)), float_parameter("cost", IndexingStructure(False, False)), @@ -207,38 +208,39 @@ def test_variable_bound() -> None: definition=var("generation"), ) ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) - network = create_one_node_network(generator_model) + system = create_one_node_network(generator_model) database = create_simple_database(max_generation=200) - problem = build_problem(network, database, TimeBlock(1, [0]), 1) - status = problem.solver.Solve() + problem = build_problem(Study(system, database), TimeBlock(1, [0]), list(range(1))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3000 - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3000 - - network = create_one_node_network(generator_model) + system = create_one_node_network(generator_model) database = create_simple_database(max_generation=80) - problem = build_problem(network, database, TimeBlock(1, [0]), 1) - status = problem.solver.Solve() - assert status == problem.solver.INFEASIBLE # Infeasible + problem = build_problem(Study(system, database), TimeBlock(1, [0]), list(range(1))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "infeasible" # Infeasible - network = create_one_node_network(generator_model) + system = create_one_node_network(generator_model) database = create_simple_database(max_generation=0) # Equal upper and lower bounds - problem = build_problem(network, database, TimeBlock(1, [0]), 1) - status = problem.solver.Solve() - assert status == problem.solver.INFEASIBLE + problem = build_problem(Study(system, database), TimeBlock(1, [0]), list(range(1))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "infeasible" - network = create_one_node_network(generator_model) + system = create_one_node_network(generator_model) database = create_simple_database(max_generation=-10) with pytest.raises( ValueError, match=r"Upper bound \(-10\) must be strictly greater than lower bound \(0\) for variable G.generation", ): - problem = build_problem(network, database, TimeBlock(1, [0]), 1) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(1)) + ) def generate_data( @@ -275,7 +277,7 @@ def short_term_storage_base(efficiency: float, horizon: int) -> None: database.add_data("STS1", "inflows", ConstantData(0)) database.add_data("STS1", "efficiency", ConstantData(efficiency)) - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") spillage = create_component(model=SPILLAGE_MODEL, id="S") unsupplied = create_component(model=UNSUPPLIED_ENERGY_MODEL, id="U") @@ -287,44 +289,30 @@ def short_term_storage_base(efficiency: float, horizon: int) -> None: id="STS1", ) - network = Network("test") - network.add_node(node) + system = System("test") + system.add_component(node) for component in [demand, short_term_storage, spillage, unsupplied]: - network.add_component(component) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect( + system.add_component(component) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect( PortRef(short_term_storage, "balance_port"), PortRef(node, "balance_port") ) - network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(unsupplied, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(unsupplied, "balance_port"), PortRef(node, "balance_port")) problem = build_problem( - network, - database, + Study(system, database), time_blocks[0], - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" # The short-term storage should satisfy the load # No spillage / unsupplied energy is expected - assert problem.solver.Objective().Value() == pytest.approx(0, abs=0.01) - - count_variables = 0 - for variable in problem.solver.variables(): - if "injection" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 100 - elif "withdrawal" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 50 - elif "level" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 1000 - assert count_variables == 3 * horizon + assert problem.objective_value == pytest.approx(0, abs=0.01) + + # TODO: update variable access def test_short_test_horizon_10() -> None: diff --git a/tests/e2e/functional/test_libs_yaml_system_python.py b/tests/e2e/functional/test_libs_yaml_system_python.py index a0a98d2a..1407f73f 100644 --- a/tests/e2e/functional/test_libs_yaml_system_python.py +++ b/tests/e2e/functional/test_libs_yaml_system_python.py @@ -13,7 +13,7 @@ """ This module contains end-to-end functional tests for systems built by: - Reading the model library from a YAML file, -- Building the network objet directly in Python. +- Building the system objet directly in Python. The tests validate various scenarios involving energy balance, generation, spillage, and demand across nodes and networks. @@ -26,7 +26,7 @@ 6. `test_changing_demand`: Tests energy balance on a single node with changing demand over three timesteps. 7. `test_min_up_down_times_2`: Similar to `test_min_up_down_times`, but with different minimum up/down time constraints for a thermal generator over three timesteps. -Each test builds a network of nodes and components, defines a database of +Each test builds a system of nodes and components, defines a database of parameters, and solves the problem. Assertions are made to ensure the solver's results meet expected outcomes. """ @@ -34,18 +34,14 @@ import pytest from gems.model.library import Library -from gems.simulation import ( - BlockBorderManagement, - OutputValues, - TimeBlock, - build_problem, -) +from gems.simulation import TimeBlock, build_problem from gems.study import ( + Component, ConstantData, DataBase, - Network, - Node, PortRef, + Study, + System, TimeScenarioSeriesData, create_component, ) @@ -64,11 +60,11 @@ def test_basic_balance(lib_dict: dict[str, Library]) -> None: database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - production_model = lib_dict["basic"].models["production"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + production_model = lib_dict["basic"].models["basic.production"] - node = Node(model=node_model, id="N") + node = Component(model=node_model, id="N") demand = create_component( model=demand_model, id="D", @@ -79,19 +75,20 @@ def test_basic_balance(lib_dict: dict[str, Library]) -> None: id="G", ) - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3000 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3000 def test_link(lib_dict: dict[str, Library]) -> None: @@ -107,13 +104,13 @@ def test_link(lib_dict: dict[str, Library]) -> None: database.add_data("L", "f_max", ConstantData(150)) - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - production_model = lib_dict["basic"].models["production"] - link_model = lib_dict["basic"].models["link"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + production_model = lib_dict["basic"].models["basic.production"] + link_model = lib_dict["basic"].models["basic.link"] - node1 = Node(model=node_model, id="1") - node2 = Node(model=node_model, id="2") + node1 = Component(model=node_model, id="1") + node2 = Component(model=node_model, id="2") demand = create_component( model=demand_model, id="D", @@ -127,29 +124,26 @@ def test_link(lib_dict: dict[str, Library]) -> None: id="L", ) - network = Network("test") - network.add_node(node1) - network.add_node(node2) - network.add_component(demand) - network.add_component(gen) - network.add_component(link) - network.connect(PortRef(demand, "balance_port"), PortRef(node1, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node2, "balance_port")) - network.connect(PortRef(link, "in_port"), PortRef(node1, "balance_port")) - network.connect(PortRef(link, "out_port"), PortRef(node2, "balance_port")) + system = System("test") + system.add_component(node1) + system.add_component(node2) + system.add_component(demand) + system.add_component(gen) + system.add_component(link) + system.connect(PortRef(demand, "balance_port"), PortRef(node1, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node2, "balance_port")) + system.connect(PortRef(link, "in_port"), PortRef(node1, "balance_port")) + system.connect(PortRef(link, "out_port"), PortRef(node2, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3500 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3500 - for variable in problem.solver.variables(): - if "balance_port_from" in variable.name(): - assert variable.solution_value() == 100 - if "balance_port_to" in variable.name(): - assert variable.solution_value() == -100 + # TODO: update variable access def test_stacking_generation(lib_dict: dict[str, Library]) -> None: @@ -166,11 +160,11 @@ def test_stacking_generation(lib_dict: dict[str, Library]) -> None: database.add_data("G2", "p_max", ConstantData(100)) database.add_data("G2", "cost", ConstantData(50)) - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - production_model = lib_dict["basic"].models["production"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + production_model = lib_dict["basic"].models["basic.production"] - node1 = Node(model=node_model, id="1") + node1 = Component(model=node_model, id="1") demand = create_component( model=demand_model, @@ -187,21 +181,22 @@ def test_stacking_generation(lib_dict: dict[str, Library]) -> None: id="G2", ) - network = Network("test") - network.add_node(node1) - network.add_component(demand) - network.add_component(gen1) - network.add_component(gen2) - network.connect(PortRef(demand, "balance_port"), PortRef(node1, "balance_port")) - network.connect(PortRef(gen1, "balance_port"), PortRef(node1, "balance_port")) - network.connect(PortRef(gen2, "balance_port"), PortRef(node1, "balance_port")) + system = System("test") + system.add_component(node1) + system.add_component(demand) + system.add_component(gen1) + system.add_component(gen2) + system.connect(PortRef(demand, "balance_port"), PortRef(node1, "balance_port")) + system.connect(PortRef(gen1, "balance_port"), PortRef(node1, "balance_port")) + system.connect(PortRef(gen2, "balance_port"), PortRef(node1, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 30 * 100 + 50 * 50 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 30 * 100 + 50 * 50 def test_spillage(lib_dict: dict[str, Library]) -> None: @@ -217,31 +212,30 @@ def test_spillage(lib_dict: dict[str, Library]) -> None: database.add_data("G1", "p_min", ConstantData(200)) database.add_data("G1", "cost", ConstantData(30)) - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - production_with_min_model = lib_dict["basic"].models["production_with_min"] - spillage_model = lib_dict["basic"].models["spillage"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + production_with_min_model = lib_dict["basic"].models["basic.production_with_min"] + spillage_model = lib_dict["basic"].models["basic.spillage"] - node = Node(model=node_model, id="1") + node = Component(model=node_model, id="1") spillage = create_component(model=spillage_model, id="S") demand = create_component(model=demand_model, id="D") gen1 = create_component(model=production_with_min_model, id="G1") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen1) - network.add_component(spillage) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen1, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - - problem = build_problem(network, database, TimeBlock(0, [1]), 1) - status = problem.solver.Solve() + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen1) + system.add_component(spillage) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen1, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 30 * 200 + 50 * 10 + problem = build_problem(Study(system, database), TimeBlock(0, [1]), list(range(1))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 30 * 200 + 50 * 10 def test_min_up_down_times(lib_dict: dict[str, Library]) -> None: @@ -295,13 +289,13 @@ def test_min_up_down_times(lib_dict: dict[str, Library]) -> None: time_block = TimeBlock(1, [0, 1, 2]) scenarios = 1 - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - spillage_model = lib_dict["basic"].models["spillage"] - unsuplied_model = lib_dict["basic"].models["unsuplied"] - thermal_cluster = lib_dict["basic"].models["thermal_cluster"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + spillage_model = lib_dict["basic"].models["basic.spillage"] + unsuplied_model = lib_dict["basic"].models["basic.unsuplied"] + thermal_cluster = lib_dict["basic"].models["basic.thermal_cluster"] - node = Node(model=node_model, id="1") + node = Component(model=node_model, id="1") demand = create_component(model=demand_model, id="D") gen = create_component(model=thermal_cluster, id="G") @@ -310,32 +304,28 @@ def test_min_up_down_times(lib_dict: dict[str, Library]) -> None: unsupplied_energy = create_component(model=unsuplied_model, id="U") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.add_component(spillage) - network.add_component(unsupplied_energy) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - network.connect( + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.add_component(spillage) + system.add_component(unsupplied_energy) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) + system.connect( PortRef(unsupplied_energy, "balance_port"), PortRef(node, "balance_port") ) problem = build_problem( - network, - database, + Study(system, database), time_block, - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() + problem.solve(solver_name="highs") - print(OutputValues(problem).component("G").var("nb_units_on").value) - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(72000, abs=0.01) + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(72000, abs=0.01) def test_changing_demand(lib_dict: dict[str, Library]) -> None: @@ -365,33 +355,30 @@ def test_changing_demand(lib_dict: dict[str, Library]) -> None: time_block = TimeBlock(1, [0, 1, 2]) scenarios = 1 - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - production_model = lib_dict["basic"].models["production"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + production_model = lib_dict["basic"].models["basic.production"] - node = Node(model=node_model, id="1") + node = Component(model=node_model, id="1") demand = create_component(model=demand_model, id="D") prod = create_component(model=production_model, id="G") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(prod) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(prod, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(prod) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(prod, "balance_port"), PortRef(node, "balance_port")) problem = build_problem( - network, - database, + Study(system, database), time_block, - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 40000 + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 40000 def test_min_up_down_times_2(lib_dict: dict[str, Library]) -> None: @@ -445,13 +432,13 @@ def test_min_up_down_times_2(lib_dict: dict[str, Library]) -> None: time_block = TimeBlock(1, [0, 1, 2]) scenarios = 1 - node_model = lib_dict["basic"].models["node"] - demand_model = lib_dict["basic"].models["demand"] - spillage_model = lib_dict["basic"].models["spillage"] - unsuplied_model = lib_dict["basic"].models["unsuplied"] - thermal_cluster = lib_dict["basic"].models["thermal_cluster"] + node_model = lib_dict["basic"].models["basic.node"] + demand_model = lib_dict["basic"].models["basic.demand"] + spillage_model = lib_dict["basic"].models["basic.spillage"] + unsuplied_model = lib_dict["basic"].models["basic.unsuplied"] + thermal_cluster = lib_dict["basic"].models["basic.thermal_cluster"] - node = Node(model=node_model, id="1") + node = Component(model=node_model, id="1") demand = create_component(model=demand_model, id="D") gen = create_component(model=thermal_cluster, id="G") @@ -460,30 +447,25 @@ def test_min_up_down_times_2(lib_dict: dict[str, Library]) -> None: unsupplied_energy = create_component(model=unsuplied_model, id="U") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.add_component(spillage) - network.add_component(unsupplied_energy) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - network.connect( + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.add_component(spillage) + system.add_component(unsupplied_energy) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) + system.connect( PortRef(unsupplied_energy, "balance_port"), PortRef(node, "balance_port") ) problem = build_problem( - network, - database, + Study(system, database), time_block, - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() - - print(problem.solver.ExportModelAsMpsFormat(fixed_format=False, obfuscated=False)) - print(OutputValues(problem).component("G").var("nb_units_on").value) + problem.solve(solver_name="highs") - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(61000) + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(61000) diff --git a/tests/e2e/functional/test_libs_yaml_system_yaml.py b/tests/e2e/functional/test_libs_yaml_system_yaml.py index 77636851..635d9f89 100644 --- a/tests/e2e/functional/test_libs_yaml_system_yaml.py +++ b/tests/e2e/functional/test_libs_yaml_system_yaml.py @@ -15,13 +15,13 @@ """ This module contains end-to-end functional tests for systems built by: - Reading the model library from a YAML file, -- Reading the network from a YAML file. +- Reading the system from a YAML file. Several cases are tested: 1. **Basic balance using YAML inputs**: - **Function**: `test_basic_balance_using_yaml` - - **Description**: Verifies that the system can achieve an optimal balance between supply and demand using basic YAML inputs for the model and network. The test ensures that the solver reaches an optimal solution with the expected objective value. + - **Description**: Verifies that the system can achieve an optimal balance between supply and demand using basic YAML inputs for the model and system. The test ensures that the solver reaches an optimal solution with the expected objective value. 2. **Basic balance with time-only series**: - **Function**: `test_basic_balance_time_only_series` @@ -41,116 +41,144 @@ import pytest -from gems.model.parsing import InputLibrary, parse_yaml_library +from gems.model.parsing import LibrarySchema, parse_yaml_library from gems.model.resolve_library import resolve_library from gems.simulation import TimeBlock, build_problem -from gems.simulation.optimization import BlockBorderManagement from gems.study.data import DataBase -from gems.study.network import Network -from gems.study.parsing import InputSystem, parse_yaml_components +from gems.study.parsing import SystemSchema, parse_yaml_components from gems.study.resolve_components import ( build_data_base, - build_network, consistency_check, resolve_system, ) +from gems.study.study import Study +from gems.study.system import System def test_basic_balance_using_yaml( - input_system: InputSystem, input_library: InputLibrary + input_system: SystemSchema, input_library: LibrarySchema ) -> None: result_lib = resolve_library([input_library]) - components_input = resolve_system(input_system, result_lib) - consistency_check(components_input.components, result_lib["basic"].models) + system = resolve_system(input_system, result_lib) + consistency_check(system, result_lib["basic"].models) database = build_data_base(input_system, None) - network = build_network(components_input) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3000 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3000 @pytest.fixture def setup_test( libs_dir: Path, systems_dir: Path, series_dir: Path -) -> Callable[[], Tuple[Network, DataBase]]: - def _setup_test(study_file_name: str): +) -> Callable[[str], Study]: + def _setup_test(study_file_name: str) -> Study: study_file = systems_dir / study_file_name lib_file = libs_dir / "lib_unittest.yml" with lib_file.open() as lib: input_library = parse_yaml_library(lib) with study_file.open() as c: - input_study = parse_yaml_components(c) + input_system = parse_yaml_components(c) lib_dict = resolve_library([input_library]) - network_components = resolve_system(input_study, lib_dict) - consistency_check(network_components.components, lib_dict["basic"].models) + system = resolve_system(input_system, lib_dict) + consistency_check(system, lib_dict["basic"].models) - database = build_data_base(input_study, series_dir) - network = build_network(network_components) - return network, database + database = build_data_base(input_system, series_dir) + return Study(system, database) return _setup_test def test_basic_balance_time_only_series( - setup_test: Callable[[], Tuple[Network, DataBase]], + setup_test: Callable[[str], Study], ) -> None: - network, database = setup_test("study_time_only_series.yml") + study = setup_test("study_time_only_series.yml") scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0, 1]), scenarios) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 10000 + problem = build_problem(study, TimeBlock(1, [0, 1]), list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 10000 def test_basic_balance_scenario_only_series( - setup_test: Callable[[], Tuple[Network, DataBase]], + setup_test: Callable[[str], Study], ) -> None: - network, database = setup_test("study_scenario_only_series.yml") + study = setup_test("study_scenario_only_series.yml") scenarios = 2 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 0.5 * 5000 + 0.5 * 10000 + problem = build_problem(study, TimeBlock(1, [0]), list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 0.5 * 5000 + 0.5 * 10000 def test_short_term_storage_base_with_yaml( - setup_test: Callable[[], Tuple[Network, DataBase]], + setup_test: Callable[[str], Study], ) -> None: - network, database = setup_test("components_for_short_term_storage.yml") + study = setup_test("components_for_short_term_storage.yml") # 18 produced in the 1st time-step, then consumed 2 * efficiency in the rest scenarios = 1 horizon = 10 time_blocks = [TimeBlock(0, list(range(horizon)))] problem = build_problem( - network, - database, + study, time_blocks[0], - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" # The short-term storage should satisfy the load # No spillage / unsupplied energy is expected - assert problem.solver.Objective().Value() == 0 - - count_variables = 0 - for variable in problem.solver.variables(): - if "injection" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 100 - elif "withdrawal" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 50 - elif "level" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 1000 - assert count_variables == 3 * horizon + assert problem.objective_value == 0 + + # TODO: update variable access + + +def test_varying_down_time( + setup_test: Callable[[str], Study], +) -> None: + """ + Two thermal clusters with different min-down-times actually start and stop, + proving the per-component aggregation constraint is binding. + + Setup: + G_0: cost=10, p_min=p_max=50, d_min_down=3 + G_1: cost=15, p_min=p_max=50, d_min_down=5 + G_2: cost=50, p_max=100 (backup generator, no p_min) + Demand: [100,100,100, 0,0,0, 100,100,100,100] + + Both G_0 and G_1 have p_min=50, so they MUST be off when demand=0 (t=3,4,5). + They stop at t=3 and cannot restart until their min-down-time is met: + G_0 (d_min_down=3): off at t=3,4,5 → restarts at t=6 + G_1 (d_min_down=5): off at t=3,4,5,6,7 → restarts at t=8 + + At t=6 and t=7, demand=100 but G_1 is still locked out → G_2 covers 50. + + The fact that objective=12250 (not 9250 which would be achieved if G_1 + could restart at t=6) proves d_min_down=5 is binding. + + Expected objective: + G_0: 7 steps × 50 × 10 = 3500 + G_1: 5 steps × 50 × 15 = 3750 + G_2: 2 steps × 50 × 50 = 5000 + Total = 12250 + """ + study = setup_test("system_with_varying_down_time.yml") + scenarios = 1 + horizon = 10 + + problem = build_problem( + study, + TimeBlock(0, list(range(horizon))), + list(range(scenarios)), + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(12250.0) diff --git a/tests/e2e/functional/test_optim_modes.py b/tests/e2e/functional/test_optim_modes.py new file mode 100644 index 00000000..fa14fd8e --- /dev/null +++ b/tests/e2e/functional/test_optim_modes.py @@ -0,0 +1,175 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +E2E consistency test: frontal, parallel-subproblems, and sequential-subproblems +resolution modes must produce identical per-timestep results for a fully +time-separable LP problem. + +All three modes run on the dsr_3_blocks study over 504 timesteps (last-time-step=503). +The sequential config uses block-length=168 and block-overlap=1, which produces +4 blocks (3 full + 1 partial) over the shared time scope. +Per-timestep values are compared after deduplicating overlap rows. +""" + +import shutil +import textwrap +from pathlib import Path + +import pandas as pd +import pytest + +from gems.study.runner import run_study + +_STUDY_SRC = Path(__file__).parent / "studies" / "dsr_3_blocks" + +_FRONTAL_CONFIG = textwrap.dedent("""\ + time-scope: + first-time-step: 0 + last-time-step: 503 + solver-options: + name: highs + logs: false + parameters: "" + scenario-scope: + nb-scenarios: 1 + resolution: + mode: frontal +""") + +_PARALLEL_CONFIG = textwrap.dedent("""\ + time-scope: + first-time-step: 0 + last-time-step: 503 + solver-options: + name: highs + logs: false + parameters: "" + scenario-scope: + nb-scenarios: 1 + resolution: + mode: parallel-subproblems + block-length: 168 +""") + +_SEQUENTIAL_CONFIG = textwrap.dedent("""\ + time-scope: + first-time-step: 0 + last-time-step: 503 + solver-options: + name: highs + logs: false + parameters: "" + scenario-scope: + nb-scenarios: 1 + resolution: + mode: sequential-subproblems + block-length: 168 + block-overlap: 1 +""") + +_KEY_COLS = [ + "component", + "output", + "absolute-time-index", + "scenario-index", +] + + +def _load_csv(study_dir: Path) -> pd.DataFrame: + output_files = list((study_dir / "output").glob("**/simulation_table_*.csv")) + assert len(output_files) == 1, f"Expected 1 output file, got {len(output_files)}" + return pd.read_csv(output_files[0]) + + +def _run_with_config(study_dir: Path, config_yaml: str) -> pd.DataFrame: + """Write optim-config.yml, run the study, return the raw simulation table.""" + config_path = study_dir / "input" / "optim-config.yml" + config_path.write_text(config_yaml) + run_study(study_dir) + return _load_csv(study_dir) + + +def _per_timestep_df(raw: pd.DataFrame) -> pd.DataFrame: + """Return per-timestep rows, dropping aggregate and overlap-duplicate rows.""" + df = raw[raw["output"] != "objective-value"].copy() + df = df.drop_duplicates(subset=_KEY_COLS) + df = df.sort_values(_KEY_COLS).reset_index(drop=True) + return df + + +def _total_objective(raw: pd.DataFrame) -> float: + """Sum all per-block objective-value entries.""" + return float(raw.loc[raw["output"] == "objective-value", "value"].sum()) + + +@pytest.mark.parametrize( + "other_config,label,check_objective", + [ + (_PARALLEL_CONFIG, "parallel", True), + (_SEQUENTIAL_CONFIG, "sequential", False), + ], +) +def test_optim_modes_produce_identical_results( + tmp_path: Path, + other_config: str, + label: str, + check_objective: bool, +) -> None: + """Frontal mode and *label* mode yield the same per-timestep dispatch values. + + For parallel mode (no overlap) the summed block objectives are also asserted + equal to the frontal objective. For sequential mode the summed block objectives + are not directly comparable to frontal because overlapping timesteps contribute + to two blocks' objective values each. + """ + frontal_dir = tmp_path / "frontal" + other_dir = tmp_path / label + shutil.copytree(_STUDY_SRC, frontal_dir) + shutil.copytree(_STUDY_SRC, other_dir) + + frontal_raw = _run_with_config(frontal_dir, _FRONTAL_CONFIG) + other_raw = _run_with_config(other_dir, other_config) + + if check_objective: + assert _total_objective(frontal_raw) == pytest.approx( + _total_objective(other_raw), rel=1e-6 + ), ( + f"Total objective differs: frontal={_total_objective(frontal_raw)}, " + f"{label}={_total_objective(other_raw)}" + ) + + frontal_df = _per_timestep_df(frontal_raw) + other_df = _per_timestep_df(other_raw) + + assert set(frontal_df[_KEY_COLS].itertuples(index=False)) == set( + other_df[_KEY_COLS].itertuples(index=False) + ), ( + f"Frontal and {label} modes cover different " + "(component, output, timestep, scenario) sets" + ) + + merged = frontal_df[_KEY_COLS + ["value"]].merge( + other_df[_KEY_COLS + ["value"]], + on=_KEY_COLS, + suffixes=("_frontal", f"_{label}"), + ) + + mismatches = merged[ + (merged["value_frontal"] - merged[f"value_{label}"]).abs() + > 1e-6 * merged["value_frontal"].abs().clip(lower=1e-12) + ] + + assert mismatches.empty, ( + f"Per-timestep value mismatches between frontal and {label} " + f"({len(mismatches)} rows):\n{mismatches[_KEY_COLS + ['value_frontal', f'value_{label}']].to_string()}" + ) diff --git a/tests/e2e/functional/test_out_of_bounds_processing.py b/tests/e2e/functional/test_out_of_bounds_processing.py new file mode 100644 index 00000000..84bd0a89 --- /dev/null +++ b/tests/e2e/functional/test_out_of_bounds_processing.py @@ -0,0 +1,185 @@ +# Copyright (c) 2026, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Functional tests for out-of-bounds-processing in optim-config.yml. + +Two families of studies are tested. + +--- Storage studies (simple_system_*) --- + +The ``state_of_charge_balance`` constraint in the storage_unit model +references ``state_of_charge[t-1]``. On the first timestep (t=0), this +shift falls outside the current 3-step block. + +cyclic (default) + The shifted term wraps around: ``state_of_charge[-1]`` maps to + ``state_of_charge[2]``. The storage state is coupled across the + full block, so the net dispatch is zero and the generator must + supply the entire load (50 units/step × 3 steps × cost 10 = 1500). + +drop + The constraint is not instantiated at t=0. The storage can freely + dispatch up to 40 units at t=0 (no energy balance required there), + and continues to satisfy the balance at t=1 and t=2. The generator + only needs to cover the gap (50 − 40 = 10 units/step × 3 steps + × cost 10 = 300). + +--- Generator commitment studies (system_*_with_param_in_shift) --- + +These studies test out-of-bounds processing when the time shift is expressed +via a model parameter (d_min_up, d_min_down) and used within a sum bound rather than a literal integer. +3 time steps, load [10, 10, 100]. + +gen_1: p_max=90, p_min=10, marginal_cost=100, startup_cost=500, + d_min_up=2, d_min_down=1. +gen_2: p_max=10, p_min=0, marginal_cost=10, startup_cost=0, + d_min_up=1, d_min_down=1. + +cyclic + All commitment constraints apply at every time step with cyclic wrapping + (6 instances each for is_on_dynamics, min_up_duration, min_down_duration). + Optimal: gen_1 off at t=0 (cyclic is_on_dynamics allows this), starts at + t=1, on at t=2. gen_2 covers t=0. + Cost = 10*10 + 100*(10+90) + 10*10 + 500 = 10700. + +drop + Commitment constraints are dropped at out-of-bounds time steps. + is_on_dynamics (shift -1): dropped at t=0 for both components → 4 instances. + min_up_duration (sum t-d_min_up+1..t): dropped at t=0 for gen_1 (d_min_up=2) + but kept for gen_2 (d_min_up=1, no out-of-bounds shift) → 5 instances. + min_down_duration (sum t-d_min_down+1..t): never dropped (d_min_down=1 for + both, shift range [0,0] always in-bounds) → 6 instances. + Gen_1 starts at t=2 only; gen_2 covers t=0 and t=1. + Cost = 10*10 + 10*10 + 100*90 + 10*10 + 500 = 9800. +""" + +import time +from pathlib import Path +from typing import Dict + +import linopy +import pytest + +from gems.optim_config.parsing import load_optim_config +from gems.simulation import TimeBlock, build_decomposed_problems +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study.folder import load_study + +STUDIES_DIR = Path(__file__).parent / "studies" + + +def _count_active(model: linopy.Model, name: str) -> int: + """Return the number of active (non-NaN) rows in a linopy constraint.""" + con = model.constraints[name] + return int((con.lhs.vars.values >= 0).any(axis=-1).sum()) + + +@pytest.mark.parametrize( + "study_id, expected_objective", + [ + ("simple_system_cyclic", 1500.0), + ("simple_system_drop", 300.0), + ("system_cyclic_with_param_in_shift", 10700.0), + ("system_drop_with_param_in_shift", 9800.0), + ], +) +def test_out_of_bounds_processing(study_id: str, expected_objective: float) -> None: + study = load_study(STUDIES_DIR / study_id) + config_path = STUDIES_DIR / study_id / "input" / "optim-config.yml" + optim_config = load_optim_config(config_path) + + # 3-step block matching the study parameters (first-time-step: 0, last-time-step: 2) + time_block = TimeBlock(1, [0, 1, 2]) + scenarios = [0] + + decomposed = build_decomposed_problems(study, time_block, scenarios, optim_config) + decomposed.subproblem.solve(solver_name="highs") + + passed = False + try: + assert decomposed.subproblem.termination_condition == "optimal" + assert decomposed.subproblem.objective_value == pytest.approx( + expected_objective + ) + passed = True + finally: + if not passed: + # Dbugging information if the test fails + timestamp = time.strftime("%Y%m%d-%H%M%S") + study_path = STUDIES_DIR / study_id + output_dir = study_path / "output" + output_dir.mkdir(parents=True, exist_ok=True) + decomposed.subproblem.export_lp( + output_dir / f"{study_path.stem}_{timestamp}.lp" + ) + decomposed.subproblem.export_lp( + output_dir / f"{study_path.stem}_{timestamp}.mps" + ) + builder = SimulationTableBuilder(simulation_id=study_path.stem) + st = builder.build(decomposed.subproblem) + st.write_csv( + output_dir=output_dir, + simulation_id=f"{study_path.stem}_{timestamp}", + optim_nb=decomposed.subproblem.block.id, + ) + + +_GEN_PREFIX = "simple_models.generator" + +# Expected number of active constraint rows per (study_id, constraint_name). +# Constraints: 2 components (gen_1, gen_2) × 3 timesteps = 6 potential instances each. +# +# system_cyclic_with_param_in_shift — no drop mode, all instances present: +# is_on_dynamics (lb + ub): 6 each +# min_up_duration (ub only): 6 +# min_down_duration (ub only): 6 +# +# system_drop_with_param_in_shift — drop mode active: +# is_on_dynamics: shift -1 (from is_on[t-1]) → dropped at t=0 for all → 4 +# min_up_duration: gen_1 has d_min_up=2 → range [-1,0] → dropped at t=0 for gen_1; +# gen_2 has d_min_up=1 → range [0,0] → kept at t=0 → 2+3=5 +# min_down_duration: both have d_min_down=1 → range [0,0] → never dropped → 6 +_EXPECTED_CONSTRAINT_COUNTS: Dict[str, Dict[str, int]] = { + "system_cyclic_with_param_in_shift": { + f"{_GEN_PREFIX}__is_on_dynamics__lb": 6, + f"{_GEN_PREFIX}__is_on_dynamics__ub": 6, + f"{_GEN_PREFIX}__min_up_duration__ub": 6, + f"{_GEN_PREFIX}__min_down_duration__ub": 6, + }, + "system_drop_with_param_in_shift": { + f"{_GEN_PREFIX}__is_on_dynamics__lb": 4, + f"{_GEN_PREFIX}__is_on_dynamics__ub": 4, + f"{_GEN_PREFIX}__min_up_duration__ub": 5, + f"{_GEN_PREFIX}__min_down_duration__ub": 6, + }, +} + + +@pytest.mark.parametrize("study_id", list(_EXPECTED_CONSTRAINT_COUNTS)) +def test_constraint_instantiation(study_id: str) -> None: + """Check that is_on_dynamics, min_up_duration, and min_down_duration are + instantiated at exactly the expected (component, time) pairs.""" + study = load_study(STUDIES_DIR / study_id) + config_path = STUDIES_DIR / study_id / "input" / "optim-config.yml" + optim_config = load_optim_config(config_path) + time_block = TimeBlock(1, [0, 1, 2]) + decomposed = build_decomposed_problems(study, time_block, [0], optim_config) + linopy_model = decomposed.subproblem.linopy_model + + expected = _EXPECTED_CONSTRAINT_COUNTS[study_id] + for constraint_name, expected_count in expected.items(): + actual = _count_active(linopy_model, constraint_name) + assert actual == expected_count, ( + f"{study_id}: constraint '{constraint_name}' has {actual} active rows, " + f"expected {expected_count}" + ) diff --git a/tests/e2e/functional/test_performance.py b/tests/e2e/functional/test_performance.py index 4caa342e..ef162f9b 100644 --- a/tests/e2e/functional/test_performance.py +++ b/tests/e2e/functional/test_performance.py @@ -20,7 +20,15 @@ from gems.expression.indexing_structure import IndexingStructure from gems.model import float_parameter, float_variable, model from gems.simulation import TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) from gems.study.data import TimeScenarioSeriesData from tests.e2e.functional.libs.standard import ( DEMAND_MODEL, @@ -58,21 +66,24 @@ def test_large_sum_inside_model_with_loop() -> None: float_parameter(f"cost_{i}", IndexingStructure(False, False)) for i in range(1, nb_terms) ], - objective_operational_contribution=cast( - ExpressionNode, sum(param(f"cost_{i}") for i in range(1, nb_terms)) - ), + objective_contributions={ + "operational": cast( + ExpressionNode, sum(param(f"cost_{i}") for i in range(1, nb_terms)) + ) + }, ) - network = Network("test") + system = System("test") cost_model = create_component(model=SIMPLE_COST_MODEL, id="simple_cost") - network.add_component(cost_model) - - problem = build_problem(network, database, time_blocks[0], scenarios) - status = problem.solver.Solve() + system.add_component(cost_model) - assert status == problem.solver.OPTIMAL + problem = build_problem( + Study(system, database), time_blocks[0], list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" assert math.isclose( - problem.solver.Objective().Value(), sum([1 / i for i in range(1, nb_terms)]) + problem.objective_value, sum([1 / i for i in range(1, nb_terms)]) ) @@ -92,22 +103,23 @@ def test_large_sum_outside_model_with_loop() -> None: SIMPLE_COST_MODEL = model( id="SIMPLE_COST", parameters=[], - objective_operational_contribution=literal(obj_coeff), + objective_contributions={"operational": literal(obj_coeff)}, ) - network = Network("test") + system = System("test") simple_model = create_component( model=SIMPLE_COST_MODEL, id="simple_cost", ) - network.add_component(simple_model) - - problem = build_problem(network, database, time_blocks[0], scenarios) - status = problem.solver.Solve() + system.add_component(simple_model) - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == obj_coeff + problem = build_problem( + Study(system, database), time_blocks[0], list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == obj_coeff # Takes 3 minutes with current implementation !! @@ -141,19 +153,22 @@ def test_large_sum_inside_model_with_sum_operator() -> None: structure=IndexingStructure(True, False), ), ], - objective_operational_contribution=(param("cost") * var("var")).time_sum(), + objective_contributions={ + "operational": (param("cost") * var("var")).time_sum() + }, ) - network = Network("test") + system = System("test") cost_model = create_component(model=SIMPLE_COST_MODEL, id="simple_cost") - network.add_component(cost_model) - - problem = build_problem(network, database, time_blocks[0], scenarios) - status = problem.solver.Solve() + system.add_component(cost_model) - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3 * nb_terms + problem = build_problem( + Study(system, database), time_blocks[0], list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3 * nb_terms def test_large_sum_of_port_connections() -> None: @@ -173,31 +188,29 @@ def test_large_sum_of_port_connections() -> None: database.add_data(f"G_{gen_id}", "p_max", ConstantData(1)) database.add_data(f"G_{gen_id}", "cost", ConstantData(5)) - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component(model=DEMAND_MODEL, id="D") generators = [ create_component(model=GENERATOR_MODEL, id=f"G_{gen_id}") for gen_id in range(nb_generators) ] - network = Network("test") - network.add_node(node) + system = System("test") + system.add_component(node) - network.add_component(demand) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.add_component(demand) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) for gen_id in range(nb_generators): - network.add_component(generators[gen_id]) - network.connect( + system.add_component(generators[gen_id]) + system.connect( PortRef(generators[gen_id], "balance_port"), PortRef(node, "balance_port") ) - problem = build_problem(network, database, time_block, scenarios) - - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 5 * nb_generators + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 5 * nb_generators def test_basic_balance_on_whole_year() -> None: @@ -217,25 +230,26 @@ def test_basic_balance_on_whole_year() -> None: database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component(model=DEMAND_MODEL, id="D") gen = create_component(model=GENERATOR_MODEL, id="G") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) with cProfile.Profile() as pr: - problem = build_problem(network, database, time_block, scenarios) + problem = build_problem( + Study(system, database), time_block, list(range(scenarios)) + ) pr.print_stats(sort=SortKey.CUMULATIVE) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 30 * 100 * horizon + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 30 * 100 * horizon def test_basic_balance_on_whole_year_with_large_sum() -> None: @@ -256,21 +270,20 @@ def test_basic_balance_on_whole_year_with_large_sum() -> None: database.add_data("G", "cost", ConstantData(30)) database.add_data("G", "full_storage", ConstantData(100 * horizon)) - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component(model=DEMAND_MODEL, id="D") gen = create_component( model=GENERATOR_MODEL_WITH_STORAGE, id="G" ) # Limits the total generation inside a TimeBlock - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) - - problem = build_problem(network, database, time_block, scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 30 * 100 * horizon + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 30 * 100 * horizon diff --git a/tests/e2e/functional/test_rolling_horizon_suboptimality.py b/tests/e2e/functional/test_rolling_horizon_suboptimality.py new file mode 100644 index 00000000..65beca20 --- /dev/null +++ b/tests/e2e/functional/test_rolling_horizon_suboptimality.py @@ -0,0 +1,199 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +E2E test: rolling-horizon suboptimality and sequential carry-over (Issue #102). + +System: 1 generator (p_max=2, gen_cost=1) + 1 load (oscillating demand) + + 1 storage (capacity=2, max_rate=2, η=1, drop-mode SoC balance) + + 1 bus (power balance with unserved-energy slack, ens_cost=100). + +6 time steps (t=0..5): + demand = [0, 4, 0, 4, 0, 4] (zero at even steps, peak=4 at odd steps) + +The generator alone (p_max=2) cannot cover the full peak demand of 4; the +storage must pre-charge during zero-demand steps so it can contribute 2 units +at each peak step. Any demand not covered by gen+storage is handled by the +`unsupplied` variable in the bus at a penalty cost of 100/unit. + +Sequential mode: block_length=3, block_overlap=1 → blocks [0,1,2], [2,3,4], [4,5]. + +─── Frontal optimal (total objective = 10) ─────────────────────────────────── +With drop mode, SoC[t=0] is a free variable; the optimizer sets it to 2 at no +cost. Zero-demand even steps are used to recharge the storage; peak odd steps +are served by gen=2 + discharge=2. No unserved energy. + + t=0: gen=0, discharge=0, charge=0, SoC=2 (free initial state) + t=1: gen=2, discharge=2, charge=0, SoC=0 cost = 2 + t=2: gen=2, discharge=0, charge=2, SoC=2 cost = 2 + t=3: gen=2, discharge=2, charge=0, SoC=0 cost = 2 + t=4: gen=2, discharge=0, charge=2, SoC=2 cost = 2 + t=5: gen=2, discharge=2, charge=0, SoC=0 cost = 2 + Total generation cost = 10; unserved = 0 → total objective = 10 + +─── Sequential suboptimality (sum of block objectives = 406) ───────────────── +Block 1 [0,1,2]: SoC[0]=2 free; serves t=1 with gen=2+discharge=2 (cost=2). + t=2 is the look-ahead step with demand=0. Within the window no future peak + is visible, so the optimizer does not recharge → SoC[t=2]=0. + → carry-over: SoC[t=2] = 0. Block objective = 2. + +Block 2 [2,3,4]: SoC[t=2]=0 fixed by carry-over. + t=3: peak demand=4 but SoC=0 → gen=2, unsupplied=2. Cost = 2 + 200 = 202. + t=4 (look-ahead, demand=0): no recharge incentive → SoC[t=4]=0. + → carry-over: SoC[t=4] = 0. Block objective = 202. + +Block 3 [4,5]: SoC[t=4]=0 fixed by carry-over. + t=5: peak demand=4, same situation → gen=2, unsupplied=2. Cost = 2 + 200 = 202. + Block objective = 202. + + Sum of block objectives = 2 + 202 + 202 = 406. +""" + +import shutil +import textwrap +from pathlib import Path + +import pytest + +from gems.study.runner import run_study + +_STUDY_SRC = Path(__file__).parent / "studies" / "rolling_horizon_suboptimality" + +_BASE_CONFIG = textwrap.dedent("""\ + time-scope: + first-time-step: 0 + last-time-step: 5 + solver-options: + name: highs + logs: false + parameters: "" + scenario-scope: + nb-scenarios: 1 + models: + - id: rolling-horizon-lib.storage + out-of-bounds-processing: + constraints: + - id: soc_balance + mode: drop +""") + +_FRONTAL_CONFIG = _BASE_CONFIG + textwrap.dedent("""\ + resolution: + mode: frontal +""") + +_SEQUENTIAL_CONFIG = _BASE_CONFIG + textwrap.dedent("""\ + resolution: + mode: sequential-subproblems + block-length: 3 + block-overlap: 1 +""") + + +def _run_with_config(study_dir: Path, config_yaml: str): + import pandas as pd + + config_path = study_dir / "input" / "optim-config.yml" + config_path.write_text(config_yaml) + run_study(study_dir) + output_files = list((study_dir / "output").glob("**/simulation_table_*.csv")) + assert len(output_files) == 1 + return pd.read_csv(output_files[0]) + + +def _total_objective(raw) -> float: + """Sum all per-block objective-value entries.""" + return float(raw.loc[raw["output"] == "objective-value", "value"].sum()) + + +def _get_value(raw, component: str, output: str, timestep: int) -> float: + mask = ( + (raw["component"] == component) + & (raw["output"] == output) + & (raw["absolute-time-index"] == timestep) + ) + rows = raw[mask] + assert ( + len(rows) >= 1 + ), f"No row for component={component} output={output} t={timestep}" + return float(rows.iloc[0]["value"]) + + +def test_rolling_horizon_suboptimality(tmp_path: Path) -> None: + """Sequential mode is suboptimal vs frontal for a storage+oscillating-load system. + + Frontal objective = 10 (storage pre-charged for free via drop mode; covers + every peak with gen=2 + discharge=2; recharges at cheap zero-demand steps). + + Sequential sum of block objectives = 406 (rolling horizon misses recharge + opportunities at look-ahead steps → empty storage at peaks → 2 units of + unserved energy at t=3 and t=5, each penalised at 100/unit). + + The per-timestep assertions pin down exactly where the carry-over state + diverges between the two modes. + """ + frontal_dir = tmp_path / "frontal" + seq_dir = tmp_path / "sequential" + shutil.copytree(_STUDY_SRC, frontal_dir) + shutil.copytree(_STUDY_SRC, seq_dir) + + frontal_raw = _run_with_config(frontal_dir, _FRONTAL_CONFIG) + seq_raw = _run_with_config(seq_dir, _SEQUENTIAL_CONFIG) + + # ── Objective assertions ────────────────────────────────────────────────── + frontal_obj = _total_objective(frontal_raw) + seq_obj = _total_objective(seq_raw) + + assert frontal_obj == pytest.approx( + 10.0, rel=1e-6 + ), f"Frontal objective should be 10.0, got {frontal_obj}" + assert seq_obj == pytest.approx( + 406.0, rel=1e-6 + ), f"Sequential sum of block objectives should be 406.0, got {seq_obj}" + assert ( + seq_obj > frontal_obj + ), "Sequential mode must be strictly suboptimal vs frontal" + + # ── SoC carry-over assertions ───────────────────────────────────────────── + # Frontal recharges at t=2 and t=4 (zero-demand steps); sequential does not + # because the look-ahead window never reveals the upcoming peak. + assert _get_value(frontal_raw, "storage", "soc", 2) == pytest.approx( + 2.0, rel=1e-6 + ), "Frontal: storage should be fully recharged (SoC=2) at t=2" + assert _get_value(seq_raw, "storage", "soc", 2) == pytest.approx( + 0.0, abs=1e-6 + ), "Sequential: storage should be empty (SoC=0) at t=2 — missed recharge" + + assert _get_value(frontal_raw, "storage", "soc", 4) == pytest.approx( + 2.0, rel=1e-6 + ), "Frontal: storage should be fully recharged (SoC=2) at t=4" + assert _get_value(seq_raw, "storage", "soc", 4) == pytest.approx( + 0.0, abs=1e-6 + ), "Sequential: storage should be empty (SoC=0) at t=4 — missed recharge" + + # ── Unserved energy assertions ──────────────────────────────────────────── + # Frontal has zero unserved energy at every step (storage covers the gap). + # Sequential cannot discharge at t=3 and t=5 (carry-over SoC=0) → 2 units + # of unserved energy at each of those steps. + assert _get_value(frontal_raw, "bus", "unsupplied", 3) == pytest.approx( + 0.0, abs=1e-6 + ), "Frontal: no unserved energy at t=3 (storage discharges)" + assert _get_value(seq_raw, "bus", "unsupplied", 3) == pytest.approx( + 2.0, rel=1e-6 + ), "Sequential: 2 units unserved at t=3 (storage empty after missed recharge)" + + assert _get_value(frontal_raw, "bus", "unsupplied", 5) == pytest.approx( + 0.0, abs=1e-6 + ), "Frontal: no unserved energy at t=5 (storage discharges)" + assert _get_value(seq_raw, "bus", "unsupplied", 5) == pytest.approx( + 2.0, rel=1e-6 + ), "Sequential: 2 units unserved at t=5 (storage empty after missed recharge)" diff --git a/tests/e2e/functional/test_scalability.py b/tests/e2e/functional/test_scalability.py new file mode 100644 index 00000000..40585a4f --- /dev/null +++ b/tests/e2e/functional/test_scalability.py @@ -0,0 +1,80 @@ +import time + +import numpy as np +import pandas as pd + +from gems.simulation import TimeBlock, build_problem +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + TimeScenarioSeriesData, + create_component, +) +from tests.e2e.functional.libs.standard import ( + DEMAND_MODEL, + GENERATOR_MODEL_WITH_STORAGE, + NODE_BALANCE_MODEL, +) + + +def generate_scalar_matrix_data( + value: float, horizon: int, scenarios: int +) -> TimeScenarioSeriesData: + data = pd.DataFrame(value, index=range(horizon), columns=range(scenarios)) + + return TimeScenarioSeriesData(time_scenario_series=data) + + +def test_basic_balance_on_whole_year_with_large_sum() -> None: + """ + Balance on one node with one fixed demand and one generation with storage, on 8760 timestep. + """ + + durations = {} + for horizon in np.logspace(1, 6, num=10): + durations[int(horizon)] = build_for_horizon(int(horizon), 1) + + duration_df = pd.DataFrame.from_dict(durations, orient="index") + duration_df.columns = pd.Index(["build time"]) + print(duration_df) + duration_df.to_csv("build_time_scalability.csv") + + +def build_for_horizon(horizon_size: int, scenario_count: int) -> float: + scenarios = scenario_count + time_block = TimeBlock(1, list(range(horizon_size))) + database = DataBase() + database.add_data( + "D", "demand", generate_scalar_matrix_data(100, horizon_size, scenarios) + ) + + database.add_data("G", "p_max", ConstantData(100)) + database.add_data("G", "cost", ConstantData(30)) + database.add_data("G", "full_storage", ConstantData(100 * horizon_size)) + + node = Component(model=NODE_BALANCE_MODEL, id="N") + demand = create_component(model=DEMAND_MODEL, id="D") + gen = create_component( + model=GENERATOR_MODEL_WITH_STORAGE, id="G" + ) # Limits the total generation inside a TimeBlock + + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + + start = time.time() + problem = build_problem(Study(system, database), time_block, list(range(scenarios))) + end = time.time() + print(f"Time elapsed for horizon {horizon_size}: {end - start:.4f}") + return end - start + + +if __name__ == "__main__": + test_basic_balance_on_whole_year_with_large_sum() diff --git a/tests/e2e/functional/test_scenario_builder.py b/tests/e2e/functional/test_scenario_builder.py index 31582958..d3991fbc 100644 --- a/tests/e2e/functional/test_scenario_builder.py +++ b/tests/e2e/functional/test_scenario_builder.py @@ -12,37 +12,36 @@ from pathlib import Path -import pandas as pd import pytest from gems.model.parsing import parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.simulation.optimization import build_problem +from gems.simulation import build_problem from gems.simulation.time_block import TimeBlock +from gems.study import Study from gems.study.data import DataBase -from gems.study.parsing import parse_scenario_builder, parse_yaml_components +from gems.study.parsing import parse_yaml_components from gems.study.resolve_components import ( - build_network, - build_scenarized_data_base, + build_data_base, consistency_check, resolve_system, ) +from gems.study.scenario_builder import ScenarioBuilder @pytest.fixture -def scenario_builder(series_dir: Path) -> pd.DataFrame: - buider_path = series_dir / "scenario_builder.csv" - return parse_scenario_builder(buider_path) +def scenario_builder(series_dir: Path) -> ScenarioBuilder: + return ScenarioBuilder.load(series_dir / "scenariobuilder.dat") @pytest.fixture def database( - series_dir: Path, systems_dir: Path, scenario_builder: pd.DataFrame + series_dir: Path, systems_dir: Path, scenario_builder: ScenarioBuilder ) -> DataBase: system_path = systems_dir / "with_scenarization.yml" with system_path.open() as components: - return build_scenarized_data_base( - parse_yaml_components(components), scenario_builder, series_dir + return build_data_base( + parse_yaml_components(components), series_dir, scenario_builder ) @@ -59,14 +58,11 @@ def test_system_with_scenarization( yaml_comp = parse_yaml_components(file) components = resolve_system(yaml_comp, lib_dict) - consistency_check(components.components, lib_dict["basic"].models) - network = build_network(components) + consistency_check(components, lib_dict["basic"].models) timeblock = TimeBlock(1, list(range(2))) - problem = build_problem(network, database, timeblock, 3) + problem = build_problem(Study(components, database), timeblock, list(range(3))) - status = problem.solver.Solve() - cost = problem.solver.Objective().Value() - - assert status == 0 - assert cost == pytest.approx(40000 / 3, abs=0.001) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(40000 / 3, abs=0.001) diff --git a/tests/e2e/functional/test_simtable_timeblock.py b/tests/e2e/functional/test_simtable_timeblock.py new file mode 100644 index 00000000..c4a4822c --- /dev/null +++ b/tests/e2e/functional/test_simtable_timeblock.py @@ -0,0 +1,128 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Tests for SimulationTable correctness when the optimization TimeBlock does not +cover the full data horizon. + +Test: `test_simtable_on_partial_timeblock` + - System: 3 components — node, generator (pmax=200), demand (timevarying: demand[t]=t) + - Data horizon: 150 timesteps + - TimeBlock: [40, 90) → 50 timesteps (indices 40–89) + - Checks that SimulationTable absolute-time-index, block-time-index, and + generation values are all consistent with the partial block. +""" + +import pandas as pd +import pytest + +from gems.model.library import Library +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationColumns, SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + TimeScenarioSeriesData, + create_component, +) + +HORIZON = 150 +BLOCK_START = 40 +BLOCK_END = 90 # half-open: timesteps 40–89 + + +def test_simtable_on_partial_timeblock(lib_dict_unittest: dict[str, Library]) -> None: + """ + Verify that SimulationTable reflects absolute time indices [40, 90) when the + TimeBlock covers only a slice of a 150-timestep data horizon. + + With demand[t] = t and pmax = 200, the optimizer sets generation[t] = t at + every timestep, making all three assertions self-consistent: + 1. generation values equal their absolute timestep index. + 2. absolute-time-index runs from 40 to 89 (not 0–149 or 0–49). + 3. block-time-index runs from 0 to 49 and equals absolute-time-index − 40. + """ + node_model = lib_dict_unittest["basic"].models["basic.node"] + generator_model = lib_dict_unittest["basic"].models["basic.generator"] + demand_model = lib_dict_unittest["basic"].models["basic.demand"] + + # demand[t] = t for all 150 timesteps + demand_data = pd.DataFrame( + list(range(HORIZON)), + index=range(HORIZON), + columns=[0], + ) + + database = DataBase() + database.add_data("G", "p_max", ConstantData(200)) + database.add_data("G", "cost", ConstantData(1)) + database.add_data("D", "demand", TimeScenarioSeriesData(demand_data)) + + node = Component(model=node_model, id="N") + gen = create_component(model=generator_model, id="G") + demand = create_component(model=demand_model, id="D") + + system = System("test") + system.add_component(node) + system.add_component(gen) + system.add_component(demand) + system.connect(PortRef(gen, "injection_port"), PortRef(node, "injection_port")) + system.connect(PortRef(demand, "injection_port"), PortRef(node, "injection_port")) + + time_block = TimeBlock(1, list(range(BLOCK_START, BLOCK_END))) + + problem = build_problem(Study(system, database), time_block, [0]) + problem.solve(solver_name="highs") + + assert problem.termination_condition == "optimal" + + # cost=1, generation[t]=t for t in [40, 90) → sum(40..89) = 3225 + assert problem.objective_value == pytest.approx(3225) + + sim_table = SimulationTableBuilder().build(problem) + + # Check 1: fluent API — generation values equal absolute timestep index + gen_series = sim_table.component("G").output("generation").value(scenario_index=0) + expected = pd.Series( + data=[float(t) for t in range(BLOCK_START, BLOCK_END)], + index=pd.Index( + range(BLOCK_START, BLOCK_END), + name=SimulationColumns.ABSOLUTE_TIME_INDEX.value, + ), + name=0, + ) + pd.testing.assert_series_equal(gen_series, expected, check_dtype=False) + + # Check 2: raw DataFrame — both index columns are correct and consistently related + gen_rows = sim_table.data[ + (sim_table.data[SimulationColumns.COMPONENT.value] == "G") + & (sim_table.data[SimulationColumns.OUTPUT.value] == "generation") + ].copy() + + abs_times = sorted( + gen_rows[SimulationColumns.ABSOLUTE_TIME_INDEX.value].astype(int) + ) + block_times = sorted(gen_rows[SimulationColumns.BLOCK_TIME_INDEX.value].astype(int)) + + assert abs_times == list(range(BLOCK_START, BLOCK_END)) + assert block_times == list(range(BLOCK_END - BLOCK_START)) + + # absolute-time-index = block-time-index + BLOCK_START for every row + offset = ( + gen_rows[SimulationColumns.ABSOLUTE_TIME_INDEX.value].astype(int) + - gen_rows[SimulationColumns.BLOCK_TIME_INDEX.value].astype(int) + ).unique() + assert list(offset) == [BLOCK_START] diff --git a/tests/e2e/functional/test_stochastic.py b/tests/e2e/functional/test_stochastic.py index 008ae67c..f4c993ab 100644 --- a/tests/e2e/functional/test_stochastic.py +++ b/tests/e2e/functional/test_stochastic.py @@ -16,7 +16,15 @@ import pytest from gems.simulation import TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) from gems.study.data import TimeScenarioSeriesData from tests.e2e.functional.libs.standard import ( DEMAND_MODEL, @@ -104,7 +112,7 @@ def test_stochastic_model_with_HD_for_thermal_startup( Randomness only comes from the availability of thermal plants, demand is fixed. """ - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component(model=DEMAND_MODEL, id="D") base = create_component(model=THERMAL_CLUSTER_MODEL_HD, id="BASE") @@ -113,23 +121,22 @@ def test_stochastic_model_with_HD_for_thermal_startup( peak = create_component(model=THERMAL_CLUSTER_MODEL_HD, id="PEAK") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(base) - network.add_component(semibase) - network.add_component(peak) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(base, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(semibase, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(base) + system.add_component(semibase) + system.add_component(peak) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(base, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(semibase, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port")) for block in time_blocks: # TODO : To manage blocks simply for now - problem = build_problem(network, database, block, scenarios) - status = problem.solver.Solve() - + problem = build_problem(Study(system, database), block, list(range(scenarios))) + problem.solve(solver_name="highs") assert ( - status == problem.solver.OPTIMAL + problem.termination_condition == "optimal" ) # Tester qu'on trouve bien la solution optimale # Generation, nb_on, nb_start, nb_stop for each of 3 thermal clusters @@ -145,16 +152,7 @@ def test_stochastic_model_with_HD_for_thermal_startup( nb_scenario_varying_constraint = 1 + 5 * 3 # TODO this test should pass with the next port implementation - assert ( - problem.solver.NumVariables() - == nb_anticipative_time_varying_var * horizon * scenarios - + nb_non_anticipative_time_varying_var * horizon - ) - assert ( - problem.solver.NumConstraints() - == nb_scenario_constant_constraint * horizon - + nb_scenario_varying_constraint * horizon * scenarios - ) + # TODO: update variable count checks (NumVariables/NumConstraints not available in linopy API) def test_stochastic_model_with_DH_for_thermal_startup( @@ -170,7 +168,7 @@ def test_stochastic_model_with_DH_for_thermal_startup( time_blocks = [TimeBlock(1, list(range(horizon)))] - node = Node(model=NODE_BALANCE_MODEL, id="N") + node = Component(model=NODE_BALANCE_MODEL, id="N") demand = create_component(model=DEMAND_MODEL, id="D") base = create_component(model=THERMAL_CLUSTER_MODEL_DHD, id="BASE") @@ -179,23 +177,22 @@ def test_stochastic_model_with_DH_for_thermal_startup( peak = create_component(model=THERMAL_CLUSTER_MODEL_DHD, id="PEAK") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(base) - network.add_component(semibase) - network.add_component(peak) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(base, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(semibase, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(base) + system.add_component(semibase) + system.add_component(peak) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(base, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(semibase, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(peak, "balance_port"), PortRef(node, "balance_port")) for block in time_blocks: # TODO : To manage blocks simply for now - problem = build_problem(network, database, block, scenarios) - status = problem.solver.Solve() - + problem = build_problem(Study(system, database), block, list(range(scenarios))) + problem.solve(solver_name="highs") assert ( - status == problem.solver.OPTIMAL + problem.termination_condition == "optimal" ) # Tester qu'on trouve bien la solution optimale # Generation for each of 3 thermal clusters @@ -210,13 +207,4 @@ def test_stochastic_model_with_DH_for_thermal_startup( # Balance constraint + For each 3 thermal clusters : Max generation, Min generation nb_scenario_varying_constraint = 1 + 2 * 3 - assert ( - problem.solver.NumVariables() - == nb_anticipative_time_varying_var * horizon * scenarios - + nb_non_anticipative_time_varying_var * horizon - ) - assert ( - problem.solver.NumConstraints() - == nb_scenario_constant_constraint * horizon - + nb_scenario_varying_constraint * horizon * scenarios - ) + # TODO: update variable count checks (NumVariables/NumConstraints not available in linopy API) diff --git a/tests/e2e/functional/test_study_from_folder.py b/tests/e2e/functional/test_study_from_folder.py new file mode 100644 index 00000000..73f34445 --- /dev/null +++ b/tests/e2e/functional/test_study_from_folder.py @@ -0,0 +1,29 @@ +import shutil +from pathlib import Path + +import pandas as pd + +from gems.study.folder import load_study +from gems.study.runner import run_study + + +def test_load_study(): + study_dir = Path(__file__).parent / "studies" / "7_4" + + study = load_study(study_dir) + assert len(study.system.components) == 12 + assert len(study.system.connections) == 11 + assert len(study.database._data) == 76 + + +def test_run_study(tmp_path: Path) -> None: + # Copy study to tmp_path so output files don't pollute the source tree. + study_dir = tmp_path / "7_4" + shutil.copytree(Path(__file__).parent / "studies" / "7_4", study_dir) + + run_study(study_dir) + + output_files = list((study_dir / "output").glob("**/simulation_table_*.csv")) + assert len(output_files) == 1 + df = pd.read_csv(output_files[0]) + assert "objective-value" in df["output"].values diff --git a/tests/e2e/integration/libs/standard.py b/tests/e2e/integration/libs/standard.py deleted file mode 100644 index a8f6cd7f..00000000 --- a/tests/e2e/integration/libs/standard.py +++ /dev/null @@ -1,495 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -""" -The standard module contains the definition of standard models. -""" - -from gems.expression import literal, param, var -from gems.expression.expression import port_field -from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext -from gems.model.constraint import Constraint -from gems.model.model import ModelPort, model -from gems.model.parameter import float_parameter, int_parameter -from gems.model.port import PortField, PortFieldDefinition, PortFieldId, PortType -from gems.model.variable import float_variable, int_variable - -CONSTANT = IndexingStructure(False, False) -TIME_AND_SCENARIO_FREE = IndexingStructure(True, True) -ANTICIPATIVE_TIME_VARYING = IndexingStructure(True, True) -NON_ANTICIPATIVE_TIME_VARYING = IndexingStructure(True, False) - -BALANCE_PORT_TYPE = PortType(id="balance", fields=[PortField("flow")]) - -NODE_BALANCE_MODEL = model( - id="NODE_BALANCE_MODEL", - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - binding_constraints=[ - Constraint( - name="Balance", - expression=port_field("balance_port", "flow").sum_connections() - == literal(0), - ) - ], -) - -NODE_WITH_SPILL_AND_ENS = model( - id="NODE_WITH_SPILL_AND_ENS_MODEL", - parameters=[float_parameter("spillage_cost"), float_parameter("ens_cost")], - variables=[ - float_variable("spillage", lower_bound=literal(0)), - float_variable("unsupplied_energy", lower_bound=literal(0)), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - binding_constraints=[ - Constraint( - name="Balance", - expression=port_field("balance_port", "flow").sum_connections() - == var("spillage") - var("unsupplied_energy"), - ) - ], - objective_operational_contribution=( - param("spillage_cost") * var("spillage") - + param("ens_cost") * var("unsupplied_energy") - ) - .time_sum() - .expec(), -) - -""" -Basic link model using ports -""" -LINK_MODEL = model( - id="LINK", - parameters=[float_parameter("f_max", TIME_AND_SCENARIO_FREE)], - variables=[ - float_variable("flow", lower_bound=-param("f_max"), upper_bound=param("f_max")) - ], - ports=[ - ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port_from"), - ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port_to"), - ], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port_from", "flow"), - definition=-var("flow"), - ), - PortFieldDefinition( - port_field=PortFieldId("balance_port_to", "flow"), - definition=var("flow"), - ), - ], -) - -""" -A standard model for a fixed demand of energy. -""" -DEMAND_MODEL = model( - id="FIXED_DEMAND", - parameters=[ - float_parameter("demand", TIME_AND_SCENARIO_FREE), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=-param("demand"), - ) - ], -) - -""" -A standard model for a linear cost generation, limited by a maximum generation. -""" -GENERATOR_MODEL = model( - id="GEN", - parameters=[ - float_parameter("p_max", CONSTANT), - float_parameter("cost", CONSTANT), - ], - variables=[float_variable("generation", lower_bound=literal(0))], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", expression=var("generation") <= param("p_max") - ), - ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), -) - -GENERATOR_MODEL_WITH_PMIN = model( - id="GEN", - parameters=[ - float_parameter("p_max", CONSTANT), - float_parameter("p_min", CONSTANT), - float_parameter("cost", CONSTANT), - ], - variables=[float_variable("generation")], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", expression=var("generation") <= param("p_max") - ), - Constraint( - name="Min generation", - expression=var("generation") - param("p_min"), - lower_bound=literal(0), - ), # To test both ways of setting constraints - ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), -) - -""" -A model for a linear cost generation limited by a maximum generation per time-step -and total generation in whole period. It considers a full storage with no replenishing -""" -GENERATOR_MODEL_WITH_STORAGE = model( - id="GEN", - parameters=[ - float_parameter("p_max", CONSTANT), - float_parameter("cost", CONSTANT), - float_parameter("full_storage", CONSTANT), - ], - variables=[float_variable("generation", lower_bound=literal(0))], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", expression=var("generation") <= param("p_max") - ), - Constraint( - name="Total storage", - expression=var("generation").time_sum() <= param("full_storage"), - ), - ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), -) - -# For now, no starting cost -THERMAL_CLUSTER_MODEL_HD = model( - id="GEN", - parameters=[ - float_parameter("p_max", CONSTANT), # p_max of a single unit - float_parameter("p_min", CONSTANT), - float_parameter("d_min_up", CONSTANT), - float_parameter("d_min_down", CONSTANT), - float_parameter("cost", CONSTANT), - int_parameter("nb_units_max", CONSTANT), - int_parameter("nb_failures", TIME_AND_SCENARIO_FREE), - ], - variables=[ - float_variable( - "generation", - lower_bound=literal(0), - upper_bound=param("nb_units_max") * param("p_max"), - structure=ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_on", - lower_bound=literal(0), - upper_bound=param("nb_units_max"), - structure=ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_stop", - lower_bound=literal(0), - structure=ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_start", - lower_bound=literal(0), - structure=ANTICIPATIVE_TIME_VARYING, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - "Max generation", - var("generation") <= param("p_max") * var("nb_on"), - ), - Constraint( - "Min generation", - var("generation") >= param("p_min") * var("nb_on"), - ), - Constraint( - "NODU balance", - var("nb_on") == var("nb_on").shift(-1) + var("nb_start") - var("nb_stop"), - ), - Constraint( - "Min up time", - var("nb_start").time_sum(-param("d_min_up") + 1, literal(0)) - <= var("nb_on"), - ), - Constraint( - "Min down time", - var("nb_stop").time_sum(-param("d_min_down") + 1, literal(0)) - <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), - ), - ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), -) - -# Same model as previous one, except that starting/stopping variables are now non anticipative -THERMAL_CLUSTER_MODEL_DHD = model( - id="GEN", - parameters=[ - float_parameter("p_max", CONSTANT), # p_max of a single unit - float_parameter("p_min", CONSTANT), - float_parameter("d_min_up", CONSTANT), - float_parameter("d_min_down", CONSTANT), - float_parameter("cost", CONSTANT), - int_parameter("nb_units_max", CONSTANT), - int_parameter("nb_failures", TIME_AND_SCENARIO_FREE), - ], - variables=[ - float_variable( - "generation", - lower_bound=literal(0), - upper_bound=param("nb_units_max") * param("p_max"), - structure=ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_on", - lower_bound=literal(0), - upper_bound=param("nb_units_max"), - structure=NON_ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_stop", - lower_bound=literal(0), - structure=NON_ANTICIPATIVE_TIME_VARYING, - ), - int_variable( - "nb_start", - lower_bound=literal(0), - structure=NON_ANTICIPATIVE_TIME_VARYING, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - "Max generation", - var("generation") <= param("p_max") * var("nb_on"), - ), - Constraint( - "Min generation", - var("generation") >= param("p_min") * var("nb_on"), - ), - Constraint( - "NODU balance", - var("nb_on") == var("nb_on").shift(-1) + var("nb_start") - var("nb_stop"), - ), - Constraint( - "Min up time", - var("nb_start").time_sum(-param("d_min_up") + 1, literal(0)) - <= var("nb_on"), - ), - Constraint( - "Min down time", - var("nb_stop").time_sum(-param("d_min_down") + 1, literal(0)) - <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), - ), - ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), -) - -SPILLAGE_MODEL = model( - id="SPI", - parameters=[float_parameter("cost", CONSTANT)], - variables=[float_variable("spillage", lower_bound=literal(0))], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=-var("spillage"), - ) - ], - objective_operational_contribution=(param("cost") * var("spillage")) - .time_sum() - .expec(), -) - -UNSUPPLIED_ENERGY_MODEL = model( - id="UNSP", - parameters=[float_parameter("cost", CONSTANT)], - variables=[float_variable("unsupplied_energy", lower_bound=literal(0))], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("unsupplied_energy"), - ) - ], - objective_operational_contribution=(param("cost") * var("unsupplied_energy")) - .time_sum() - .expec(), -) - -# Simplified model -# - In antares-solver, some constraints have modulation we decide not to include it here for the sake of simplicity. -# - No capacity (level max) -# - The initial level is not fixed (it is optimized) -SHORT_TERM_STORAGE_SIMPLE = model( - id="STS_SIMPLE", - parameters=[ - float_parameter("p_max_injection"), - float_parameter("p_max_withdrawal"), - float_parameter("level_min"), - float_parameter("level_max"), - float_parameter("inflows"), - float_parameter( - "efficiency" - ), # Should be constant, but time-dependent values should work as well - ], - variables=[ - float_variable( - "injection", lower_bound=literal(0), upper_bound=param("p_max_injection") - ), - float_variable( - "withdrawal", lower_bound=literal(0), upper_bound=param("p_max_withdrawal") - ), - float_variable( - "level", lower_bound=param("level_min"), upper_bound=param("level_max") - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("withdrawal") - var("injection"), - ) - ], - constraints=[ - Constraint( - name="Level", - expression=var("level") - - var("level").shift(-1) - - param("efficiency") * var("injection") - + var("withdrawal") - == param("inflows"), - ), - ], - objective_operational_contribution=literal(0), # Implcitement nul ? -) - -""" Simple thermal unit that can be invested on""" -THERMAL_CANDIDATE = model( - id="GEN", - parameters=[ - float_parameter("op_cost", CONSTANT), - float_parameter("invest_cost", CONSTANT), - float_parameter("max_invest", CONSTANT), - ], - variables=[ - float_variable("generation", lower_bound=literal(0)), - float_variable( - "p_max", - lower_bound=literal(0), - upper_bound=param("max_invest"), - structure=CONSTANT, - context=ProblemContext.COUPLING, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint(name="Max generation", expression=var("generation") <= var("p_max")) - ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), -) - -""" Simple thermal unit that can be invested on and with already installed capacity""" -THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA = model( - id="GEN", - parameters=[ - float_parameter("op_cost", CONSTANT), - float_parameter("invest_cost", CONSTANT), - float_parameter("max_invest", CONSTANT), - float_parameter("already_installed_capa", CONSTANT), - ], - variables=[ - float_variable("generation", lower_bound=literal(0)), - float_variable( - "invested_capa", - lower_bound=literal(0), - upper_bound=param("max_invest"), - structure=CONSTANT, - context=ProblemContext.COUPLING, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", - expression=var("generation") - <= param("already_installed_capa") + var("invested_capa"), - ) - ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("invested_capa"), -) diff --git a/tests/e2e/integration/test_benders_decomposed.py b/tests/e2e/integration/test_benders_decomposed.py deleted file mode 100644 index 2ac3049c..00000000 --- a/tests/e2e/integration/test_benders_decomposed.py +++ /dev/null @@ -1,610 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -import pandas as pd -import pytest - -from gems.expression.expression import literal, param, var -from gems.model import ( - Constraint, - Model, - ModelPort, - ProblemContext, - float_parameter, - float_variable, - model, -) -from gems.model.port import PortFieldDefinition, PortFieldId -from gems.simulation import BendersSolution, TimeBlock, build_benders_decomposed_problem -from gems.simulation.decision_tree import ( - DecisionTreeNode, - InterDecisionTimeScenarioConfig, -) -from gems.study import ( - Component, - ConstantData, - DataBase, - Network, - Node, - PortRef, - ScenarioIndex, - ScenarioSeriesData, - TimeIndex, - TimeScenarioSeriesData, - TimeSeriesData, - create_component, -) -from tests.e2e.integration.libs.standard import ( - BALANCE_PORT_TYPE, - CONSTANT, - DEMAND_MODEL, - GENERATOR_MODEL, - NODE_WITH_SPILL_AND_ENS, -) - -INVESTMENT = ProblemContext.INVESTMENT -OPERATIONAL = ProblemContext.OPERATIONAL -COUPLING = ProblemContext.COUPLING - - -@pytest.fixture -def thermal_candidate() -> Model: - THERMAL_CANDIDATE = model( - id="GEN", - parameters=[ - float_parameter("op_cost", CONSTANT), - float_parameter("invest_cost", CONSTANT), - ], - variables=[ - float_variable("generation", lower_bound=literal(0)), - float_variable( - "p_max", - lower_bound=literal(0), - upper_bound=literal(1000), - structure=CONSTANT, - context=COUPLING, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", expression=var("generation") <= var("p_max") - ) - ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), - ) - return THERMAL_CANDIDATE - - -@pytest.fixture -def discrete_candidate() -> Model: - DISCRETE_CANDIDATE = model( - id="DISCRETE", - parameters=[ - float_parameter("op_cost", CONSTANT), - float_parameter("invest_cost", CONSTANT), - float_parameter("p_max_per_unit", CONSTANT), - ], - variables=[ - float_variable("generation", lower_bound=literal(0)), - float_variable( - "p_max", - lower_bound=literal(0), - structure=CONSTANT, - context=COUPLING, - ), - # TODO set it back to int_variable - float_variable( - "nb_units", - lower_bound=literal(0), - upper_bound=literal(10), - structure=CONSTANT, - context=INVESTMENT, - ), - ], - ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], - port_fields_definitions=[ - PortFieldDefinition( - port_field=PortFieldId("balance_port", "flow"), - definition=var("generation"), - ) - ], - constraints=[ - Constraint( - name="Max generation", expression=var("generation") <= var("p_max") - ), - Constraint( - name="Max investment", - expression=var("p_max") == param("p_max_per_unit") * var("nb_units"), - context=INVESTMENT, - ), - ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), - ) - return DISCRETE_CANDIDATE - - -@pytest.fixture -def generator() -> Component: - generator = create_component( - model=GENERATOR_MODEL, - id="G1", - ) - return generator - - -@pytest.fixture -def candidate(thermal_candidate: Model) -> Component: - candidate = create_component(model=thermal_candidate, id="CAND") - return candidate - - -@pytest.fixture -def cluster_candidate(discrete_candidate: Model) -> Component: - cluster = create_component(model=discrete_candidate, id="DISCRETE") - return cluster - - -def test_benders_decomposed_one_candidate( - generator: Component, - candidate: Component, -) -> None: - """ - Study case 13_1 : to test investment problems - Simple generation expansion problem on one node, one timestep and one scenario with one candidate. - - Demand = 400 - Generator : P_max : 200, Cost : 45 - Unsupplied energy : Cost : 501 - - -> 200 of unsupplied energy - -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 - - Continuous candidate : Invest cost : 400 / MW; Prod cost : 10 - - Optimal investment : 200 MW - - -> Optimal cost = 400 * 200 + 10 * 200 (Invest cost + prod cost of new generator) - + 45 * 200 (Generator) - = 80_000 + 11_000 - = 91_000 - """ - - database = DataBase() - database.add_data("D", "demand", ConstantData(400)) - - database.add_data("N", "spillage_cost", ConstantData(1)) - database.add_data("N", "ens_cost", ConstantData(501)) - - database.add_data("G1", "p_max", ConstantData(200)) - database.add_data("G1", "cost", ConstantData(45)) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "invest_cost", ConstantData(400)) - - demand = create_component(model=DEMAND_MODEL, id="D") - - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - - scenarios = 1 - blocks = [TimeBlock(1, [0])] - - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - decision_tree_root = DecisionTreeNode("", config, network) - - xpansion = build_benders_decomposed_problem(decision_tree_root, database) - - data = { - "solution": { - "overall_cost": 91_000, - "values": { - "CAND.p_max": 200, - }, - } - } - solution = BendersSolution(data) - - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" - - assert xpansion.run(should_merge=True) - merged_solution = xpansion.solution - if merged_solution is not None: # For mypy only - assert merged_solution.is_close( - solution - ), f"Solution differs from expected: {merged_solution}" - - -def test_benders_decomposed_with_discrete_candidate( - generator: Component, - candidate: Component, - cluster_candidate: Component, -) -> None: - """ - Study case 13_2 : to test investment problems - Simple generation expansion problem on one node, one timestep and one scenario - but this time with two candidates: one continuous and one discrete. - We separate master/subproblem and export the problems in MPS format to be solved by the Benders and MergeMPS - - Demand = 400 - Generator : P_max : 200, Cost : 45 - Unsupplied energy : Cost : 501 - - -> 200 of unsupplied energy - -> Total cost without investment = 45 * 200 + 501 * 200 = 109_200 - - Continuos candidate : Invest cost : 490 / MW; Prod cost : 10 - Discrete candidate : Invest cost : 200 / MW; Prod cost : 10; Nb of units: 10; Prod per unit: 10 - - Optimal investment : 100 MW (Discrete) + 100 MW (Continuos) - - -> Optimal cost = 490 * 100 + 10 * 100 (Continuos) - + 200 * 100 + 10 * 100 (Discrete) - + 45 * 200 (Generator) - = 69_000 + 11_000 - = 80_000 - """ - - database = DataBase() - database.add_data("D", "demand", ConstantData(400)) - - database.add_data("N", "spillage_cost", ConstantData(1)) - database.add_data("N", "ens_cost", ConstantData(501)) - - database.add_data("G1", "p_max", ConstantData(200)) - database.add_data("G1", "cost", ConstantData(45)) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "invest_cost", ConstantData(490)) - - database.add_data("DISCRETE", "op_cost", ConstantData(10)) - database.add_data("DISCRETE", "invest_cost", ConstantData(200)) - database.add_data("DISCRETE", "p_max_per_unit", ConstantData(10)) - - demand = create_component(model=DEMAND_MODEL, id="D") - - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.add_component(cluster_candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - network.connect( - PortRef(cluster_candidate, "balance_port"), PortRef(node, "balance_port") - ) - scenarios = 1 - blocks = [TimeBlock(1, [0])] - - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - decision_tree_root = DecisionTreeNode("", config, network) - - xpansion = build_benders_decomposed_problem(decision_tree_root, database) - - data = { - "solution": { - "overall_cost": 80_000, - "values": { - "CAND.p_max": 100, - "DISCRETE.p_max": 100, - "DISCRETE.nb_units": 10, - }, - } - } - solution = BendersSolution(data) - - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" - - assert xpansion.run(should_merge=True) - merged_solution = xpansion.solution - if merged_solution is not None: # For mypy only - assert merged_solution.is_close( - solution - ), f"Solution differs from expected: {merged_solution}" - - -def test_benders_decomposed_multi_time_block_single_scenario( - generator: Component, - candidate: Component, -) -> None: - """ - Simple generation xpansion problem on one node. Two time blocks with one timestep each, - one scenario, one thermal cluster candidate. - - Demand = [200, 300] - Generator : P_max : 200, Cost : 40 - Unsupplied energy : Cost : 501 - - -> [0, 100] of unsupplied energy - -> Total cost without investment = (200 * 40) + (200 * 40 + 100 * 501) = 66_100 - - Candidate : Invest cost : 480 / MW, Prod cost : 10 - - Optimal investment : 100 MW - - -> Optimal cost = 480 * 100 (investment) - + 10 * 100 + 40 * 100 (operational - time block 1) - + 10 * 100 + 40 * 200 (operational - time block 2) - = 62_000 - - """ - - data = {} - data[TimeIndex(0)] = 200.0 - data[TimeIndex(1)] = 300.0 - - demand_data = TimeSeriesData(time_series=data) - - database = DataBase() - database.add_data("D", "demand", demand_data) - - database.add_data("N", "spillage_cost", ConstantData(1)) - database.add_data("N", "ens_cost", ConstantData(501)) - - database.add_data("G1", "p_max", ConstantData(200)) - database.add_data("G1", "cost", ConstantData(40)) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "invest_cost", ConstantData(480)) - - demand = create_component( - model=DEMAND_MODEL, - id="D", - ) - - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - - scenarios = 1 - blocks = [TimeBlock(1, [0]), TimeBlock(2, [1])] - - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - decision_tree_root = DecisionTreeNode("", config, network) - - xpansion = build_benders_decomposed_problem(decision_tree_root, database) - - data_output = { - "solution": { - "overall_cost": 62_000, - "values": { - "CAND.p_max": 100, - }, - } - } - solution = BendersSolution(data_output) - - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" - - -def test_benders_decomposed_single_time_block_multi_scenario( - generator: Component, - candidate: Component, -) -> None: - """ - Simple generation xpansion problem on one node. One time block with one timestep each, - two scenarios, one thermal cluster candidate. - - Demand = [200; 300] - Generator : P_max : 200, Cost : 40 - Unsupplied energy : Cost : 1_000 - - -> [0; 100] of unsupplied energy - -> Total cost without investment = 0.5 * [(200 * 40)] - + 0.5 * [(200 * 40) + (100 * 1_000)] - = 58_000 - - Candidate : Invest cost : 480 / MW, Prod cost : 10 - - Optimal investment : 100 MW - - -> Optimal cost = 480 * 100 (investment) - + 0.5 * (10 * 100 + 40 * 100) (operational - scenario 1) - + 0.5 * (10 * 100 + 40 * 200) (operational - scenario 2) - = 55_000 - - """ - - data = {} - data[ScenarioIndex(0)] = 200 - data[ScenarioIndex(1)] = 300 - - demand_data = ScenarioSeriesData(scenario_series=data) - - database = DataBase() - database.add_data("D", "demand", demand_data) - - database.add_data("N", "spillage_cost", ConstantData(1)) - database.add_data("N", "ens_cost", ConstantData(1_000)) - - database.add_data("G1", "p_max", ConstantData(200)) - database.add_data("G1", "cost", ConstantData(40)) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "invest_cost", ConstantData(480)) - - demand = create_component( - model=DEMAND_MODEL, - id="D", - ) - - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - - scenarios = 2 - blocks = [TimeBlock(1, [0])] - - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - decision_tree_root = DecisionTreeNode("", config, network) - - xpansion = build_benders_decomposed_problem(decision_tree_root, database) - - data_output = { - "solution": { - "overall_cost": 55_000, - "values": { - "CAND.p_max": 100, - }, - } - } - solution = BendersSolution(data_output) - - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" - - -def test_benders_decomposed_multi_time_block_multi_scenario( - generator: Component, - candidate: Component, -) -> None: - """ - Simple generation xpansion problem on one node. One time block with one timestep each, - two scenarios, one thermal cluster candidate. - - Demand = [200 200; 100 300] - Generator : P_max : 200, Cost : 40 - Unsupplied energy : Cost : 1_000 - - -> [0 0; 0 100] of unsupplied energy - -> Total cost without investment = 0.5 * [(200 * 40) + (200 * 40)] - + 0.5 * [(100 * 40) + (200 * 40 + 100 * 1_000)] - = 64_000 - - Candidate : Invest cost : 480 / MW, Prod cost : 10 - - Optimal investment : 100 MW - - -> Optimal cost = 480 * 100 (investment) - + 0.5 * (10 * 100 + 40 * 100) (operational - time block 1 scenario 1) - + 0.5 * (10 * 100 + 40 * 100) (operational - time block 2 scenario 1) - + 0.5 * (10 * 100) (operational - time block 1 scenario 2) - + 0.5 * (10 * 100 + 40 * 200) (operational - time block 2 scenario 2) - = 58_000 - - """ - - data = pd.DataFrame( - [ - [200, 200], - [100, 300], - ], - index=[0, 1], - columns=[0, 1], - ) - - demand_data = TimeScenarioSeriesData(time_scenario_series=data) - - database = DataBase() - database.add_data("D", "demand", demand_data) - - database.add_data("N", "spillage_cost", ConstantData(1)) - database.add_data("N", "ens_cost", ConstantData(1_000)) - - database.add_data("G1", "p_max", ConstantData(200)) - database.add_data("G1", "cost", ConstantData(40)) - - database.add_data("CAND", "op_cost", ConstantData(10)) - database.add_data("CAND", "invest_cost", ConstantData(480)) - - demand = create_component( - model=DEMAND_MODEL, - id="D", - ) - - node = Node(model=NODE_WITH_SPILL_AND_ENS, id="N") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(generator) - network.add_component(candidate) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(generator, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(candidate, "balance_port"), PortRef(node, "balance_port")) - - scenarios = 2 - blocks = [TimeBlock(1, [0]), TimeBlock(2, [1])] - - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - decision_tree_root = DecisionTreeNode("", config, network) - - xpansion = build_benders_decomposed_problem(decision_tree_root, database) - - data_output = { - "solution": { - "overall_cost": 58_000, - "values": { - "CAND.p_max": 100, - }, - } - } - solution = BendersSolution(data_output) - - assert xpansion.run() - decomposed_solution = xpansion.solution - if decomposed_solution is not None: # For mypy only - assert decomposed_solution.is_close( - solution - ), f"Solution differs from expected: {decomposed_solution}" diff --git a/tests/e2e/models/andromede-v1/test_andromede_v1_models.py b/tests/e2e/models/andromede-v1/test_andromede_v1_models.py index ff0ea8bc..53af1ea3 100644 --- a/tests/e2e/models/andromede-v1/test_andromede_v1_models.py +++ b/tests/e2e/models/andromede-v1/test_andromede_v1_models.py @@ -17,12 +17,13 @@ import pandas as pd import pytest -from gems.model.parsing import InputLibrary, parse_yaml_library +from gems.model.parsing import LibrarySchema, parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.simulation.optimization import build_problem +from gems.simulation import build_problem from gems.simulation.time_block import TimeBlock +from gems.study import Study from gems.study.parsing import parse_yaml_components -from gems.study.resolve_components import build_data_base, build_network, resolve_system +from gems.study.resolve_components import build_data_base, resolve_system @pytest.fixture @@ -46,7 +47,7 @@ def series_dir(data_dir: Path) -> Path: @pytest.fixture -def input_libraries(data_dir: Path) -> List[InputLibrary]: +def input_libraries(data_dir: Path) -> List[LibrarySchema]: libs_dir = data_dir / "libs" with open(libs_dir / "antares_historic.yml") as lib_file: lib_historic = parse_yaml_library(lib_file) @@ -115,7 +116,7 @@ def test_model_behaviour( timespan: int, batch: int, relative_accuracy: float, - input_libraries: List[InputLibrary], + input_libraries: List[LibrarySchema], results_dir: Path, systems_dir: Path, series_dir: Path, @@ -124,26 +125,19 @@ def test_model_behaviour( with open(systems_dir / system_file) as compo_file: input_component = parse_yaml_components(compo_file) result_lib = resolve_library(input_libraries) - components_input = resolve_system(input_component, result_lib) + system_input = resolve_system(input_component, result_lib) database = build_data_base(input_component, Path(series_dir)) - network = build_network(components_input) reference_values = pd.read_csv(results_dir / optim_result_file, header=None).values for k in range(batch): problem = build_problem( - network, - database, + Study(system_input, database), TimeBlock(1, [i for i in range(k * timespan, (k + 1) * timespan)]), - scenarios, - ) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert math.isclose( - problem.solver.Objective().Value(), - problem.solver.Objective().BestBound(), - rel_tol=relative_accuracy, + list(range(scenarios)), ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" assert math.isclose( reference_values[k, 0], - problem.solver.Objective().Value(), + problem.objective_value, rel_tol=relative_accuracy, ) diff --git a/tests/e2e/models/operators/optest1/input/system.yml b/tests/e2e/models/operators/optest1/input/system.yml index a0d9720a..15e34912 100644 --- a/tests/e2e/models/operators/optest1/input/system.yml +++ b/tests/e2e/models/operators/optest1/input/system.yml @@ -165,4 +165,3 @@ system: #This system aims at testing the correct handling of complex expressions port1: balance_port port2: balance_port id: e2e_general_test_177428933315 - nodes: [] diff --git a/tests/e2e/models/operators/optest1/parameters.yml b/tests/e2e/models/operators/optest1/parameters.yml deleted file mode 100644 index 96583a2e..00000000 --- a/tests/e2e/models/operators/optest1/parameters.yml +++ /dev/null @@ -1,6 +0,0 @@ -solver: highs -solver-logs: true -solver-parameters: THREADS 1 -no-output: false -first-time-step: 0 -last-time-step: 167 \ No newline at end of file diff --git a/tests/e2e/models/operators/optest2/input/system.yml b/tests/e2e/models/operators/optest2/input/system.yml index 3abef7ef..1997496c 100644 --- a/tests/e2e/models/operators/optest2/input/system.yml +++ b/tests/e2e/models/operators/optest2/input/system.yml @@ -166,4 +166,3 @@ system: #This system aims at testing the correct handling of complex expressions port1: balance_port port2: balance_port id: e2e_general_test_177428933315 - nodes: [] diff --git a/tests/e2e/models/operators/optest2/parameters.yml b/tests/e2e/models/operators/optest2/parameters.yml deleted file mode 100644 index 96583a2e..00000000 --- a/tests/e2e/models/operators/optest2/parameters.yml +++ /dev/null @@ -1,6 +0,0 @@ -solver: highs -solver-logs: true -solver-parameters: THREADS 1 -no-output: false -first-time-step: 0 -last-time-step: 167 \ No newline at end of file diff --git a/tests/e2e/models/operators/optest3/input/system.yml b/tests/e2e/models/operators/optest3/input/system.yml index b2ffee57..72e3e866 100644 --- a/tests/e2e/models/operators/optest3/input/system.yml +++ b/tests/e2e/models/operators/optest3/input/system.yml @@ -165,4 +165,3 @@ system: #This system aims at testing the correct handling of complex expressions port1: balance_port port2: balance_port id: e2e_general_test_177428933315 - nodes: [] diff --git a/tests/e2e/models/operators/optest3/parameters.yml b/tests/e2e/models/operators/optest3/parameters.yml deleted file mode 100644 index 96583a2e..00000000 --- a/tests/e2e/models/operators/optest3/parameters.yml +++ /dev/null @@ -1,6 +0,0 @@ -solver: highs -solver-logs: true -solver-parameters: THREADS 1 -no-output: false -first-time-step: 0 -last-time-step: 167 \ No newline at end of file diff --git a/tests/e2e/models/operators/optest4/input/data-series/load_unique.tsv b/tests/e2e/models/operators/optest4/input/data-series/load_unique.tsv new file mode 100644 index 00000000..105db174 --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/load_unique.tsv @@ -0,0 +1,168 @@ +736.73 736.73 +547.99 547.99 +844.24 844.24 +547.27 547.27 +509.54 509.54 +803.45 803.45 +480.97 480.97 +521.64 521.64 +604.89 604.89 +599.11 599.11 +722.59 722.59 +636.72 636.72 +544.49 544.49 +528.57 528.57 +480.98 480.98 +498.35 498.35 +681.24 681.24 +841.3 841.3 +874.61 874.61 +506.36 506.36 +784.14 784.14 +446.29 446.29 +594.61 594.61 +638.76 638.76 +766.24 766.24 +845.49 845.49 +537.89 537.89 +579.86 579.86 +796.61 796.61 +649.69 649.69 +654.61 654.61 +604.63 604.63 +862.36 862.36 +656.75 656.75 +799.12 799.12 +561.84 561.84 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +654.39 654.39 +687.5 687.5 +479.1 479.1 +590.43 590.43 +755.26 755.26 +574.25 574.25 +739.42 739.42 +635.71 635.71 +907.0 907.0 +858.84 858.84 +556.55 556.55 +690.38 690.38 +577.92 577.92 +734.97 734.97 +827.65 827.65 +589.21 589.21 +706.6 706.6 +692.94 692.94 +0.0 0.0 +0.0 0.0 +708.89 708.89 +0.0 0.0 +683.05 683.05 +712.32 712.32 +658.41 658.41 +641.16 641.16 +885.88 885.88 +737.91 737.91 +577.85 577.85 +772.26 772.26 +637.61 637.61 +614.17 614.17 +751.17 751.17 +807.55 807.55 +780.66 780.66 +768.38 768.38 +707.41 707.41 +736.61 736.61 +767.37 767.37 +843.95 843.95 +0.0 0.0 +0.0 0.0 +0.0 0.0 +656.74 656.74 +796.69 796.69 +712.19 712.19 +796.34 796.34 +605.81 605.81 +697.36 697.36 +603.45 603.45 +833.54 833.54 +606.89 606.89 +673.68 673.68 +774.9 774.9 +0.0 0.0 +695.18 695.18 +881.96 881.96 +621.29 621.29 +539.14 539.14 +713.11 713.11 +897.81 897.81 +651.17 651.17 +702.63 702.63 +846.88 846.88 +655.82 655.82 +868.32 868.32 +911.14 911.14 +655.08 655.08 +844.09 844.09 +850.72 850.72 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +999.64 999.64 +869.94 869.94 +797.79 797.79 +748.35 748.35 +809.36 809.36 +815.73 815.73 +963.16 963.16 +752.9 752.9 +756.33 756.33 +763.16 763.16 +931.61 931.61 +786.89 786.89 +758.99 758.99 +711.5 711.5 +719.95 719.95 +841.85 841.85 +814.44 814.44 +932.7 932.7 +723.29 723.29 +917.69 917.69 +920.01 920.01 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +0.0 0.0 +866.0 866.0 +822.11 822.11 +726.63 726.63 +932.05 932.05 +766.61 766.61 +764.17 764.17 +750.01 750.01 +785.51 785.51 +752.31 752.31 +925.49 925.49 +0.0 0.0 +836.95 836.95 +847.57 847.57 +868.01 868.01 diff --git a/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod.tsv b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod.tsv new file mode 100644 index 00000000..b66a52fb --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod.tsv @@ -0,0 +1,168 @@ +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 +0.0 0.5 +0.3 0.1 diff --git a/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod2.tsv b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod2.tsv new file mode 100644 index 00000000..419d02ee --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod2.tsv @@ -0,0 +1,168 @@ +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 diff --git a/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod3.tsv b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod3.tsv new file mode 100644 index 00000000..419d02ee --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/minimum_generation_modulation_unique_prod3.tsv @@ -0,0 +1,168 @@ +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 +0.0 0.4 +0.3 0.1 diff --git a/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod.tsv b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod.tsv new file mode 100644 index 00000000..44b5abde --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod.tsv @@ -0,0 +1,168 @@ +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 +150.0 150.0 diff --git a/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod2.tsv b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod2.tsv new file mode 100644 index 00000000..dd6c95a6 --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod2.tsv @@ -0,0 +1,168 @@ +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 +200.0 200.0 diff --git a/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod3.tsv b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod3.tsv new file mode 100644 index 00000000..caeb183b --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/data-series/p_max_cluster_unique_prod3.tsv @@ -0,0 +1,168 @@ +86.7446002046025 78.76965939633274 +35.04066653994295 78.19476049966464 +115.22715497750636 70.89041592427252 +70.29037453708783 56.30186433267118 +107.7575553774754 60.08334506994804 +44.38393736858112 75.07544340851376 +27.90812397783256 30.538203692842814 +62.13066828918777 80.11478977887727 +46.38069094544448 46.83818528842543 +104.80710952829392 39.43263182781594 +43.87990506112771 38.60343831820062 +95.27707522391124 36.19864501933177 +61.64679626964877 55.43435197171313 +41.4966259154289 90.63877180908068 +95.37978380084544 58.4607810630673 +30.67987977311564 109.57110296635986 +108.99237162055208 43.38046842477036 +115.45386232526802 34.617840331546816 +113.38976972904103 74.64544908388669 +63.88137149231116 107.294911585072 +26.782239301139644 49.13994032267007 +118.27463852142698 71.9419316391619 +56.6052176289877 65.14218252633826 +91.84197061332516 74.88037331323937 +58.74019849032594 79.09889489255677 +27.370166203330697 61.61814240455673 +106.08559231107915 31.63335507475876 +87.09633678148728 107.25897699568289 +97.5055674379429 39.79234369110462 +77.19237484842185 101.1342356042608 +108.96892191461755 51.64111182818744 +110.80297107814678 49.78283299891224 +25.000483253659603 92.8778894851564 +31.15744666967309 108.61494295187578 +47.4844041369394 119.2435228363008 +36.79725623534255 68.0016035435769 +91.00080963933304 99.37240999409764 +62.227668818489775 94.91461782002357 +108.77973037479772 69.46396590874734 +41.37672098627549 70.54413168364225 +65.5199204920333 113.56848941877476 +25.74174746444857 96.25476853823176 +90.37803472212887 39.29172750811918 +69.09030259544033 36.94180464679707 +36.30933020675019 63.42500867432062 +109.56835692378807 96.62748117432268 +112.14754794922706 44.401250111611496 +31.01961539316178 54.14682260483802 +28.363002550084342 43.32224982018152 +66.1029979911987 114.08881956055971 +81.7652092704441 72.4238244402521 +53.78179526069761 81.95821919210792 +89.46319123057434 104.4046641428368 +44.06526248078546 82.57751619161607 +73.88201253345935 94.61498533223865 +78.33492773463077 63.33754863483858 +66.35206968840738 71.00544102905212 +37.205391092089656 72.85675396343464 +44.50014634919643 72.62230178161981 +36.80370164218972 83.3607426115513 +54.94048334383784 104.12206240550807 +46.4531630567644 80.31027613927807 +74.6398229779028 26.0455094165459 +78.33323154974914 94.61984246143685 +66.06962733946175 112.38083103384938 +54.89977191169046 96.54501321755215 +77.72551782645753 51.33655508504299 +107.07317579371264 115.16150556960636 +105.33915622802124 104.62875341078755 +34.350447934570695 102.23520044132242 +54.36667046153164 95.39306627316984 +63.30995124492366 111.82745983220022 +89.96054533655514 110.0384623276083 +91.41458759743456 35.55876713442672 +66.83500263860772 59.95825558476117 +64.35091949752449 49.31377802907779 +101.05253674271408 54.85275554570137 +75.7072862705928 93.78880640841014 +112.6560469837484 94.84295483733592 +29.94312357357038 68.58316212216208 +59.288011520525465 97.34761358088824 +117.15828215524007 47.20325947073047 +91.11397904264584 110.7839206000386 +109.53365567301496 78.87419731878202 +48.34106096591516 54.07502774392543 +93.65060215882642 26.26576910987093 +43.53546575051528 73.92499779853551 +75.60113507870943 79.71853044239194 +46.74652835778889 36.319390961533024 +65.9032473378589 97.66255650070116 +100.6103532981048 116.3015794743065 +53.60727982466643 40.982512611384784 +83.66559633621029 105.2636510840414 +66.20621929395452 109.25001729072078 +111.6790757224403 49.2157051855299 +87.23384670251241 119.30065713757358 +86.74861901906132 80.86826172200678 +58.41194720445105 57.45938174811396 +74.20239045149803 70.90807601785771 +28.328381983147924 78.48268361153634 +33.0537275427542 102.89289257212656 +112.4747705335715 96.1047134082262 +31.01227213425721 82.48178710553559 +113.22406603594165 70.40095825060513 +92.83345316373612 102.87355435076792 +83.74181239097713 111.5817174310365 +96.47901255510716 35.9994302078137 +37.60031126098823 115.85948174415304 +100.2069309789552 95.67491949369698 +93.63870238928416 61.2845134130854 +75.9781709392689 118.21388732310056 +70.32811134368916 69.546327234458 +69.74185059575868 40.80087309440676 +66.819151722241 73.36409039414407 +102.34272599108569 107.97403192357152 +50.337743361954615 97.22300173357488 +29.94835273707276 69.80433951292001 +116.18844879970256 25.003883745167897 +67.97934047191055 39.88203721432625 +96.75763818847716 54.91007405251952 +89.39122586906633 50.65418177567204 +76.01068087425277 108.65747242478616 +36.75191899766365 109.48586816168164 +48.307740943249584 119.60830151084488 +66.61013693495912 84.18139018892424 +77.63999104031826 51.59486811780064 +102.13097804561951 115.86924660137656 +54.51152028143028 73.54716828972008 +62.116328944704414 74.16671550528157 +109.45615183616675 86.52061330188079 +103.23995594888773 83.2494153467833 +34.24195997082933 65.58059128911671 +39.33848405454816 75.02154890492338 +86.51705362929295 51.80904988374823 +78.63016449539404 75.02700345532398 +118.47203262961038 96.75818691573429 +95.91926521528424 24.844095146288936 +101.03036456202332 81.99361527175905 +45.91774848072596 104.90232220601128 +73.79649081150123 117.54077190066572 +84.4904066982728 99.47305842971178 +24.20979469873612 97.29139960414872 +73.26861750393367 68.6179935341604 +67.12225620883785 89.4504334471957 +57.5382096376846 101.4825412740188 +50.2734009003796 81.68360316072628 +110.59995509510676 111.91149391573497 +26.97354815993073 72.57528662126354 +55.12023262262158 51.51981133832267 +51.51290377430287 100.68949489477376 +115.88506354936112 59.56796829200803 +25.58717434230721 78.66303858770705 +67.9030726301407 95.14508997077198 +55.19383641398229 57.37583698125071 +90.86048601858838 91.03876086554924 +90.50397719470809 95.40149065681624 +89.53402264832472 93.87883731739907 +109.64452396992549 110.55829491379497 +114.77548827745078 108.1487136744247 +31.85352243578241 77.95060384291641 +35.2213651729531 33.27955288205062 +27.539420853491617 83.28865906502426 +112.6838597899374 101.06591241632049 +118.2624760716756 81.78014837193 +60.156024318310045 60.43885109303644 +30.093696699377272 30.477996119018563 +36.274220468001445 108.55624233701764 +108.46933199367363 42.992465482817046 diff --git a/tests/e2e/models/operators/optest4/input/model-libraries/test_lib.yml b/tests/e2e/models/operators/optest4/input/model-libraries/test_lib.yml new file mode 100644 index 00000000..e2c9dfb9 --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/model-libraries/test_lib.yml @@ -0,0 +1,119 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +library: + id: test_lib + description: This library aims at testing the correct handling of complex expressions of bounds of variables, which requires an "expression expansion" step in the code. The test is successful if the optimization problem is solved without errors and the optimal solution is correct. + + + + port-types: + - id: flow + description: A port that transfers power flow. + fields: + - id: flow + + models: + - id: area + parameters: + - id: spillage_cost + - id: ens_cost + variables: + - id: spillage + lower-bound: 0 + variable-type: continuous + - id: unsupplied_energy + lower-bound: 0 + variable-type: continuous + ports: + - id: balance_port + type: flow + binding-constraints: + - id: balance + expression: sum_connections(balance_port.flow) = spillage - unsupplied_energy + objective-contributions: + - id: objective + expression: expec(sum(spillage_cost * spillage + ens_cost * unsupplied_energy)) + + - id: load + parameters: + - id: load + time-dependent: true + scenario-dependent: true + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: -load + + + + + - id: thermal + parameters: + - id: minimum_generation_modulation + scenario-dependent: true + time-dependent: true + - id: p_max_cluster # timeseries that takes outages into account + scenario-dependent: true + time-dependent: true + - id: p_min_unit + - id: p_max_unit + - id: generation_cost + - id: startup_cost + - id: fixed_cost + - id: d_min_up + - id: d_min_down + - id: unit_count + variables: + - id: generation + lower-bound: min(p_max_cluster, minimum_generation_modulation * unit_count * p_max_unit) + upper-bound: p_max_cluster + variable-type: continuous + - id: nb_units_on + lower-bound: ceil(min(p_max_cluster, minimum_generation_modulation * unit_count * p_max_unit)/p_max_unit) + upper-bound: ceil(p_max_cluster/p_max_unit) + variable-type: continuous #integer + - id: nb_starting + lower-bound: 0 + variable-type: continuous #integer + - id: nb_stopping + lower-bound: 0 + variable-type: continuous #integer + - id: nb_failing + lower-bound: 0 + upper-bound: max(0, (ceil(p_max_cluster/p_max_unit))[t-1] - (ceil(p_max_cluster/p_max_unit))) + variable-type: continuous #integer + ports: + - id: balance_port + type: flow + port-field-definitions: + - port: balance_port + field: flow + definition: generation + constraints: + - id: max_generation + expression: generation <= nb_units_on * p_max_unit + - id: min_generation + expression: generation >= nb_units_on * p_min_unit + - id: on_units_dynamics + expression: nb_units_on = nb_units_on[t-1] + nb_starting - nb_stopping + - id: nb_failing_lower_than_stopping + expression: nb_failing <= nb_stopping + - id: min_up_duration + expression: sum(t-d_min_up + 1 .. t, nb_starting - nb_failing) <= nb_units_on + - id: min_down_duration + expression: sum(t-d_min_down + 1 .. t, nb_stopping) <= ((ceil(p_max_cluster/p_max_unit)))[t-d_min_down] - nb_units_on + sum(t-d_min_down + 1 .. t, max(0, ceil(p_max_cluster/p_max_unit) - (ceil(p_max_cluster/p_max_unit))[t-1])) + objective-contributions: + - id: objective + expression: expec(sum(generation_cost * generation + startup_cost * nb_starting + fixed_cost * nb_units_on)) \ No newline at end of file diff --git a/tests/e2e/models/operators/optest4/input/system.yml b/tests/e2e/models/operators/optest4/input/system.yml new file mode 100644 index 00000000..15e34912 --- /dev/null +++ b/tests/e2e/models/operators/optest4/input/system.yml @@ -0,0 +1,167 @@ +system: #This system aims at testing the correct handling of complex expressions of bounds of variables, which requires an "expression expansion" step in the code. The test is successful if the optimization problem is solved without errors and the optimal solution is correct. + components: + - id: unique_prod + model: test_lib.thermal + parameters: + - id: minimum_generation_modulation + scenario-dependent: true + time-dependent: true + value: minimum_generation_modulation_unique_prod + - id: p_max_cluster + scenario-dependent: true + time-dependent: true + value: p_max_cluster_unique_prod + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 150.0 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 10.0 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: unit_count + scenario-dependent: false + time-dependent: false + value: 2.0 + - id: unique_prod2 + model: test_lib.thermal + parameters: + - id: minimum_generation_modulation + scenario-dependent: true + time-dependent: true + value: minimum_generation_modulation_unique_prod2 + - id: p_max_cluster + scenario-dependent: true + time-dependent: true + value: p_max_cluster_unique_prod2 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 200.0 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 20.0 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: unit_count + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: unique_prod3 + model: test_lib.thermal + parameters: + - id: minimum_generation_modulation + scenario-dependent: true + time-dependent: true + value: minimum_generation_modulation_unique_prod3 + - id: p_max_cluster + scenario-dependent: true + time-dependent: true + value: p_max_cluster_unique_prod3 + - id: p_min_unit + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: p_max_unit + scenario-dependent: false + time-dependent: false + value: 40.0 + - id: generation_cost + scenario-dependent: false + time-dependent: false + value: 5.0 + - id: startup_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: fixed_cost + scenario-dependent: false + time-dependent: false + value: 0.0 + - id: d_min_up + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: d_min_down + scenario-dependent: false + time-dependent: false + value: 1.0 + - id: unit_count + scenario-dependent: false + time-dependent: false + value: 3.0 + - id: load_unique + model: test_lib.load + parameters: + - id: load + scenario-dependent: true + time-dependent: true + value: load_unique + - id: unique + model: test_lib.area + parameters: + - id: ens_cost + scenario-dependent: false + time-dependent: false + value: 500.0 + - id: spillage_cost + scenario-dependent: false + time-dependent: false + value: 10.0 + connections: + - component1: unique_prod + component2: unique + port1: balance_port + port2: balance_port + - component1: unique_prod2 + component2: unique + port1: balance_port + port2: balance_port + - component1: unique_prod3 + component2: unique + port1: balance_port + port2: balance_port + - component1: load_unique + component2: unique + port1: balance_port + port2: balance_port + id: e2e_general_test_177428933315 diff --git a/tests/e2e/models/operators/optest4/output/simulation_table--20260323-1953.csv b/tests/e2e/models/operators/optest4/output/simulation_table--20260323-1953.csv new file mode 100644 index 00000000..16625dab --- /dev/null +++ b/tests/e2e/models/operators/optest4/output/simulation_table--20260323-1953.csv @@ -0,0 +1,6722 @@ +block,component,output,absolute_time_index,block_time_index,scenario_index,value,basis_status +1,unique_prod,generation,1,1,0,150,Free +1,unique_prod,generation,2,2,0,150,Free +1,unique_prod,generation,3,3,0,150,Free +1,unique_prod,generation,4,4,0,150,Free +1,unique_prod,generation,5,5,0,150,Free +1,unique_prod,generation,6,6,0,150,Free +1,unique_prod,generation,7,7,0,150,Free +1,unique_prod,generation,8,8,0,150,Free +1,unique_prod,generation,9,9,0,150,Free +1,unique_prod,generation,10,10,0,150,Free +1,unique_prod,generation,11,11,0,150,Free +1,unique_prod,generation,12,12,0,150,Free +1,unique_prod,generation,13,13,0,150,Free +1,unique_prod,generation,14,14,0,150,Free +1,unique_prod,generation,15,15,0,150,Free +1,unique_prod,generation,16,16,0,150,Free +1,unique_prod,generation,17,17,0,150,Free +1,unique_prod,generation,18,18,0,150,Free +1,unique_prod,generation,19,19,0,150,Free +1,unique_prod,generation,20,20,0,150,Free +1,unique_prod,generation,21,21,0,150,Free +1,unique_prod,generation,22,22,0,150,Free +1,unique_prod,generation,23,23,0,150,Free +1,unique_prod,generation,24,24,0,150,Free +1,unique_prod,generation,25,25,0,150,Free +1,unique_prod,generation,26,26,0,150,Free +1,unique_prod,generation,27,27,0,150,Free +1,unique_prod,generation,28,28,0,150,Free +1,unique_prod,generation,29,29,0,150,Free +1,unique_prod,generation,30,30,0,150,Free +1,unique_prod,generation,31,31,0,150,Free +1,unique_prod,generation,32,32,0,150,Free +1,unique_prod,generation,33,33,0,150,Free +1,unique_prod,generation,34,34,0,150,Free +1,unique_prod,generation,35,35,0,150,Free +1,unique_prod,generation,36,36,0,150,Free +1,unique_prod,generation,37,37,0,0,Free +1,unique_prod,generation,38,38,0,90,Free +1,unique_prod,generation,39,39,0,0,Free +1,unique_prod,generation,40,40,0,90,Free +1,unique_prod,generation,41,41,0,0,Free +1,unique_prod,generation,42,42,0,90,Free +1,unique_prod,generation,43,43,0,0,Free +1,unique_prod,generation,44,44,0,90,Free +1,unique_prod,generation,45,45,0,0,Free +1,unique_prod,generation,46,46,0,150,Free +1,unique_prod,generation,47,47,0,150,Free +1,unique_prod,generation,48,48,0,150,Free +1,unique_prod,generation,49,49,0,150,Free +1,unique_prod,generation,50,50,0,150,Free +1,unique_prod,generation,51,51,0,150,Free +1,unique_prod,generation,52,52,0,150,Free +1,unique_prod,generation,53,53,0,150,Free +1,unique_prod,generation,54,54,0,150,Free +1,unique_prod,generation,55,55,0,150,Free +1,unique_prod,generation,56,56,0,150,Free +1,unique_prod,generation,57,57,0,150,Free +1,unique_prod,generation,58,58,0,150,Free +1,unique_prod,generation,59,59,0,150,Free +1,unique_prod,generation,60,60,0,150,Free +1,unique_prod,generation,61,61,0,150,Free +1,unique_prod,generation,62,62,0,150,Free +1,unique_prod,generation,63,63,0,150,Free +1,unique_prod,generation,64,64,0,90,Free +1,unique_prod,generation,65,65,0,0,Free +1,unique_prod,generation,66,66,0,150,Free +1,unique_prod,generation,67,67,0,0,Free +1,unique_prod,generation,68,68,0,150,Free +1,unique_prod,generation,69,69,0,150,Free +1,unique_prod,generation,70,70,0,150,Free +1,unique_prod,generation,71,71,0,150,Free +1,unique_prod,generation,72,72,0,150,Free +1,unique_prod,generation,73,73,0,150,Free +1,unique_prod,generation,74,74,0,150,Free +1,unique_prod,generation,75,75,0,150,Free +1,unique_prod,generation,76,76,0,150,Free +1,unique_prod,generation,77,77,0,150,Free +1,unique_prod,generation,78,78,0,150,Free +1,unique_prod,generation,79,79,0,150,Free +1,unique_prod,generation,80,80,0,150,Free +1,unique_prod,generation,81,81,0,150,Free +1,unique_prod,generation,82,82,0,150,Free +1,unique_prod,generation,83,83,0,150,Free +1,unique_prod,generation,84,84,0,150,Free +1,unique_prod,generation,85,85,0,150,Free +1,unique_prod,generation,86,86,0,90,Free +1,unique_prod,generation,87,87,0,0,Free +1,unique_prod,generation,88,88,0,90,Free +1,unique_prod,generation,89,89,0,150,Free +1,unique_prod,generation,90,90,0,150,Free +1,unique_prod,generation,91,91,0,150,Free +1,unique_prod,generation,92,92,0,150,Free +1,unique_prod,generation,93,93,0,150,Free +1,unique_prod,generation,94,94,0,150,Free +1,unique_prod,generation,95,95,0,150,Free +1,unique_prod,generation,96,96,0,150,Free +1,unique_prod,generation,97,97,0,150,Free +1,unique_prod,generation,98,98,0,150,Free +1,unique_prod,generation,99,99,0,150,Free +1,unique_prod,generation,100,100,0,90,Free +1,unique_prod,generation,101,101,0,150,Free +1,unique_prod,generation,102,102,0,150,Free +1,unique_prod,generation,103,103,0,150,Free +1,unique_prod,generation,104,104,0,150,Free +1,unique_prod,generation,105,105,0,150,Free +1,unique_prod,generation,106,106,0,150,Free +1,unique_prod,generation,107,107,0,150,Free +1,unique_prod,generation,108,108,0,150,Free +1,unique_prod,generation,109,109,0,150,Free +1,unique_prod,generation,110,110,0,150,Free +1,unique_prod,generation,111,111,0,150,Free +1,unique_prod,generation,112,112,0,150,Free +1,unique_prod,generation,113,113,0,150,Free +1,unique_prod,generation,114,114,0,150,Free +1,unique_prod,generation,115,115,0,150,Free +1,unique_prod,generation,116,116,0,90,Free +1,unique_prod,generation,117,117,0,0,Free +1,unique_prod,generation,118,118,0,90,Free +1,unique_prod,generation,119,119,0,0,Free +1,unique_prod,generation,120,120,0,90,Free +1,unique_prod,generation,121,121,0,0,Free +1,unique_prod,generation,122,122,0,90,Free +1,unique_prod,generation,123,123,0,0,Free +1,unique_prod,generation,124,124,0,90,Free +1,unique_prod,generation,125,125,0,150,Free +1,unique_prod,generation,126,126,0,150,Free +1,unique_prod,generation,127,127,0,150,Free +1,unique_prod,generation,128,128,0,150,Free +1,unique_prod,generation,129,129,0,150,Free +1,unique_prod,generation,130,130,0,150,Free +1,unique_prod,generation,131,131,0,150,Free +1,unique_prod,generation,132,132,0,150,Free +1,unique_prod,generation,133,133,0,150,Free +1,unique_prod,generation,134,134,0,150,Free +1,unique_prod,generation,135,135,0,150,Free +1,unique_prod,generation,136,136,0,150,Free +1,unique_prod,generation,137,137,0,150,Free +1,unique_prod,generation,138,138,0,150,Free +1,unique_prod,generation,139,139,0,150,Free +1,unique_prod,generation,140,140,0,150,Free +1,unique_prod,generation,141,141,0,150,Free +1,unique_prod,generation,142,142,0,150,Free +1,unique_prod,generation,143,143,0,150,Free +1,unique_prod,generation,144,144,0,150,Free +1,unique_prod,generation,145,145,0,150,Free +1,unique_prod,generation,146,146,0,90,Free +1,unique_prod,generation,147,147,0,0,Free +1,unique_prod,generation,148,148,0,90,Free +1,unique_prod,generation,149,149,0,0,Free +1,unique_prod,generation,150,150,0,90,Free +1,unique_prod,generation,151,151,0,0,Free +1,unique_prod,generation,152,152,0,90,Free +1,unique_prod,generation,153,153,0,0,Free +1,unique_prod,generation,154,154,0,90,Free +1,unique_prod,generation,155,155,0,150,Free +1,unique_prod,generation,156,156,0,150,Free +1,unique_prod,generation,157,157,0,150,Free +1,unique_prod,generation,158,158,0,150,Free +1,unique_prod,generation,159,159,0,150,Free +1,unique_prod,generation,160,160,0,150,Free +1,unique_prod,generation,161,161,0,150,Free +1,unique_prod,generation,162,162,0,150,Free +1,unique_prod,generation,163,163,0,150,Free +1,unique_prod,generation,164,164,0,150,Free +1,unique_prod,generation,165,165,0,0,Free +1,unique_prod,generation,166,166,0,150,Free +1,unique_prod,generation,167,167,0,150,Free +1,unique_prod,generation,168,168,0,150,Free +1,unique_prod,nb_units_on,1,1,0,1,Free +1,unique_prod,nb_units_on,2,2,0,1,Free +1,unique_prod,nb_units_on,3,3,0,1,Free +1,unique_prod,nb_units_on,4,4,0,1,Free +1,unique_prod,nb_units_on,5,5,0,1,Free +1,unique_prod,nb_units_on,6,6,0,1,Free +1,unique_prod,nb_units_on,7,7,0,1,Free +1,unique_prod,nb_units_on,8,8,0,1,Free +1,unique_prod,nb_units_on,9,9,0,1,Free +1,unique_prod,nb_units_on,10,10,0,1,Free +1,unique_prod,nb_units_on,11,11,0,1,Free +1,unique_prod,nb_units_on,12,12,0,1,Free +1,unique_prod,nb_units_on,13,13,0,1,Free +1,unique_prod,nb_units_on,14,14,0,1,Free +1,unique_prod,nb_units_on,15,15,0,1,Free +1,unique_prod,nb_units_on,16,16,0,1,Free +1,unique_prod,nb_units_on,17,17,0,1,Free +1,unique_prod,nb_units_on,18,18,0,1,Free +1,unique_prod,nb_units_on,19,19,0,1,Free +1,unique_prod,nb_units_on,20,20,0,1,Free +1,unique_prod,nb_units_on,21,21,0,1,Free +1,unique_prod,nb_units_on,22,22,0,1,Free +1,unique_prod,nb_units_on,23,23,0,1,Free +1,unique_prod,nb_units_on,24,24,0,1,Free +1,unique_prod,nb_units_on,25,25,0,1,Free +1,unique_prod,nb_units_on,26,26,0,1,Free +1,unique_prod,nb_units_on,27,27,0,1,Free +1,unique_prod,nb_units_on,28,28,0,1,Free +1,unique_prod,nb_units_on,29,29,0,1,Free +1,unique_prod,nb_units_on,30,30,0,1,Free +1,unique_prod,nb_units_on,31,31,0,1,Free +1,unique_prod,nb_units_on,32,32,0,1,Free +1,unique_prod,nb_units_on,33,33,0,1,Free +1,unique_prod,nb_units_on,34,34,0,1,Free +1,unique_prod,nb_units_on,35,35,0,1,Free +1,unique_prod,nb_units_on,36,36,0,1,Free +1,unique_prod,nb_units_on,37,37,0,0,Free +1,unique_prod,nb_units_on,38,38,0,1,Free +1,unique_prod,nb_units_on,39,39,0,0,Free +1,unique_prod,nb_units_on,40,40,0,1,Free +1,unique_prod,nb_units_on,41,41,0,0,Free +1,unique_prod,nb_units_on,42,42,0,1,Free +1,unique_prod,nb_units_on,43,43,0,0,Free +1,unique_prod,nb_units_on,44,44,0,1,Free +1,unique_prod,nb_units_on,45,45,0,0,Free +1,unique_prod,nb_units_on,46,46,0,1,Free +1,unique_prod,nb_units_on,47,47,0,1,Free +1,unique_prod,nb_units_on,48,48,0,1,Free +1,unique_prod,nb_units_on,49,49,0,1,Free +1,unique_prod,nb_units_on,50,50,0,1,Free +1,unique_prod,nb_units_on,51,51,0,1,Free +1,unique_prod,nb_units_on,52,52,0,1,Free +1,unique_prod,nb_units_on,53,53,0,1,Free +1,unique_prod,nb_units_on,54,54,0,1,Free +1,unique_prod,nb_units_on,55,55,0,1,Free +1,unique_prod,nb_units_on,56,56,0,1,Free +1,unique_prod,nb_units_on,57,57,0,1,Free +1,unique_prod,nb_units_on,58,58,0,1,Free +1,unique_prod,nb_units_on,59,59,0,1,Free +1,unique_prod,nb_units_on,60,60,0,1,Free +1,unique_prod,nb_units_on,61,61,0,1,Free +1,unique_prod,nb_units_on,62,62,0,1,Free +1,unique_prod,nb_units_on,63,63,0,1,Free +1,unique_prod,nb_units_on,64,64,0,1,Free +1,unique_prod,nb_units_on,65,65,0,0,Free +1,unique_prod,nb_units_on,66,66,0,1,Free +1,unique_prod,nb_units_on,67,67,0,0,Free +1,unique_prod,nb_units_on,68,68,0,1,Free +1,unique_prod,nb_units_on,69,69,0,1,Free +1,unique_prod,nb_units_on,70,70,0,1,Free +1,unique_prod,nb_units_on,71,71,0,1,Free +1,unique_prod,nb_units_on,72,72,0,1,Free +1,unique_prod,nb_units_on,73,73,0,1,Free +1,unique_prod,nb_units_on,74,74,0,1,Free +1,unique_prod,nb_units_on,75,75,0,1,Free +1,unique_prod,nb_units_on,76,76,0,1,Free +1,unique_prod,nb_units_on,77,77,0,1,Free +1,unique_prod,nb_units_on,78,78,0,1,Free +1,unique_prod,nb_units_on,79,79,0,1,Free +1,unique_prod,nb_units_on,80,80,0,1,Free +1,unique_prod,nb_units_on,81,81,0,1,Free +1,unique_prod,nb_units_on,82,82,0,1,Free +1,unique_prod,nb_units_on,83,83,0,1,Free +1,unique_prod,nb_units_on,84,84,0,1,Free +1,unique_prod,nb_units_on,85,85,0,1,Free +1,unique_prod,nb_units_on,86,86,0,1,Free +1,unique_prod,nb_units_on,87,87,0,0,Free +1,unique_prod,nb_units_on,88,88,0,1,Free +1,unique_prod,nb_units_on,89,89,0,1,Free +1,unique_prod,nb_units_on,90,90,0,1,Free +1,unique_prod,nb_units_on,91,91,0,1,Free +1,unique_prod,nb_units_on,92,92,0,1,Free +1,unique_prod,nb_units_on,93,93,0,1,Free +1,unique_prod,nb_units_on,94,94,0,1,Free +1,unique_prod,nb_units_on,95,95,0,1,Free +1,unique_prod,nb_units_on,96,96,0,1,Free +1,unique_prod,nb_units_on,97,97,0,1,Free +1,unique_prod,nb_units_on,98,98,0,1,Free +1,unique_prod,nb_units_on,99,99,0,1,Free +1,unique_prod,nb_units_on,100,100,0,1,Free +1,unique_prod,nb_units_on,101,101,0,1,Free +1,unique_prod,nb_units_on,102,102,0,1,Free +1,unique_prod,nb_units_on,103,103,0,1,Free +1,unique_prod,nb_units_on,104,104,0,1,Free +1,unique_prod,nb_units_on,105,105,0,1,Free +1,unique_prod,nb_units_on,106,106,0,1,Free +1,unique_prod,nb_units_on,107,107,0,1,Free +1,unique_prod,nb_units_on,108,108,0,1,Free +1,unique_prod,nb_units_on,109,109,0,1,Free +1,unique_prod,nb_units_on,110,110,0,1,Free +1,unique_prod,nb_units_on,111,111,0,1,Free +1,unique_prod,nb_units_on,112,112,0,1,Free +1,unique_prod,nb_units_on,113,113,0,1,Free +1,unique_prod,nb_units_on,114,114,0,1,Free +1,unique_prod,nb_units_on,115,115,0,1,Free +1,unique_prod,nb_units_on,116,116,0,1,Free +1,unique_prod,nb_units_on,117,117,0,0,Free +1,unique_prod,nb_units_on,118,118,0,1,Free +1,unique_prod,nb_units_on,119,119,0,0,Free +1,unique_prod,nb_units_on,120,120,0,1,Free +1,unique_prod,nb_units_on,121,121,0,0,Free +1,unique_prod,nb_units_on,122,122,0,1,Free +1,unique_prod,nb_units_on,123,123,0,0,Free +1,unique_prod,nb_units_on,124,124,0,1,Free +1,unique_prod,nb_units_on,125,125,0,1,Free +1,unique_prod,nb_units_on,126,126,0,1,Free +1,unique_prod,nb_units_on,127,127,0,1,Free +1,unique_prod,nb_units_on,128,128,0,1,Free +1,unique_prod,nb_units_on,129,129,0,1,Free +1,unique_prod,nb_units_on,130,130,0,1,Free +1,unique_prod,nb_units_on,131,131,0,1,Free +1,unique_prod,nb_units_on,132,132,0,1,Free +1,unique_prod,nb_units_on,133,133,0,1,Free +1,unique_prod,nb_units_on,134,134,0,1,Free +1,unique_prod,nb_units_on,135,135,0,1,Free +1,unique_prod,nb_units_on,136,136,0,1,Free +1,unique_prod,nb_units_on,137,137,0,1,Free +1,unique_prod,nb_units_on,138,138,0,1,Free +1,unique_prod,nb_units_on,139,139,0,1,Free +1,unique_prod,nb_units_on,140,140,0,1,Free +1,unique_prod,nb_units_on,141,141,0,1,Free +1,unique_prod,nb_units_on,142,142,0,1,Free +1,unique_prod,nb_units_on,143,143,0,1,Free +1,unique_prod,nb_units_on,144,144,0,1,Free +1,unique_prod,nb_units_on,145,145,0,1,Free +1,unique_prod,nb_units_on,146,146,0,1,Free +1,unique_prod,nb_units_on,147,147,0,0,Free +1,unique_prod,nb_units_on,148,148,0,1,Free +1,unique_prod,nb_units_on,149,149,0,0,Free +1,unique_prod,nb_units_on,150,150,0,1,Free +1,unique_prod,nb_units_on,151,151,0,0,Free +1,unique_prod,nb_units_on,152,152,0,1,Free +1,unique_prod,nb_units_on,153,153,0,0,Free +1,unique_prod,nb_units_on,154,154,0,1,Free +1,unique_prod,nb_units_on,155,155,0,1,Free +1,unique_prod,nb_units_on,156,156,0,1,Free +1,unique_prod,nb_units_on,157,157,0,1,Free +1,unique_prod,nb_units_on,158,158,0,1,Free +1,unique_prod,nb_units_on,159,159,0,1,Free +1,unique_prod,nb_units_on,160,160,0,1,Free +1,unique_prod,nb_units_on,161,161,0,1,Free +1,unique_prod,nb_units_on,162,162,0,1,Free +1,unique_prod,nb_units_on,163,163,0,1,Free +1,unique_prod,nb_units_on,164,164,0,1,Free +1,unique_prod,nb_units_on,165,165,0,0,Free +1,unique_prod,nb_units_on,166,166,0,1,Free +1,unique_prod,nb_units_on,167,167,0,1,Free +1,unique_prod,nb_units_on,168,168,0,1,Free +1,unique_prod,nb_starting,1,1,0,0,Free +1,unique_prod,nb_starting,2,2,0,-0,Free +1,unique_prod,nb_starting,3,3,0,0,Free +1,unique_prod,nb_starting,4,4,0,-0,Free +1,unique_prod,nb_starting,5,5,0,0,Free +1,unique_prod,nb_starting,6,6,0,-0,Free +1,unique_prod,nb_starting,7,7,0,0,Free +1,unique_prod,nb_starting,8,8,0,-0,Free +1,unique_prod,nb_starting,9,9,0,0,Free +1,unique_prod,nb_starting,10,10,0,-0,Free +1,unique_prod,nb_starting,11,11,0,0,Free +1,unique_prod,nb_starting,12,12,0,-0,Free +1,unique_prod,nb_starting,13,13,0,0,Free +1,unique_prod,nb_starting,14,14,0,-0,Free +1,unique_prod,nb_starting,15,15,0,0,Free +1,unique_prod,nb_starting,16,16,0,-0,Free +1,unique_prod,nb_starting,17,17,0,0,Free +1,unique_prod,nb_starting,18,18,0,-0,Free +1,unique_prod,nb_starting,19,19,0,0,Free +1,unique_prod,nb_starting,20,20,0,-0,Free +1,unique_prod,nb_starting,21,21,0,0,Free +1,unique_prod,nb_starting,22,22,0,-0,Free +1,unique_prod,nb_starting,23,23,0,0,Free +1,unique_prod,nb_starting,24,24,0,-0,Free +1,unique_prod,nb_starting,25,25,0,0,Free +1,unique_prod,nb_starting,26,26,0,-0,Free +1,unique_prod,nb_starting,27,27,0,0,Free +1,unique_prod,nb_starting,28,28,0,-0,Free +1,unique_prod,nb_starting,29,29,0,0,Free +1,unique_prod,nb_starting,30,30,0,-0,Free +1,unique_prod,nb_starting,31,31,0,0,Free +1,unique_prod,nb_starting,32,32,0,-0,Free +1,unique_prod,nb_starting,33,33,0,0,Free +1,unique_prod,nb_starting,34,34,0,-0,Free +1,unique_prod,nb_starting,35,35,0,0,Free +1,unique_prod,nb_starting,36,36,0,-0,Free +1,unique_prod,nb_starting,37,37,0,0,Free +1,unique_prod,nb_starting,38,38,0,1,Free +1,unique_prod,nb_starting,39,39,0,0,Free +1,unique_prod,nb_starting,40,40,0,1,Free +1,unique_prod,nb_starting,41,41,0,0,Free +1,unique_prod,nb_starting,42,42,0,1,Free +1,unique_prod,nb_starting,43,43,0,0,Free +1,unique_prod,nb_starting,44,44,0,1,Free +1,unique_prod,nb_starting,45,45,0,0,Free +1,unique_prod,nb_starting,46,46,0,1,Free +1,unique_prod,nb_starting,47,47,0,0,Free +1,unique_prod,nb_starting,48,48,0,-0,Free +1,unique_prod,nb_starting,49,49,0,0,Free +1,unique_prod,nb_starting,50,50,0,-0,Free +1,unique_prod,nb_starting,51,51,0,0,Free +1,unique_prod,nb_starting,52,52,0,-0,Free +1,unique_prod,nb_starting,53,53,0,0,Free +1,unique_prod,nb_starting,54,54,0,-0,Free +1,unique_prod,nb_starting,55,55,0,0,Free +1,unique_prod,nb_starting,56,56,0,-0,Free +1,unique_prod,nb_starting,57,57,0,0,Free +1,unique_prod,nb_starting,58,58,0,-0,Free +1,unique_prod,nb_starting,59,59,0,0,Free +1,unique_prod,nb_starting,60,60,0,-0,Free +1,unique_prod,nb_starting,61,61,0,0,Free +1,unique_prod,nb_starting,62,62,0,-0,Free +1,unique_prod,nb_starting,63,63,0,0,Free +1,unique_prod,nb_starting,64,64,0,-0,Free +1,unique_prod,nb_starting,65,65,0,0,Free +1,unique_prod,nb_starting,66,66,0,1,Free +1,unique_prod,nb_starting,67,67,0,0,Free +1,unique_prod,nb_starting,68,68,0,1,Free +1,unique_prod,nb_starting,69,69,0,0,Free +1,unique_prod,nb_starting,70,70,0,-0,Free +1,unique_prod,nb_starting,71,71,0,0,Free +1,unique_prod,nb_starting,72,72,0,-0,Free +1,unique_prod,nb_starting,73,73,0,0,Free +1,unique_prod,nb_starting,74,74,0,-0,Free +1,unique_prod,nb_starting,75,75,0,0,Free +1,unique_prod,nb_starting,76,76,0,-0,Free +1,unique_prod,nb_starting,77,77,0,0,Free +1,unique_prod,nb_starting,78,78,0,-0,Free +1,unique_prod,nb_starting,79,79,0,0,Free +1,unique_prod,nb_starting,80,80,0,-0,Free +1,unique_prod,nb_starting,81,81,0,0,Free +1,unique_prod,nb_starting,82,82,0,-0,Free +1,unique_prod,nb_starting,83,83,0,0,Free +1,unique_prod,nb_starting,84,84,0,-0,Free +1,unique_prod,nb_starting,85,85,0,0,Free +1,unique_prod,nb_starting,86,86,0,-0,Free +1,unique_prod,nb_starting,87,87,0,0,Free +1,unique_prod,nb_starting,88,88,0,1,Free +1,unique_prod,nb_starting,89,89,0,0,Free +1,unique_prod,nb_starting,90,90,0,-0,Free +1,unique_prod,nb_starting,91,91,0,0,Free +1,unique_prod,nb_starting,92,92,0,-0,Free +1,unique_prod,nb_starting,93,93,0,0,Free +1,unique_prod,nb_starting,94,94,0,-0,Free +1,unique_prod,nb_starting,95,95,0,0,Free +1,unique_prod,nb_starting,96,96,0,-0,Free +1,unique_prod,nb_starting,97,97,0,0,Free +1,unique_prod,nb_starting,98,98,0,-0,Free +1,unique_prod,nb_starting,99,99,0,0,Free +1,unique_prod,nb_starting,100,100,0,-0,Free +1,unique_prod,nb_starting,101,101,0,0,Free +1,unique_prod,nb_starting,102,102,0,-0,Free +1,unique_prod,nb_starting,103,103,0,0,Free +1,unique_prod,nb_starting,104,104,0,-0,Free +1,unique_prod,nb_starting,105,105,0,0,Free +1,unique_prod,nb_starting,106,106,0,-0,Free +1,unique_prod,nb_starting,107,107,0,0,Free +1,unique_prod,nb_starting,108,108,0,-0,Free +1,unique_prod,nb_starting,109,109,0,0,Free +1,unique_prod,nb_starting,110,110,0,-0,Free +1,unique_prod,nb_starting,111,111,0,0,Free +1,unique_prod,nb_starting,112,112,0,-0,Free +1,unique_prod,nb_starting,113,113,0,0,Free +1,unique_prod,nb_starting,114,114,0,-0,Free +1,unique_prod,nb_starting,115,115,0,0,Free +1,unique_prod,nb_starting,116,116,0,-0,Free +1,unique_prod,nb_starting,117,117,0,0,Free +1,unique_prod,nb_starting,118,118,0,1,Free +1,unique_prod,nb_starting,119,119,0,0,Free +1,unique_prod,nb_starting,120,120,0,1,Free +1,unique_prod,nb_starting,121,121,0,0,Free +1,unique_prod,nb_starting,122,122,0,1,Free +1,unique_prod,nb_starting,123,123,0,0,Free +1,unique_prod,nb_starting,124,124,0,1,Free +1,unique_prod,nb_starting,125,125,0,0,Free +1,unique_prod,nb_starting,126,126,0,-0,Free +1,unique_prod,nb_starting,127,127,0,0,Free +1,unique_prod,nb_starting,128,128,0,-0,Free +1,unique_prod,nb_starting,129,129,0,0,Free +1,unique_prod,nb_starting,130,130,0,-0,Free +1,unique_prod,nb_starting,131,131,0,0,Free +1,unique_prod,nb_starting,132,132,0,-0,Free +1,unique_prod,nb_starting,133,133,0,0,Free +1,unique_prod,nb_starting,134,134,0,-0,Free +1,unique_prod,nb_starting,135,135,0,0,Free +1,unique_prod,nb_starting,136,136,0,-0,Free +1,unique_prod,nb_starting,137,137,0,0,Free +1,unique_prod,nb_starting,138,138,0,-0,Free +1,unique_prod,nb_starting,139,139,0,0,Free +1,unique_prod,nb_starting,140,140,0,-0,Free +1,unique_prod,nb_starting,141,141,0,0,Free +1,unique_prod,nb_starting,142,142,0,-0,Free +1,unique_prod,nb_starting,143,143,0,0,Free +1,unique_prod,nb_starting,144,144,0,-0,Free +1,unique_prod,nb_starting,145,145,0,0,Free +1,unique_prod,nb_starting,146,146,0,-0,Free +1,unique_prod,nb_starting,147,147,0,0,Free +1,unique_prod,nb_starting,148,148,0,1,Free +1,unique_prod,nb_starting,149,149,0,0,Free +1,unique_prod,nb_starting,150,150,0,1,Free +1,unique_prod,nb_starting,151,151,0,0,Free +1,unique_prod,nb_starting,152,152,0,1,Free +1,unique_prod,nb_starting,153,153,0,0,Free +1,unique_prod,nb_starting,154,154,0,1,Free +1,unique_prod,nb_starting,155,155,0,0,Free +1,unique_prod,nb_starting,156,156,0,-0,Free +1,unique_prod,nb_starting,157,157,0,0,Free +1,unique_prod,nb_starting,158,158,0,-0,Free +1,unique_prod,nb_starting,159,159,0,0,Free +1,unique_prod,nb_starting,160,160,0,-0,Free +1,unique_prod,nb_starting,161,161,0,0,Free +1,unique_prod,nb_starting,162,162,0,-0,Free +1,unique_prod,nb_starting,163,163,0,0,Free +1,unique_prod,nb_starting,164,164,0,-0,Free +1,unique_prod,nb_starting,165,165,0,0,Free +1,unique_prod,nb_starting,166,166,0,1,Free +1,unique_prod,nb_starting,167,167,0,0,Free +1,unique_prod,nb_starting,168,168,0,-0,Free +1,unique_prod,nb_stopping,1,1,0,-0,Free +1,unique_prod,nb_stopping,2,2,0,0,Free +1,unique_prod,nb_stopping,3,3,0,-0,Free +1,unique_prod,nb_stopping,4,4,0,0,Free +1,unique_prod,nb_stopping,5,5,0,-0,Free +1,unique_prod,nb_stopping,6,6,0,0,Free +1,unique_prod,nb_stopping,7,7,0,-0,Free +1,unique_prod,nb_stopping,8,8,0,0,Free +1,unique_prod,nb_stopping,9,9,0,-0,Free +1,unique_prod,nb_stopping,10,10,0,0,Free +1,unique_prod,nb_stopping,11,11,0,-0,Free +1,unique_prod,nb_stopping,12,12,0,0,Free +1,unique_prod,nb_stopping,13,13,0,-0,Free +1,unique_prod,nb_stopping,14,14,0,0,Free +1,unique_prod,nb_stopping,15,15,0,-0,Free +1,unique_prod,nb_stopping,16,16,0,0,Free +1,unique_prod,nb_stopping,17,17,0,-0,Free +1,unique_prod,nb_stopping,18,18,0,0,Free +1,unique_prod,nb_stopping,19,19,0,-0,Free +1,unique_prod,nb_stopping,20,20,0,0,Free +1,unique_prod,nb_stopping,21,21,0,-0,Free +1,unique_prod,nb_stopping,22,22,0,0,Free +1,unique_prod,nb_stopping,23,23,0,-0,Free +1,unique_prod,nb_stopping,24,24,0,0,Free +1,unique_prod,nb_stopping,25,25,0,-0,Free +1,unique_prod,nb_stopping,26,26,0,0,Free +1,unique_prod,nb_stopping,27,27,0,-0,Free +1,unique_prod,nb_stopping,28,28,0,0,Free +1,unique_prod,nb_stopping,29,29,0,-0,Free +1,unique_prod,nb_stopping,30,30,0,0,Free +1,unique_prod,nb_stopping,31,31,0,-0,Free +1,unique_prod,nb_stopping,32,32,0,0,Free +1,unique_prod,nb_stopping,33,33,0,-0,Free +1,unique_prod,nb_stopping,34,34,0,0,Free +1,unique_prod,nb_stopping,35,35,0,-0,Free +1,unique_prod,nb_stopping,36,36,0,0,Free +1,unique_prod,nb_stopping,37,37,0,1,Free +1,unique_prod,nb_stopping,38,38,0,0,Free +1,unique_prod,nb_stopping,39,39,0,1,Free +1,unique_prod,nb_stopping,40,40,0,0,Free +1,unique_prod,nb_stopping,41,41,0,1,Free +1,unique_prod,nb_stopping,42,42,0,0,Free +1,unique_prod,nb_stopping,43,43,0,1,Free +1,unique_prod,nb_stopping,44,44,0,0,Free +1,unique_prod,nb_stopping,45,45,0,1,Free +1,unique_prod,nb_stopping,46,46,0,0,Free +1,unique_prod,nb_stopping,47,47,0,-0,Free +1,unique_prod,nb_stopping,48,48,0,0,Free +1,unique_prod,nb_stopping,49,49,0,-0,Free +1,unique_prod,nb_stopping,50,50,0,0,Free +1,unique_prod,nb_stopping,51,51,0,-0,Free +1,unique_prod,nb_stopping,52,52,0,0,Free +1,unique_prod,nb_stopping,53,53,0,-0,Free +1,unique_prod,nb_stopping,54,54,0,0,Free +1,unique_prod,nb_stopping,55,55,0,-0,Free +1,unique_prod,nb_stopping,56,56,0,0,Free +1,unique_prod,nb_stopping,57,57,0,-0,Free +1,unique_prod,nb_stopping,58,58,0,0,Free +1,unique_prod,nb_stopping,59,59,0,-0,Free +1,unique_prod,nb_stopping,60,60,0,0,Free +1,unique_prod,nb_stopping,61,61,0,-0,Free +1,unique_prod,nb_stopping,62,62,0,0,Free +1,unique_prod,nb_stopping,63,63,0,-0,Free +1,unique_prod,nb_stopping,64,64,0,0,Free +1,unique_prod,nb_stopping,65,65,0,1,Free +1,unique_prod,nb_stopping,66,66,0,0,Free +1,unique_prod,nb_stopping,67,67,0,1,Free +1,unique_prod,nb_stopping,68,68,0,0,Free +1,unique_prod,nb_stopping,69,69,0,-0,Free +1,unique_prod,nb_stopping,70,70,0,0,Free +1,unique_prod,nb_stopping,71,71,0,-0,Free +1,unique_prod,nb_stopping,72,72,0,0,Free +1,unique_prod,nb_stopping,73,73,0,-0,Free +1,unique_prod,nb_stopping,74,74,0,0,Free +1,unique_prod,nb_stopping,75,75,0,-0,Free +1,unique_prod,nb_stopping,76,76,0,0,Free +1,unique_prod,nb_stopping,77,77,0,-0,Free +1,unique_prod,nb_stopping,78,78,0,0,Free +1,unique_prod,nb_stopping,79,79,0,-0,Free +1,unique_prod,nb_stopping,80,80,0,0,Free +1,unique_prod,nb_stopping,81,81,0,-0,Free +1,unique_prod,nb_stopping,82,82,0,0,Free +1,unique_prod,nb_stopping,83,83,0,-0,Free +1,unique_prod,nb_stopping,84,84,0,0,Free +1,unique_prod,nb_stopping,85,85,0,-0,Free +1,unique_prod,nb_stopping,86,86,0,0,Free +1,unique_prod,nb_stopping,87,87,0,1,Free +1,unique_prod,nb_stopping,88,88,0,0,Free +1,unique_prod,nb_stopping,89,89,0,-0,Free +1,unique_prod,nb_stopping,90,90,0,0,Free +1,unique_prod,nb_stopping,91,91,0,-0,Free +1,unique_prod,nb_stopping,92,92,0,0,Free +1,unique_prod,nb_stopping,93,93,0,-0,Free +1,unique_prod,nb_stopping,94,94,0,0,Free +1,unique_prod,nb_stopping,95,95,0,-0,Free +1,unique_prod,nb_stopping,96,96,0,0,Free +1,unique_prod,nb_stopping,97,97,0,-0,Free +1,unique_prod,nb_stopping,98,98,0,0,Free +1,unique_prod,nb_stopping,99,99,0,-0,Free +1,unique_prod,nb_stopping,100,100,0,0,Free +1,unique_prod,nb_stopping,101,101,0,-0,Free +1,unique_prod,nb_stopping,102,102,0,0,Free +1,unique_prod,nb_stopping,103,103,0,-0,Free +1,unique_prod,nb_stopping,104,104,0,0,Free +1,unique_prod,nb_stopping,105,105,0,-0,Free +1,unique_prod,nb_stopping,106,106,0,0,Free +1,unique_prod,nb_stopping,107,107,0,-0,Free +1,unique_prod,nb_stopping,108,108,0,0,Free +1,unique_prod,nb_stopping,109,109,0,-0,Free +1,unique_prod,nb_stopping,110,110,0,0,Free +1,unique_prod,nb_stopping,111,111,0,-0,Free +1,unique_prod,nb_stopping,112,112,0,0,Free +1,unique_prod,nb_stopping,113,113,0,-0,Free +1,unique_prod,nb_stopping,114,114,0,0,Free +1,unique_prod,nb_stopping,115,115,0,-0,Free +1,unique_prod,nb_stopping,116,116,0,0,Free +1,unique_prod,nb_stopping,117,117,0,1,Free +1,unique_prod,nb_stopping,118,118,0,0,Free +1,unique_prod,nb_stopping,119,119,0,1,Free +1,unique_prod,nb_stopping,120,120,0,0,Free +1,unique_prod,nb_stopping,121,121,0,1,Free +1,unique_prod,nb_stopping,122,122,0,0,Free +1,unique_prod,nb_stopping,123,123,0,1,Free +1,unique_prod,nb_stopping,124,124,0,0,Free +1,unique_prod,nb_stopping,125,125,0,-0,Free +1,unique_prod,nb_stopping,126,126,0,0,Free +1,unique_prod,nb_stopping,127,127,0,-0,Free +1,unique_prod,nb_stopping,128,128,0,0,Free +1,unique_prod,nb_stopping,129,129,0,-0,Free +1,unique_prod,nb_stopping,130,130,0,0,Free +1,unique_prod,nb_stopping,131,131,0,-0,Free +1,unique_prod,nb_stopping,132,132,0,0,Free +1,unique_prod,nb_stopping,133,133,0,-0,Free +1,unique_prod,nb_stopping,134,134,0,0,Free +1,unique_prod,nb_stopping,135,135,0,-0,Free +1,unique_prod,nb_stopping,136,136,0,0,Free +1,unique_prod,nb_stopping,137,137,0,-0,Free +1,unique_prod,nb_stopping,138,138,0,0,Free +1,unique_prod,nb_stopping,139,139,0,-0,Free +1,unique_prod,nb_stopping,140,140,0,0,Free +1,unique_prod,nb_stopping,141,141,0,-0,Free +1,unique_prod,nb_stopping,142,142,0,0,Free +1,unique_prod,nb_stopping,143,143,0,-0,Free +1,unique_prod,nb_stopping,144,144,0,0,Free +1,unique_prod,nb_stopping,145,145,0,-0,Free +1,unique_prod,nb_stopping,146,146,0,0,Free +1,unique_prod,nb_stopping,147,147,0,1,Free +1,unique_prod,nb_stopping,148,148,0,0,Free +1,unique_prod,nb_stopping,149,149,0,1,Free +1,unique_prod,nb_stopping,150,150,0,0,Free +1,unique_prod,nb_stopping,151,151,0,1,Free +1,unique_prod,nb_stopping,152,152,0,0,Free +1,unique_prod,nb_stopping,153,153,0,1,Free +1,unique_prod,nb_stopping,154,154,0,0,Free +1,unique_prod,nb_stopping,155,155,0,-0,Free +1,unique_prod,nb_stopping,156,156,0,0,Free +1,unique_prod,nb_stopping,157,157,0,-0,Free +1,unique_prod,nb_stopping,158,158,0,0,Free +1,unique_prod,nb_stopping,159,159,0,-0,Free +1,unique_prod,nb_stopping,160,160,0,0,Free +1,unique_prod,nb_stopping,161,161,0,-0,Free +1,unique_prod,nb_stopping,162,162,0,0,Free +1,unique_prod,nb_stopping,163,163,0,-0,Free +1,unique_prod,nb_stopping,164,164,0,0,Free +1,unique_prod,nb_stopping,165,165,0,1,Free +1,unique_prod,nb_stopping,166,166,0,0,Free +1,unique_prod,nb_stopping,167,167,0,-0,Free +1,unique_prod,nb_stopping,168,168,0,0,Free +1,unique_prod,nb_failing,1,1,0,0,Free +1,unique_prod,nb_failing,2,2,0,0,Free +1,unique_prod,nb_failing,3,3,0,0,Free +1,unique_prod,nb_failing,4,4,0,0,Free +1,unique_prod,nb_failing,5,5,0,0,Free +1,unique_prod,nb_failing,6,6,0,0,Free +1,unique_prod,nb_failing,7,7,0,0,Free +1,unique_prod,nb_failing,8,8,0,0,Free +1,unique_prod,nb_failing,9,9,0,0,Free +1,unique_prod,nb_failing,10,10,0,0,Free +1,unique_prod,nb_failing,11,11,0,0,Free +1,unique_prod,nb_failing,12,12,0,0,Free +1,unique_prod,nb_failing,13,13,0,0,Free +1,unique_prod,nb_failing,14,14,0,0,Free +1,unique_prod,nb_failing,15,15,0,0,Free +1,unique_prod,nb_failing,16,16,0,0,Free +1,unique_prod,nb_failing,17,17,0,0,Free +1,unique_prod,nb_failing,18,18,0,0,Free +1,unique_prod,nb_failing,19,19,0,0,Free +1,unique_prod,nb_failing,20,20,0,0,Free +1,unique_prod,nb_failing,21,21,0,0,Free +1,unique_prod,nb_failing,22,22,0,0,Free +1,unique_prod,nb_failing,23,23,0,0,Free +1,unique_prod,nb_failing,24,24,0,0,Free +1,unique_prod,nb_failing,25,25,0,0,Free +1,unique_prod,nb_failing,26,26,0,0,Free +1,unique_prod,nb_failing,27,27,0,0,Free +1,unique_prod,nb_failing,28,28,0,0,Free +1,unique_prod,nb_failing,29,29,0,0,Free +1,unique_prod,nb_failing,30,30,0,0,Free +1,unique_prod,nb_failing,31,31,0,0,Free +1,unique_prod,nb_failing,32,32,0,0,Free +1,unique_prod,nb_failing,33,33,0,0,Free +1,unique_prod,nb_failing,34,34,0,0,Free +1,unique_prod,nb_failing,35,35,0,0,Free +1,unique_prod,nb_failing,36,36,0,0,Free +1,unique_prod,nb_failing,37,37,0,0,Free +1,unique_prod,nb_failing,38,38,0,0,Free +1,unique_prod,nb_failing,39,39,0,0,Free +1,unique_prod,nb_failing,40,40,0,0,Free +1,unique_prod,nb_failing,41,41,0,0,Free +1,unique_prod,nb_failing,42,42,0,0,Free +1,unique_prod,nb_failing,43,43,0,0,Free +1,unique_prod,nb_failing,44,44,0,0,Free +1,unique_prod,nb_failing,45,45,0,0,Free +1,unique_prod,nb_failing,46,46,0,0,Free +1,unique_prod,nb_failing,47,47,0,0,Free +1,unique_prod,nb_failing,48,48,0,0,Free +1,unique_prod,nb_failing,49,49,0,0,Free +1,unique_prod,nb_failing,50,50,0,0,Free +1,unique_prod,nb_failing,51,51,0,0,Free +1,unique_prod,nb_failing,52,52,0,0,Free +1,unique_prod,nb_failing,53,53,0,0,Free +1,unique_prod,nb_failing,54,54,0,0,Free +1,unique_prod,nb_failing,55,55,0,0,Free +1,unique_prod,nb_failing,56,56,0,0,Free +1,unique_prod,nb_failing,57,57,0,0,Free +1,unique_prod,nb_failing,58,58,0,0,Free +1,unique_prod,nb_failing,59,59,0,0,Free +1,unique_prod,nb_failing,60,60,0,0,Free +1,unique_prod,nb_failing,61,61,0,0,Free +1,unique_prod,nb_failing,62,62,0,0,Free +1,unique_prod,nb_failing,63,63,0,0,Free +1,unique_prod,nb_failing,64,64,0,0,Free +1,unique_prod,nb_failing,65,65,0,0,Free +1,unique_prod,nb_failing,66,66,0,0,Free +1,unique_prod,nb_failing,67,67,0,0,Free +1,unique_prod,nb_failing,68,68,0,0,Free +1,unique_prod,nb_failing,69,69,0,0,Free +1,unique_prod,nb_failing,70,70,0,0,Free +1,unique_prod,nb_failing,71,71,0,0,Free +1,unique_prod,nb_failing,72,72,0,0,Free +1,unique_prod,nb_failing,73,73,0,0,Free +1,unique_prod,nb_failing,74,74,0,0,Free +1,unique_prod,nb_failing,75,75,0,0,Free +1,unique_prod,nb_failing,76,76,0,0,Free +1,unique_prod,nb_failing,77,77,0,0,Free +1,unique_prod,nb_failing,78,78,0,0,Free +1,unique_prod,nb_failing,79,79,0,0,Free +1,unique_prod,nb_failing,80,80,0,0,Free +1,unique_prod,nb_failing,81,81,0,0,Free +1,unique_prod,nb_failing,82,82,0,0,Free +1,unique_prod,nb_failing,83,83,0,0,Free +1,unique_prod,nb_failing,84,84,0,0,Free +1,unique_prod,nb_failing,85,85,0,0,Free +1,unique_prod,nb_failing,86,86,0,0,Free +1,unique_prod,nb_failing,87,87,0,0,Free +1,unique_prod,nb_failing,88,88,0,0,Free +1,unique_prod,nb_failing,89,89,0,0,Free +1,unique_prod,nb_failing,90,90,0,0,Free +1,unique_prod,nb_failing,91,91,0,0,Free +1,unique_prod,nb_failing,92,92,0,0,Free +1,unique_prod,nb_failing,93,93,0,0,Free +1,unique_prod,nb_failing,94,94,0,0,Free +1,unique_prod,nb_failing,95,95,0,0,Free +1,unique_prod,nb_failing,96,96,0,0,Free +1,unique_prod,nb_failing,97,97,0,0,Free +1,unique_prod,nb_failing,98,98,0,0,Free +1,unique_prod,nb_failing,99,99,0,0,Free +1,unique_prod,nb_failing,100,100,0,0,Free +1,unique_prod,nb_failing,101,101,0,0,Free +1,unique_prod,nb_failing,102,102,0,0,Free +1,unique_prod,nb_failing,103,103,0,0,Free +1,unique_prod,nb_failing,104,104,0,0,Free +1,unique_prod,nb_failing,105,105,0,0,Free +1,unique_prod,nb_failing,106,106,0,0,Free +1,unique_prod,nb_failing,107,107,0,0,Free +1,unique_prod,nb_failing,108,108,0,0,Free +1,unique_prod,nb_failing,109,109,0,0,Free +1,unique_prod,nb_failing,110,110,0,0,Free +1,unique_prod,nb_failing,111,111,0,0,Free +1,unique_prod,nb_failing,112,112,0,0,Free +1,unique_prod,nb_failing,113,113,0,0,Free +1,unique_prod,nb_failing,114,114,0,0,Free +1,unique_prod,nb_failing,115,115,0,0,Free +1,unique_prod,nb_failing,116,116,0,0,Free +1,unique_prod,nb_failing,117,117,0,0,Free +1,unique_prod,nb_failing,118,118,0,0,Free +1,unique_prod,nb_failing,119,119,0,0,Free +1,unique_prod,nb_failing,120,120,0,0,Free +1,unique_prod,nb_failing,121,121,0,0,Free +1,unique_prod,nb_failing,122,122,0,0,Free +1,unique_prod,nb_failing,123,123,0,0,Free +1,unique_prod,nb_failing,124,124,0,0,Free +1,unique_prod,nb_failing,125,125,0,0,Free +1,unique_prod,nb_failing,126,126,0,0,Free +1,unique_prod,nb_failing,127,127,0,0,Free +1,unique_prod,nb_failing,128,128,0,0,Free +1,unique_prod,nb_failing,129,129,0,0,Free +1,unique_prod,nb_failing,130,130,0,0,Free +1,unique_prod,nb_failing,131,131,0,0,Free +1,unique_prod,nb_failing,132,132,0,0,Free +1,unique_prod,nb_failing,133,133,0,0,Free +1,unique_prod,nb_failing,134,134,0,0,Free +1,unique_prod,nb_failing,135,135,0,0,Free +1,unique_prod,nb_failing,136,136,0,0,Free +1,unique_prod,nb_failing,137,137,0,0,Free +1,unique_prod,nb_failing,138,138,0,0,Free +1,unique_prod,nb_failing,139,139,0,0,Free +1,unique_prod,nb_failing,140,140,0,0,Free +1,unique_prod,nb_failing,141,141,0,0,Free +1,unique_prod,nb_failing,142,142,0,0,Free +1,unique_prod,nb_failing,143,143,0,0,Free +1,unique_prod,nb_failing,144,144,0,0,Free +1,unique_prod,nb_failing,145,145,0,0,Free +1,unique_prod,nb_failing,146,146,0,0,Free +1,unique_prod,nb_failing,147,147,0,0,Free +1,unique_prod,nb_failing,148,148,0,0,Free +1,unique_prod,nb_failing,149,149,0,0,Free +1,unique_prod,nb_failing,150,150,0,0,Free +1,unique_prod,nb_failing,151,151,0,0,Free +1,unique_prod,nb_failing,152,152,0,0,Free +1,unique_prod,nb_failing,153,153,0,0,Free +1,unique_prod,nb_failing,154,154,0,0,Free +1,unique_prod,nb_failing,155,155,0,0,Free +1,unique_prod,nb_failing,156,156,0,0,Free +1,unique_prod,nb_failing,157,157,0,0,Free +1,unique_prod,nb_failing,158,158,0,0,Free +1,unique_prod,nb_failing,159,159,0,0,Free +1,unique_prod,nb_failing,160,160,0,0,Free +1,unique_prod,nb_failing,161,161,0,0,Free +1,unique_prod,nb_failing,162,162,0,0,Free +1,unique_prod,nb_failing,163,163,0,0,Free +1,unique_prod,nb_failing,164,164,0,0,Free +1,unique_prod,nb_failing,165,165,0,0,Free +1,unique_prod,nb_failing,166,166,0,0,Free +1,unique_prod,nb_failing,167,167,0,0,Free +1,unique_prod,nb_failing,168,168,0,0,Free +1,unique_prod,max_generation,1,1,0,None,Free +1,unique_prod,max_generation,2,2,0,None,Free +1,unique_prod,max_generation,3,3,0,None,Free +1,unique_prod,max_generation,4,4,0,None,Free +1,unique_prod,max_generation,5,5,0,None,Free +1,unique_prod,max_generation,6,6,0,None,Free +1,unique_prod,max_generation,7,7,0,None,Free +1,unique_prod,max_generation,8,8,0,None,Free +1,unique_prod,max_generation,9,9,0,None,Free +1,unique_prod,max_generation,10,10,0,None,Free +1,unique_prod,max_generation,11,11,0,None,Free +1,unique_prod,max_generation,12,12,0,None,Free +1,unique_prod,max_generation,13,13,0,None,Free +1,unique_prod,max_generation,14,14,0,None,Free +1,unique_prod,max_generation,15,15,0,None,Free +1,unique_prod,max_generation,16,16,0,None,Free +1,unique_prod,max_generation,17,17,0,None,Free +1,unique_prod,max_generation,18,18,0,None,Free +1,unique_prod,max_generation,19,19,0,None,Free +1,unique_prod,max_generation,20,20,0,None,Free +1,unique_prod,max_generation,21,21,0,None,Free +1,unique_prod,max_generation,22,22,0,None,Free +1,unique_prod,max_generation,23,23,0,None,Free +1,unique_prod,max_generation,24,24,0,None,Free +1,unique_prod,max_generation,25,25,0,None,Free +1,unique_prod,max_generation,26,26,0,None,Free +1,unique_prod,max_generation,27,27,0,None,Free +1,unique_prod,max_generation,28,28,0,None,Free +1,unique_prod,max_generation,29,29,0,None,Free +1,unique_prod,max_generation,30,30,0,None,Free +1,unique_prod,max_generation,31,31,0,None,Free +1,unique_prod,max_generation,32,32,0,None,Free +1,unique_prod,max_generation,33,33,0,None,Free +1,unique_prod,max_generation,34,34,0,None,Free +1,unique_prod,max_generation,35,35,0,None,Free +1,unique_prod,max_generation,36,36,0,None,Free +1,unique_prod,max_generation,37,37,0,None,Free +1,unique_prod,max_generation,38,38,0,None,Free +1,unique_prod,max_generation,39,39,0,None,Free +1,unique_prod,max_generation,40,40,0,None,Free +1,unique_prod,max_generation,41,41,0,None,Free +1,unique_prod,max_generation,42,42,0,None,Free +1,unique_prod,max_generation,43,43,0,None,Free +1,unique_prod,max_generation,44,44,0,None,Free +1,unique_prod,max_generation,45,45,0,None,Free +1,unique_prod,max_generation,46,46,0,None,Free +1,unique_prod,max_generation,47,47,0,None,Free +1,unique_prod,max_generation,48,48,0,None,Free +1,unique_prod,max_generation,49,49,0,None,Free +1,unique_prod,max_generation,50,50,0,None,Free +1,unique_prod,max_generation,51,51,0,None,Free +1,unique_prod,max_generation,52,52,0,None,Free +1,unique_prod,max_generation,53,53,0,None,Free +1,unique_prod,max_generation,54,54,0,None,Free +1,unique_prod,max_generation,55,55,0,None,Free +1,unique_prod,max_generation,56,56,0,None,Free +1,unique_prod,max_generation,57,57,0,None,Free +1,unique_prod,max_generation,58,58,0,None,Free +1,unique_prod,max_generation,59,59,0,None,Free +1,unique_prod,max_generation,60,60,0,None,Free +1,unique_prod,max_generation,61,61,0,None,Free +1,unique_prod,max_generation,62,62,0,None,Free +1,unique_prod,max_generation,63,63,0,None,Free +1,unique_prod,max_generation,64,64,0,None,Free +1,unique_prod,max_generation,65,65,0,None,Free +1,unique_prod,max_generation,66,66,0,None,Free +1,unique_prod,max_generation,67,67,0,None,Free +1,unique_prod,max_generation,68,68,0,None,Free +1,unique_prod,max_generation,69,69,0,None,Free +1,unique_prod,max_generation,70,70,0,None,Free +1,unique_prod,max_generation,71,71,0,None,Free +1,unique_prod,max_generation,72,72,0,None,Free +1,unique_prod,max_generation,73,73,0,None,Free +1,unique_prod,max_generation,74,74,0,None,Free +1,unique_prod,max_generation,75,75,0,None,Free +1,unique_prod,max_generation,76,76,0,None,Free +1,unique_prod,max_generation,77,77,0,None,Free +1,unique_prod,max_generation,78,78,0,None,Free +1,unique_prod,max_generation,79,79,0,None,Free +1,unique_prod,max_generation,80,80,0,None,Free +1,unique_prod,max_generation,81,81,0,None,Free +1,unique_prod,max_generation,82,82,0,None,Free +1,unique_prod,max_generation,83,83,0,None,Free +1,unique_prod,max_generation,84,84,0,None,Free +1,unique_prod,max_generation,85,85,0,None,Free +1,unique_prod,max_generation,86,86,0,None,Free +1,unique_prod,max_generation,87,87,0,None,Free +1,unique_prod,max_generation,88,88,0,None,Free +1,unique_prod,max_generation,89,89,0,None,Free +1,unique_prod,max_generation,90,90,0,None,Free +1,unique_prod,max_generation,91,91,0,None,Free +1,unique_prod,max_generation,92,92,0,None,Free +1,unique_prod,max_generation,93,93,0,None,Free +1,unique_prod,max_generation,94,94,0,None,Free +1,unique_prod,max_generation,95,95,0,None,Free +1,unique_prod,max_generation,96,96,0,None,Free +1,unique_prod,max_generation,97,97,0,None,Free +1,unique_prod,max_generation,98,98,0,None,Free +1,unique_prod,max_generation,99,99,0,None,Free +1,unique_prod,max_generation,100,100,0,None,Free +1,unique_prod,max_generation,101,101,0,None,Free +1,unique_prod,max_generation,102,102,0,None,Free +1,unique_prod,max_generation,103,103,0,None,Free +1,unique_prod,max_generation,104,104,0,None,Free +1,unique_prod,max_generation,105,105,0,None,Free +1,unique_prod,max_generation,106,106,0,None,Free +1,unique_prod,max_generation,107,107,0,None,Free +1,unique_prod,max_generation,108,108,0,None,Free +1,unique_prod,max_generation,109,109,0,None,Free +1,unique_prod,max_generation,110,110,0,None,Free +1,unique_prod,max_generation,111,111,0,None,Free +1,unique_prod,max_generation,112,112,0,None,Free +1,unique_prod,max_generation,113,113,0,None,Free +1,unique_prod,max_generation,114,114,0,None,Free +1,unique_prod,max_generation,115,115,0,None,Free +1,unique_prod,max_generation,116,116,0,None,Free +1,unique_prod,max_generation,117,117,0,None,Free +1,unique_prod,max_generation,118,118,0,None,Free +1,unique_prod,max_generation,119,119,0,None,Free +1,unique_prod,max_generation,120,120,0,None,Free +1,unique_prod,max_generation,121,121,0,None,Free +1,unique_prod,max_generation,122,122,0,None,Free +1,unique_prod,max_generation,123,123,0,None,Free +1,unique_prod,max_generation,124,124,0,None,Free +1,unique_prod,max_generation,125,125,0,None,Free +1,unique_prod,max_generation,126,126,0,None,Free +1,unique_prod,max_generation,127,127,0,None,Free +1,unique_prod,max_generation,128,128,0,None,Free +1,unique_prod,max_generation,129,129,0,None,Free +1,unique_prod,max_generation,130,130,0,None,Free +1,unique_prod,max_generation,131,131,0,None,Free +1,unique_prod,max_generation,132,132,0,None,Free +1,unique_prod,max_generation,133,133,0,None,Free +1,unique_prod,max_generation,134,134,0,None,Free +1,unique_prod,max_generation,135,135,0,None,Free +1,unique_prod,max_generation,136,136,0,None,Free +1,unique_prod,max_generation,137,137,0,None,Free +1,unique_prod,max_generation,138,138,0,None,Free +1,unique_prod,max_generation,139,139,0,None,Free +1,unique_prod,max_generation,140,140,0,None,Free +1,unique_prod,max_generation,141,141,0,None,Free +1,unique_prod,max_generation,142,142,0,None,Free +1,unique_prod,max_generation,143,143,0,None,Free +1,unique_prod,max_generation,144,144,0,None,Free +1,unique_prod,max_generation,145,145,0,None,Free +1,unique_prod,max_generation,146,146,0,None,Free +1,unique_prod,max_generation,147,147,0,None,Free +1,unique_prod,max_generation,148,148,0,None,Free +1,unique_prod,max_generation,149,149,0,None,Free +1,unique_prod,max_generation,150,150,0,None,Free +1,unique_prod,max_generation,151,151,0,None,Free +1,unique_prod,max_generation,152,152,0,None,Free +1,unique_prod,max_generation,153,153,0,None,Free +1,unique_prod,max_generation,154,154,0,None,Free +1,unique_prod,max_generation,155,155,0,None,Free +1,unique_prod,max_generation,156,156,0,None,Free +1,unique_prod,max_generation,157,157,0,None,Free +1,unique_prod,max_generation,158,158,0,None,Free +1,unique_prod,max_generation,159,159,0,None,Free +1,unique_prod,max_generation,160,160,0,None,Free +1,unique_prod,max_generation,161,161,0,None,Free +1,unique_prod,max_generation,162,162,0,None,Free +1,unique_prod,max_generation,163,163,0,None,Free +1,unique_prod,max_generation,164,164,0,None,Free +1,unique_prod,max_generation,165,165,0,None,Free +1,unique_prod,max_generation,166,166,0,None,Free +1,unique_prod,max_generation,167,167,0,None,Free +1,unique_prod,max_generation,168,168,0,None,Free +1,unique_prod,min_generation,1,1,0,None,Free +1,unique_prod,min_generation,2,2,0,None,Free +1,unique_prod,min_generation,3,3,0,None,Free +1,unique_prod,min_generation,4,4,0,None,Free +1,unique_prod,min_generation,5,5,0,None,Free +1,unique_prod,min_generation,6,6,0,None,Free +1,unique_prod,min_generation,7,7,0,None,Free +1,unique_prod,min_generation,8,8,0,None,Free +1,unique_prod,min_generation,9,9,0,None,Free +1,unique_prod,min_generation,10,10,0,None,Free +1,unique_prod,min_generation,11,11,0,None,Free +1,unique_prod,min_generation,12,12,0,None,Free +1,unique_prod,min_generation,13,13,0,None,Free +1,unique_prod,min_generation,14,14,0,None,Free +1,unique_prod,min_generation,15,15,0,None,Free +1,unique_prod,min_generation,16,16,0,None,Free +1,unique_prod,min_generation,17,17,0,None,Free +1,unique_prod,min_generation,18,18,0,None,Free +1,unique_prod,min_generation,19,19,0,None,Free +1,unique_prod,min_generation,20,20,0,None,Free +1,unique_prod,min_generation,21,21,0,None,Free +1,unique_prod,min_generation,22,22,0,None,Free +1,unique_prod,min_generation,23,23,0,None,Free +1,unique_prod,min_generation,24,24,0,None,Free +1,unique_prod,min_generation,25,25,0,None,Free +1,unique_prod,min_generation,26,26,0,None,Free +1,unique_prod,min_generation,27,27,0,None,Free +1,unique_prod,min_generation,28,28,0,None,Free +1,unique_prod,min_generation,29,29,0,None,Free +1,unique_prod,min_generation,30,30,0,None,Free +1,unique_prod,min_generation,31,31,0,None,Free +1,unique_prod,min_generation,32,32,0,None,Free +1,unique_prod,min_generation,33,33,0,None,Free +1,unique_prod,min_generation,34,34,0,None,Free +1,unique_prod,min_generation,35,35,0,None,Free +1,unique_prod,min_generation,36,36,0,None,Free +1,unique_prod,min_generation,37,37,0,None,Free +1,unique_prod,min_generation,38,38,0,None,Free +1,unique_prod,min_generation,39,39,0,None,Free +1,unique_prod,min_generation,40,40,0,None,Free +1,unique_prod,min_generation,41,41,0,None,Free +1,unique_prod,min_generation,42,42,0,None,Free +1,unique_prod,min_generation,43,43,0,None,Free +1,unique_prod,min_generation,44,44,0,None,Free +1,unique_prod,min_generation,45,45,0,None,Free +1,unique_prod,min_generation,46,46,0,None,Free +1,unique_prod,min_generation,47,47,0,None,Free +1,unique_prod,min_generation,48,48,0,None,Free +1,unique_prod,min_generation,49,49,0,None,Free +1,unique_prod,min_generation,50,50,0,None,Free +1,unique_prod,min_generation,51,51,0,None,Free +1,unique_prod,min_generation,52,52,0,None,Free +1,unique_prod,min_generation,53,53,0,None,Free +1,unique_prod,min_generation,54,54,0,None,Free +1,unique_prod,min_generation,55,55,0,None,Free +1,unique_prod,min_generation,56,56,0,None,Free +1,unique_prod,min_generation,57,57,0,None,Free +1,unique_prod,min_generation,58,58,0,None,Free +1,unique_prod,min_generation,59,59,0,None,Free +1,unique_prod,min_generation,60,60,0,None,Free +1,unique_prod,min_generation,61,61,0,None,Free +1,unique_prod,min_generation,62,62,0,None,Free +1,unique_prod,min_generation,63,63,0,None,Free +1,unique_prod,min_generation,64,64,0,None,Free +1,unique_prod,min_generation,65,65,0,None,Free +1,unique_prod,min_generation,66,66,0,None,Free +1,unique_prod,min_generation,67,67,0,None,Free +1,unique_prod,min_generation,68,68,0,None,Free +1,unique_prod,min_generation,69,69,0,None,Free +1,unique_prod,min_generation,70,70,0,None,Free +1,unique_prod,min_generation,71,71,0,None,Free +1,unique_prod,min_generation,72,72,0,None,Free +1,unique_prod,min_generation,73,73,0,None,Free +1,unique_prod,min_generation,74,74,0,None,Free +1,unique_prod,min_generation,75,75,0,None,Free +1,unique_prod,min_generation,76,76,0,None,Free +1,unique_prod,min_generation,77,77,0,None,Free +1,unique_prod,min_generation,78,78,0,None,Free +1,unique_prod,min_generation,79,79,0,None,Free +1,unique_prod,min_generation,80,80,0,None,Free +1,unique_prod,min_generation,81,81,0,None,Free +1,unique_prod,min_generation,82,82,0,None,Free +1,unique_prod,min_generation,83,83,0,None,Free +1,unique_prod,min_generation,84,84,0,None,Free +1,unique_prod,min_generation,85,85,0,None,Free +1,unique_prod,min_generation,86,86,0,None,Free +1,unique_prod,min_generation,87,87,0,None,Free +1,unique_prod,min_generation,88,88,0,None,Free +1,unique_prod,min_generation,89,89,0,None,Free +1,unique_prod,min_generation,90,90,0,None,Free +1,unique_prod,min_generation,91,91,0,None,Free +1,unique_prod,min_generation,92,92,0,None,Free +1,unique_prod,min_generation,93,93,0,None,Free +1,unique_prod,min_generation,94,94,0,None,Free +1,unique_prod,min_generation,95,95,0,None,Free +1,unique_prod,min_generation,96,96,0,None,Free +1,unique_prod,min_generation,97,97,0,None,Free +1,unique_prod,min_generation,98,98,0,None,Free +1,unique_prod,min_generation,99,99,0,None,Free +1,unique_prod,min_generation,100,100,0,None,Free +1,unique_prod,min_generation,101,101,0,None,Free +1,unique_prod,min_generation,102,102,0,None,Free +1,unique_prod,min_generation,103,103,0,None,Free +1,unique_prod,min_generation,104,104,0,None,Free +1,unique_prod,min_generation,105,105,0,None,Free +1,unique_prod,min_generation,106,106,0,None,Free +1,unique_prod,min_generation,107,107,0,None,Free +1,unique_prod,min_generation,108,108,0,None,Free +1,unique_prod,min_generation,109,109,0,None,Free +1,unique_prod,min_generation,110,110,0,None,Free +1,unique_prod,min_generation,111,111,0,None,Free +1,unique_prod,min_generation,112,112,0,None,Free +1,unique_prod,min_generation,113,113,0,None,Free +1,unique_prod,min_generation,114,114,0,None,Free +1,unique_prod,min_generation,115,115,0,None,Free +1,unique_prod,min_generation,116,116,0,None,Free +1,unique_prod,min_generation,117,117,0,None,Free +1,unique_prod,min_generation,118,118,0,None,Free +1,unique_prod,min_generation,119,119,0,None,Free +1,unique_prod,min_generation,120,120,0,None,Free +1,unique_prod,min_generation,121,121,0,None,Free +1,unique_prod,min_generation,122,122,0,None,Free +1,unique_prod,min_generation,123,123,0,None,Free +1,unique_prod,min_generation,124,124,0,None,Free +1,unique_prod,min_generation,125,125,0,None,Free +1,unique_prod,min_generation,126,126,0,None,Free +1,unique_prod,min_generation,127,127,0,None,Free +1,unique_prod,min_generation,128,128,0,None,Free +1,unique_prod,min_generation,129,129,0,None,Free +1,unique_prod,min_generation,130,130,0,None,Free +1,unique_prod,min_generation,131,131,0,None,Free +1,unique_prod,min_generation,132,132,0,None,Free +1,unique_prod,min_generation,133,133,0,None,Free +1,unique_prod,min_generation,134,134,0,None,Free +1,unique_prod,min_generation,135,135,0,None,Free +1,unique_prod,min_generation,136,136,0,None,Free +1,unique_prod,min_generation,137,137,0,None,Free +1,unique_prod,min_generation,138,138,0,None,Free +1,unique_prod,min_generation,139,139,0,None,Free +1,unique_prod,min_generation,140,140,0,None,Free +1,unique_prod,min_generation,141,141,0,None,Free +1,unique_prod,min_generation,142,142,0,None,Free +1,unique_prod,min_generation,143,143,0,None,Free +1,unique_prod,min_generation,144,144,0,None,Free +1,unique_prod,min_generation,145,145,0,None,Free +1,unique_prod,min_generation,146,146,0,None,Free +1,unique_prod,min_generation,147,147,0,None,Free +1,unique_prod,min_generation,148,148,0,None,Free +1,unique_prod,min_generation,149,149,0,None,Free +1,unique_prod,min_generation,150,150,0,None,Free +1,unique_prod,min_generation,151,151,0,None,Free +1,unique_prod,min_generation,152,152,0,None,Free +1,unique_prod,min_generation,153,153,0,None,Free +1,unique_prod,min_generation,154,154,0,None,Free +1,unique_prod,min_generation,155,155,0,None,Free +1,unique_prod,min_generation,156,156,0,None,Free +1,unique_prod,min_generation,157,157,0,None,Free +1,unique_prod,min_generation,158,158,0,None,Free +1,unique_prod,min_generation,159,159,0,None,Free +1,unique_prod,min_generation,160,160,0,None,Free +1,unique_prod,min_generation,161,161,0,None,Free +1,unique_prod,min_generation,162,162,0,None,Free +1,unique_prod,min_generation,163,163,0,None,Free +1,unique_prod,min_generation,164,164,0,None,Free +1,unique_prod,min_generation,165,165,0,None,Free +1,unique_prod,min_generation,166,166,0,None,Free +1,unique_prod,min_generation,167,167,0,None,Free +1,unique_prod,min_generation,168,168,0,None,Free +1,unique_prod,on_units_dynamics,1,1,0,None,Free +1,unique_prod,on_units_dynamics,2,2,0,None,Free +1,unique_prod,on_units_dynamics,3,3,0,None,Free +1,unique_prod,on_units_dynamics,4,4,0,None,Free +1,unique_prod,on_units_dynamics,5,5,0,None,Free +1,unique_prod,on_units_dynamics,6,6,0,None,Free +1,unique_prod,on_units_dynamics,7,7,0,None,Free +1,unique_prod,on_units_dynamics,8,8,0,None,Free +1,unique_prod,on_units_dynamics,9,9,0,None,Free +1,unique_prod,on_units_dynamics,10,10,0,None,Free +1,unique_prod,on_units_dynamics,11,11,0,None,Free +1,unique_prod,on_units_dynamics,12,12,0,None,Free +1,unique_prod,on_units_dynamics,13,13,0,None,Free +1,unique_prod,on_units_dynamics,14,14,0,None,Free +1,unique_prod,on_units_dynamics,15,15,0,None,Free +1,unique_prod,on_units_dynamics,16,16,0,None,Free +1,unique_prod,on_units_dynamics,17,17,0,None,Free +1,unique_prod,on_units_dynamics,18,18,0,None,Free +1,unique_prod,on_units_dynamics,19,19,0,None,Free +1,unique_prod,on_units_dynamics,20,20,0,None,Free +1,unique_prod,on_units_dynamics,21,21,0,None,Free +1,unique_prod,on_units_dynamics,22,22,0,None,Free +1,unique_prod,on_units_dynamics,23,23,0,None,Free +1,unique_prod,on_units_dynamics,24,24,0,None,Free +1,unique_prod,on_units_dynamics,25,25,0,None,Free +1,unique_prod,on_units_dynamics,26,26,0,None,Free +1,unique_prod,on_units_dynamics,27,27,0,None,Free +1,unique_prod,on_units_dynamics,28,28,0,None,Free +1,unique_prod,on_units_dynamics,29,29,0,None,Free +1,unique_prod,on_units_dynamics,30,30,0,None,Free +1,unique_prod,on_units_dynamics,31,31,0,None,Free +1,unique_prod,on_units_dynamics,32,32,0,None,Free +1,unique_prod,on_units_dynamics,33,33,0,None,Free +1,unique_prod,on_units_dynamics,34,34,0,None,Free +1,unique_prod,on_units_dynamics,35,35,0,None,Free +1,unique_prod,on_units_dynamics,36,36,0,None,Free +1,unique_prod,on_units_dynamics,37,37,0,None,Free +1,unique_prod,on_units_dynamics,38,38,0,None,Free +1,unique_prod,on_units_dynamics,39,39,0,None,Free +1,unique_prod,on_units_dynamics,40,40,0,None,Free +1,unique_prod,on_units_dynamics,41,41,0,None,Free +1,unique_prod,on_units_dynamics,42,42,0,None,Free +1,unique_prod,on_units_dynamics,43,43,0,None,Free +1,unique_prod,on_units_dynamics,44,44,0,None,Free +1,unique_prod,on_units_dynamics,45,45,0,None,Free +1,unique_prod,on_units_dynamics,46,46,0,None,Free +1,unique_prod,on_units_dynamics,47,47,0,None,Free +1,unique_prod,on_units_dynamics,48,48,0,None,Free +1,unique_prod,on_units_dynamics,49,49,0,None,Free +1,unique_prod,on_units_dynamics,50,50,0,None,Free +1,unique_prod,on_units_dynamics,51,51,0,None,Free +1,unique_prod,on_units_dynamics,52,52,0,None,Free +1,unique_prod,on_units_dynamics,53,53,0,None,Free +1,unique_prod,on_units_dynamics,54,54,0,None,Free +1,unique_prod,on_units_dynamics,55,55,0,None,Free +1,unique_prod,on_units_dynamics,56,56,0,None,Free +1,unique_prod,on_units_dynamics,57,57,0,None,Free +1,unique_prod,on_units_dynamics,58,58,0,None,Free +1,unique_prod,on_units_dynamics,59,59,0,None,Free +1,unique_prod,on_units_dynamics,60,60,0,None,Free +1,unique_prod,on_units_dynamics,61,61,0,None,Free +1,unique_prod,on_units_dynamics,62,62,0,None,Free +1,unique_prod,on_units_dynamics,63,63,0,None,Free +1,unique_prod,on_units_dynamics,64,64,0,None,Free +1,unique_prod,on_units_dynamics,65,65,0,None,Free +1,unique_prod,on_units_dynamics,66,66,0,None,Free +1,unique_prod,on_units_dynamics,67,67,0,None,Free +1,unique_prod,on_units_dynamics,68,68,0,None,Free +1,unique_prod,on_units_dynamics,69,69,0,None,Free +1,unique_prod,on_units_dynamics,70,70,0,None,Free +1,unique_prod,on_units_dynamics,71,71,0,None,Free +1,unique_prod,on_units_dynamics,72,72,0,None,Free +1,unique_prod,on_units_dynamics,73,73,0,None,Free +1,unique_prod,on_units_dynamics,74,74,0,None,Free +1,unique_prod,on_units_dynamics,75,75,0,None,Free +1,unique_prod,on_units_dynamics,76,76,0,None,Free +1,unique_prod,on_units_dynamics,77,77,0,None,Free +1,unique_prod,on_units_dynamics,78,78,0,None,Free +1,unique_prod,on_units_dynamics,79,79,0,None,Free +1,unique_prod,on_units_dynamics,80,80,0,None,Free +1,unique_prod,on_units_dynamics,81,81,0,None,Free +1,unique_prod,on_units_dynamics,82,82,0,None,Free +1,unique_prod,on_units_dynamics,83,83,0,None,Free +1,unique_prod,on_units_dynamics,84,84,0,None,Free +1,unique_prod,on_units_dynamics,85,85,0,None,Free +1,unique_prod,on_units_dynamics,86,86,0,None,Free +1,unique_prod,on_units_dynamics,87,87,0,None,Free +1,unique_prod,on_units_dynamics,88,88,0,None,Free +1,unique_prod,on_units_dynamics,89,89,0,None,Free +1,unique_prod,on_units_dynamics,90,90,0,None,Free +1,unique_prod,on_units_dynamics,91,91,0,None,Free +1,unique_prod,on_units_dynamics,92,92,0,None,Free +1,unique_prod,on_units_dynamics,93,93,0,None,Free +1,unique_prod,on_units_dynamics,94,94,0,None,Free +1,unique_prod,on_units_dynamics,95,95,0,None,Free +1,unique_prod,on_units_dynamics,96,96,0,None,Free +1,unique_prod,on_units_dynamics,97,97,0,None,Free +1,unique_prod,on_units_dynamics,98,98,0,None,Free +1,unique_prod,on_units_dynamics,99,99,0,None,Free +1,unique_prod,on_units_dynamics,100,100,0,None,Free +1,unique_prod,on_units_dynamics,101,101,0,None,Free +1,unique_prod,on_units_dynamics,102,102,0,None,Free +1,unique_prod,on_units_dynamics,103,103,0,None,Free +1,unique_prod,on_units_dynamics,104,104,0,None,Free +1,unique_prod,on_units_dynamics,105,105,0,None,Free +1,unique_prod,on_units_dynamics,106,106,0,None,Free +1,unique_prod,on_units_dynamics,107,107,0,None,Free +1,unique_prod,on_units_dynamics,108,108,0,None,Free +1,unique_prod,on_units_dynamics,109,109,0,None,Free +1,unique_prod,on_units_dynamics,110,110,0,None,Free +1,unique_prod,on_units_dynamics,111,111,0,None,Free +1,unique_prod,on_units_dynamics,112,112,0,None,Free +1,unique_prod,on_units_dynamics,113,113,0,None,Free +1,unique_prod,on_units_dynamics,114,114,0,None,Free +1,unique_prod,on_units_dynamics,115,115,0,None,Free +1,unique_prod,on_units_dynamics,116,116,0,None,Free +1,unique_prod,on_units_dynamics,117,117,0,None,Free +1,unique_prod,on_units_dynamics,118,118,0,None,Free +1,unique_prod,on_units_dynamics,119,119,0,None,Free +1,unique_prod,on_units_dynamics,120,120,0,None,Free +1,unique_prod,on_units_dynamics,121,121,0,None,Free +1,unique_prod,on_units_dynamics,122,122,0,None,Free +1,unique_prod,on_units_dynamics,123,123,0,None,Free +1,unique_prod,on_units_dynamics,124,124,0,None,Free +1,unique_prod,on_units_dynamics,125,125,0,None,Free +1,unique_prod,on_units_dynamics,126,126,0,None,Free +1,unique_prod,on_units_dynamics,127,127,0,None,Free +1,unique_prod,on_units_dynamics,128,128,0,None,Free +1,unique_prod,on_units_dynamics,129,129,0,None,Free +1,unique_prod,on_units_dynamics,130,130,0,None,Free +1,unique_prod,on_units_dynamics,131,131,0,None,Free +1,unique_prod,on_units_dynamics,132,132,0,None,Free +1,unique_prod,on_units_dynamics,133,133,0,None,Free +1,unique_prod,on_units_dynamics,134,134,0,None,Free +1,unique_prod,on_units_dynamics,135,135,0,None,Free +1,unique_prod,on_units_dynamics,136,136,0,None,Free +1,unique_prod,on_units_dynamics,137,137,0,None,Free +1,unique_prod,on_units_dynamics,138,138,0,None,Free +1,unique_prod,on_units_dynamics,139,139,0,None,Free +1,unique_prod,on_units_dynamics,140,140,0,None,Free +1,unique_prod,on_units_dynamics,141,141,0,None,Free +1,unique_prod,on_units_dynamics,142,142,0,None,Free +1,unique_prod,on_units_dynamics,143,143,0,None,Free +1,unique_prod,on_units_dynamics,144,144,0,None,Free +1,unique_prod,on_units_dynamics,145,145,0,None,Free +1,unique_prod,on_units_dynamics,146,146,0,None,Free +1,unique_prod,on_units_dynamics,147,147,0,None,Free +1,unique_prod,on_units_dynamics,148,148,0,None,Free +1,unique_prod,on_units_dynamics,149,149,0,None,Free +1,unique_prod,on_units_dynamics,150,150,0,None,Free +1,unique_prod,on_units_dynamics,151,151,0,None,Free +1,unique_prod,on_units_dynamics,152,152,0,None,Free +1,unique_prod,on_units_dynamics,153,153,0,None,Free +1,unique_prod,on_units_dynamics,154,154,0,None,Free +1,unique_prod,on_units_dynamics,155,155,0,None,Free +1,unique_prod,on_units_dynamics,156,156,0,None,Free +1,unique_prod,on_units_dynamics,157,157,0,None,Free +1,unique_prod,on_units_dynamics,158,158,0,None,Free +1,unique_prod,on_units_dynamics,159,159,0,None,Free +1,unique_prod,on_units_dynamics,160,160,0,None,Free +1,unique_prod,on_units_dynamics,161,161,0,None,Free +1,unique_prod,on_units_dynamics,162,162,0,None,Free +1,unique_prod,on_units_dynamics,163,163,0,None,Free +1,unique_prod,on_units_dynamics,164,164,0,None,Free +1,unique_prod,on_units_dynamics,165,165,0,None,Free +1,unique_prod,on_units_dynamics,166,166,0,None,Free +1,unique_prod,on_units_dynamics,167,167,0,None,Free +1,unique_prod,on_units_dynamics,168,168,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,1,1,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,2,2,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,3,3,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,4,4,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,5,5,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,6,6,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,7,7,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,8,8,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,9,9,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,10,10,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,11,11,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,12,12,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,13,13,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,14,14,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,15,15,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,16,16,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,17,17,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,18,18,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,19,19,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,20,20,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,21,21,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,22,22,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,23,23,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,24,24,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,25,25,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,26,26,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,27,27,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,28,28,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,29,29,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,30,30,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,31,31,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,32,32,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,33,33,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,34,34,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,35,35,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,36,36,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,37,37,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,38,38,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,39,39,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,40,40,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,41,41,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,42,42,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,43,43,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,44,44,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,45,45,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,46,46,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,47,47,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,48,48,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,49,49,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,50,50,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,51,51,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,52,52,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,53,53,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,54,54,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,55,55,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,56,56,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,57,57,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,58,58,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,59,59,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,60,60,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,61,61,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,62,62,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,63,63,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,64,64,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,65,65,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,66,66,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,67,67,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,68,68,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,69,69,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,70,70,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,71,71,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,72,72,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,73,73,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,74,74,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,75,75,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,76,76,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,77,77,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,78,78,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,79,79,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,80,80,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,81,81,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,82,82,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,83,83,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,84,84,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,85,85,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,86,86,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,87,87,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,88,88,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,89,89,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,90,90,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,91,91,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,92,92,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,93,93,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,94,94,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,95,95,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,96,96,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,97,97,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,98,98,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,99,99,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,100,100,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,101,101,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,102,102,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,103,103,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,104,104,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,105,105,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,106,106,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,107,107,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,108,108,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,109,109,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,110,110,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,111,111,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,112,112,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,113,113,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,114,114,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,115,115,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,116,116,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,117,117,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,118,118,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,119,119,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,120,120,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,121,121,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,122,122,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,123,123,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,124,124,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,125,125,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,126,126,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,127,127,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,128,128,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,129,129,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,130,130,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,131,131,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,132,132,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,133,133,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,134,134,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,135,135,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,136,136,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,137,137,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,138,138,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,139,139,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,140,140,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,141,141,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,142,142,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,143,143,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,144,144,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,145,145,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,146,146,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,147,147,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,148,148,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,149,149,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,150,150,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,151,151,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,152,152,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,153,153,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,154,154,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,155,155,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,156,156,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,157,157,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,158,158,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,159,159,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,160,160,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,161,161,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,162,162,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,163,163,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,164,164,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,165,165,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,166,166,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,167,167,0,None,Free +1,unique_prod,nb_failing_lower_than_stopping,168,168,0,None,Free +1,unique_prod,min_up_duration,1,1,0,None,Free +1,unique_prod,min_up_duration,2,2,0,None,Free +1,unique_prod,min_up_duration,3,3,0,None,Free +1,unique_prod,min_up_duration,4,4,0,None,Free +1,unique_prod,min_up_duration,5,5,0,None,Free +1,unique_prod,min_up_duration,6,6,0,None,Free +1,unique_prod,min_up_duration,7,7,0,None,Free +1,unique_prod,min_up_duration,8,8,0,None,Free +1,unique_prod,min_up_duration,9,9,0,None,Free +1,unique_prod,min_up_duration,10,10,0,None,Free +1,unique_prod,min_up_duration,11,11,0,None,Free +1,unique_prod,min_up_duration,12,12,0,None,Free +1,unique_prod,min_up_duration,13,13,0,None,Free +1,unique_prod,min_up_duration,14,14,0,None,Free +1,unique_prod,min_up_duration,15,15,0,None,Free +1,unique_prod,min_up_duration,16,16,0,None,Free +1,unique_prod,min_up_duration,17,17,0,None,Free +1,unique_prod,min_up_duration,18,18,0,None,Free +1,unique_prod,min_up_duration,19,19,0,None,Free +1,unique_prod,min_up_duration,20,20,0,None,Free +1,unique_prod,min_up_duration,21,21,0,None,Free +1,unique_prod,min_up_duration,22,22,0,None,Free +1,unique_prod,min_up_duration,23,23,0,None,Free +1,unique_prod,min_up_duration,24,24,0,None,Free +1,unique_prod,min_up_duration,25,25,0,None,Free +1,unique_prod,min_up_duration,26,26,0,None,Free +1,unique_prod,min_up_duration,27,27,0,None,Free +1,unique_prod,min_up_duration,28,28,0,None,Free +1,unique_prod,min_up_duration,29,29,0,None,Free +1,unique_prod,min_up_duration,30,30,0,None,Free +1,unique_prod,min_up_duration,31,31,0,None,Free +1,unique_prod,min_up_duration,32,32,0,None,Free +1,unique_prod,min_up_duration,33,33,0,None,Free +1,unique_prod,min_up_duration,34,34,0,None,Free +1,unique_prod,min_up_duration,35,35,0,None,Free +1,unique_prod,min_up_duration,36,36,0,None,Free +1,unique_prod,min_up_duration,37,37,0,None,Free +1,unique_prod,min_up_duration,38,38,0,None,Free +1,unique_prod,min_up_duration,39,39,0,None,Free +1,unique_prod,min_up_duration,40,40,0,None,Free +1,unique_prod,min_up_duration,41,41,0,None,Free +1,unique_prod,min_up_duration,42,42,0,None,Free +1,unique_prod,min_up_duration,43,43,0,None,Free +1,unique_prod,min_up_duration,44,44,0,None,Free +1,unique_prod,min_up_duration,45,45,0,None,Free +1,unique_prod,min_up_duration,46,46,0,None,Free +1,unique_prod,min_up_duration,47,47,0,None,Free +1,unique_prod,min_up_duration,48,48,0,None,Free +1,unique_prod,min_up_duration,49,49,0,None,Free +1,unique_prod,min_up_duration,50,50,0,None,Free +1,unique_prod,min_up_duration,51,51,0,None,Free +1,unique_prod,min_up_duration,52,52,0,None,Free +1,unique_prod,min_up_duration,53,53,0,None,Free +1,unique_prod,min_up_duration,54,54,0,None,Free +1,unique_prod,min_up_duration,55,55,0,None,Free +1,unique_prod,min_up_duration,56,56,0,None,Free +1,unique_prod,min_up_duration,57,57,0,None,Free +1,unique_prod,min_up_duration,58,58,0,None,Free +1,unique_prod,min_up_duration,59,59,0,None,Free +1,unique_prod,min_up_duration,60,60,0,None,Free +1,unique_prod,min_up_duration,61,61,0,None,Free +1,unique_prod,min_up_duration,62,62,0,None,Free +1,unique_prod,min_up_duration,63,63,0,None,Free +1,unique_prod,min_up_duration,64,64,0,None,Free +1,unique_prod,min_up_duration,65,65,0,None,Free +1,unique_prod,min_up_duration,66,66,0,None,Free +1,unique_prod,min_up_duration,67,67,0,None,Free +1,unique_prod,min_up_duration,68,68,0,None,Free +1,unique_prod,min_up_duration,69,69,0,None,Free +1,unique_prod,min_up_duration,70,70,0,None,Free +1,unique_prod,min_up_duration,71,71,0,None,Free +1,unique_prod,min_up_duration,72,72,0,None,Free +1,unique_prod,min_up_duration,73,73,0,None,Free +1,unique_prod,min_up_duration,74,74,0,None,Free +1,unique_prod,min_up_duration,75,75,0,None,Free +1,unique_prod,min_up_duration,76,76,0,None,Free +1,unique_prod,min_up_duration,77,77,0,None,Free +1,unique_prod,min_up_duration,78,78,0,None,Free +1,unique_prod,min_up_duration,79,79,0,None,Free +1,unique_prod,min_up_duration,80,80,0,None,Free +1,unique_prod,min_up_duration,81,81,0,None,Free +1,unique_prod,min_up_duration,82,82,0,None,Free +1,unique_prod,min_up_duration,83,83,0,None,Free +1,unique_prod,min_up_duration,84,84,0,None,Free +1,unique_prod,min_up_duration,85,85,0,None,Free +1,unique_prod,min_up_duration,86,86,0,None,Free +1,unique_prod,min_up_duration,87,87,0,None,Free +1,unique_prod,min_up_duration,88,88,0,None,Free +1,unique_prod,min_up_duration,89,89,0,None,Free +1,unique_prod,min_up_duration,90,90,0,None,Free +1,unique_prod,min_up_duration,91,91,0,None,Free +1,unique_prod,min_up_duration,92,92,0,None,Free +1,unique_prod,min_up_duration,93,93,0,None,Free +1,unique_prod,min_up_duration,94,94,0,None,Free +1,unique_prod,min_up_duration,95,95,0,None,Free +1,unique_prod,min_up_duration,96,96,0,None,Free +1,unique_prod,min_up_duration,97,97,0,None,Free +1,unique_prod,min_up_duration,98,98,0,None,Free +1,unique_prod,min_up_duration,99,99,0,None,Free +1,unique_prod,min_up_duration,100,100,0,None,Free +1,unique_prod,min_up_duration,101,101,0,None,Free +1,unique_prod,min_up_duration,102,102,0,None,Free +1,unique_prod,min_up_duration,103,103,0,None,Free +1,unique_prod,min_up_duration,104,104,0,None,Free +1,unique_prod,min_up_duration,105,105,0,None,Free +1,unique_prod,min_up_duration,106,106,0,None,Free +1,unique_prod,min_up_duration,107,107,0,None,Free +1,unique_prod,min_up_duration,108,108,0,None,Free +1,unique_prod,min_up_duration,109,109,0,None,Free +1,unique_prod,min_up_duration,110,110,0,None,Free +1,unique_prod,min_up_duration,111,111,0,None,Free +1,unique_prod,min_up_duration,112,112,0,None,Free +1,unique_prod,min_up_duration,113,113,0,None,Free +1,unique_prod,min_up_duration,114,114,0,None,Free +1,unique_prod,min_up_duration,115,115,0,None,Free +1,unique_prod,min_up_duration,116,116,0,None,Free +1,unique_prod,min_up_duration,117,117,0,None,Free +1,unique_prod,min_up_duration,118,118,0,None,Free +1,unique_prod,min_up_duration,119,119,0,None,Free +1,unique_prod,min_up_duration,120,120,0,None,Free +1,unique_prod,min_up_duration,121,121,0,None,Free +1,unique_prod,min_up_duration,122,122,0,None,Free +1,unique_prod,min_up_duration,123,123,0,None,Free +1,unique_prod,min_up_duration,124,124,0,None,Free +1,unique_prod,min_up_duration,125,125,0,None,Free +1,unique_prod,min_up_duration,126,126,0,None,Free +1,unique_prod,min_up_duration,127,127,0,None,Free +1,unique_prod,min_up_duration,128,128,0,None,Free +1,unique_prod,min_up_duration,129,129,0,None,Free +1,unique_prod,min_up_duration,130,130,0,None,Free +1,unique_prod,min_up_duration,131,131,0,None,Free +1,unique_prod,min_up_duration,132,132,0,None,Free +1,unique_prod,min_up_duration,133,133,0,None,Free +1,unique_prod,min_up_duration,134,134,0,None,Free +1,unique_prod,min_up_duration,135,135,0,None,Free +1,unique_prod,min_up_duration,136,136,0,None,Free +1,unique_prod,min_up_duration,137,137,0,None,Free +1,unique_prod,min_up_duration,138,138,0,None,Free +1,unique_prod,min_up_duration,139,139,0,None,Free +1,unique_prod,min_up_duration,140,140,0,None,Free +1,unique_prod,min_up_duration,141,141,0,None,Free +1,unique_prod,min_up_duration,142,142,0,None,Free +1,unique_prod,min_up_duration,143,143,0,None,Free +1,unique_prod,min_up_duration,144,144,0,None,Free +1,unique_prod,min_up_duration,145,145,0,None,Free +1,unique_prod,min_up_duration,146,146,0,None,Free +1,unique_prod,min_up_duration,147,147,0,None,Free +1,unique_prod,min_up_duration,148,148,0,None,Free +1,unique_prod,min_up_duration,149,149,0,None,Free +1,unique_prod,min_up_duration,150,150,0,None,Free +1,unique_prod,min_up_duration,151,151,0,None,Free +1,unique_prod,min_up_duration,152,152,0,None,Free +1,unique_prod,min_up_duration,153,153,0,None,Free +1,unique_prod,min_up_duration,154,154,0,None,Free +1,unique_prod,min_up_duration,155,155,0,None,Free +1,unique_prod,min_up_duration,156,156,0,None,Free +1,unique_prod,min_up_duration,157,157,0,None,Free +1,unique_prod,min_up_duration,158,158,0,None,Free +1,unique_prod,min_up_duration,159,159,0,None,Free +1,unique_prod,min_up_duration,160,160,0,None,Free +1,unique_prod,min_up_duration,161,161,0,None,Free +1,unique_prod,min_up_duration,162,162,0,None,Free +1,unique_prod,min_up_duration,163,163,0,None,Free +1,unique_prod,min_up_duration,164,164,0,None,Free +1,unique_prod,min_up_duration,165,165,0,None,Free +1,unique_prod,min_up_duration,166,166,0,None,Free +1,unique_prod,min_up_duration,167,167,0,None,Free +1,unique_prod,min_up_duration,168,168,0,None,Free +1,unique_prod,min_down_duration,1,1,0,None,Free +1,unique_prod,min_down_duration,2,2,0,None,Free +1,unique_prod,min_down_duration,3,3,0,None,Free +1,unique_prod,min_down_duration,4,4,0,None,Free +1,unique_prod,min_down_duration,5,5,0,None,Free +1,unique_prod,min_down_duration,6,6,0,None,Free +1,unique_prod,min_down_duration,7,7,0,None,Free +1,unique_prod,min_down_duration,8,8,0,None,Free +1,unique_prod,min_down_duration,9,9,0,None,Free +1,unique_prod,min_down_duration,10,10,0,None,Free +1,unique_prod,min_down_duration,11,11,0,None,Free +1,unique_prod,min_down_duration,12,12,0,None,Free +1,unique_prod,min_down_duration,13,13,0,None,Free +1,unique_prod,min_down_duration,14,14,0,None,Free +1,unique_prod,min_down_duration,15,15,0,None,Free +1,unique_prod,min_down_duration,16,16,0,None,Free +1,unique_prod,min_down_duration,17,17,0,None,Free +1,unique_prod,min_down_duration,18,18,0,None,Free +1,unique_prod,min_down_duration,19,19,0,None,Free +1,unique_prod,min_down_duration,20,20,0,None,Free +1,unique_prod,min_down_duration,21,21,0,None,Free +1,unique_prod,min_down_duration,22,22,0,None,Free +1,unique_prod,min_down_duration,23,23,0,None,Free +1,unique_prod,min_down_duration,24,24,0,None,Free +1,unique_prod,min_down_duration,25,25,0,None,Free +1,unique_prod,min_down_duration,26,26,0,None,Free +1,unique_prod,min_down_duration,27,27,0,None,Free +1,unique_prod,min_down_duration,28,28,0,None,Free +1,unique_prod,min_down_duration,29,29,0,None,Free +1,unique_prod,min_down_duration,30,30,0,None,Free +1,unique_prod,min_down_duration,31,31,0,None,Free +1,unique_prod,min_down_duration,32,32,0,None,Free +1,unique_prod,min_down_duration,33,33,0,None,Free +1,unique_prod,min_down_duration,34,34,0,None,Free +1,unique_prod,min_down_duration,35,35,0,None,Free +1,unique_prod,min_down_duration,36,36,0,None,Free +1,unique_prod,min_down_duration,37,37,0,None,Free +1,unique_prod,min_down_duration,38,38,0,None,Free +1,unique_prod,min_down_duration,39,39,0,None,Free +1,unique_prod,min_down_duration,40,40,0,None,Free +1,unique_prod,min_down_duration,41,41,0,None,Free +1,unique_prod,min_down_duration,42,42,0,None,Free +1,unique_prod,min_down_duration,43,43,0,None,Free +1,unique_prod,min_down_duration,44,44,0,None,Free +1,unique_prod,min_down_duration,45,45,0,None,Free +1,unique_prod,min_down_duration,46,46,0,None,Free +1,unique_prod,min_down_duration,47,47,0,None,Free +1,unique_prod,min_down_duration,48,48,0,None,Free +1,unique_prod,min_down_duration,49,49,0,None,Free +1,unique_prod,min_down_duration,50,50,0,None,Free +1,unique_prod,min_down_duration,51,51,0,None,Free +1,unique_prod,min_down_duration,52,52,0,None,Free +1,unique_prod,min_down_duration,53,53,0,None,Free +1,unique_prod,min_down_duration,54,54,0,None,Free +1,unique_prod,min_down_duration,55,55,0,None,Free +1,unique_prod,min_down_duration,56,56,0,None,Free +1,unique_prod,min_down_duration,57,57,0,None,Free +1,unique_prod,min_down_duration,58,58,0,None,Free +1,unique_prod,min_down_duration,59,59,0,None,Free +1,unique_prod,min_down_duration,60,60,0,None,Free +1,unique_prod,min_down_duration,61,61,0,None,Free +1,unique_prod,min_down_duration,62,62,0,None,Free +1,unique_prod,min_down_duration,63,63,0,None,Free +1,unique_prod,min_down_duration,64,64,0,None,Free +1,unique_prod,min_down_duration,65,65,0,None,Free +1,unique_prod,min_down_duration,66,66,0,None,Free +1,unique_prod,min_down_duration,67,67,0,None,Free +1,unique_prod,min_down_duration,68,68,0,None,Free +1,unique_prod,min_down_duration,69,69,0,None,Free +1,unique_prod,min_down_duration,70,70,0,None,Free +1,unique_prod,min_down_duration,71,71,0,None,Free +1,unique_prod,min_down_duration,72,72,0,None,Free +1,unique_prod,min_down_duration,73,73,0,None,Free +1,unique_prod,min_down_duration,74,74,0,None,Free +1,unique_prod,min_down_duration,75,75,0,None,Free +1,unique_prod,min_down_duration,76,76,0,None,Free +1,unique_prod,min_down_duration,77,77,0,None,Free +1,unique_prod,min_down_duration,78,78,0,None,Free +1,unique_prod,min_down_duration,79,79,0,None,Free +1,unique_prod,min_down_duration,80,80,0,None,Free +1,unique_prod,min_down_duration,81,81,0,None,Free +1,unique_prod,min_down_duration,82,82,0,None,Free +1,unique_prod,min_down_duration,83,83,0,None,Free +1,unique_prod,min_down_duration,84,84,0,None,Free +1,unique_prod,min_down_duration,85,85,0,None,Free +1,unique_prod,min_down_duration,86,86,0,None,Free +1,unique_prod,min_down_duration,87,87,0,None,Free +1,unique_prod,min_down_duration,88,88,0,None,Free +1,unique_prod,min_down_duration,89,89,0,None,Free +1,unique_prod,min_down_duration,90,90,0,None,Free +1,unique_prod,min_down_duration,91,91,0,None,Free +1,unique_prod,min_down_duration,92,92,0,None,Free +1,unique_prod,min_down_duration,93,93,0,None,Free +1,unique_prod,min_down_duration,94,94,0,None,Free +1,unique_prod,min_down_duration,95,95,0,None,Free +1,unique_prod,min_down_duration,96,96,0,None,Free +1,unique_prod,min_down_duration,97,97,0,None,Free +1,unique_prod,min_down_duration,98,98,0,None,Free +1,unique_prod,min_down_duration,99,99,0,None,Free +1,unique_prod,min_down_duration,100,100,0,None,Free +1,unique_prod,min_down_duration,101,101,0,None,Free +1,unique_prod,min_down_duration,102,102,0,None,Free +1,unique_prod,min_down_duration,103,103,0,None,Free +1,unique_prod,min_down_duration,104,104,0,None,Free +1,unique_prod,min_down_duration,105,105,0,None,Free +1,unique_prod,min_down_duration,106,106,0,None,Free +1,unique_prod,min_down_duration,107,107,0,None,Free +1,unique_prod,min_down_duration,108,108,0,None,Free +1,unique_prod,min_down_duration,109,109,0,None,Free +1,unique_prod,min_down_duration,110,110,0,None,Free +1,unique_prod,min_down_duration,111,111,0,None,Free +1,unique_prod,min_down_duration,112,112,0,None,Free +1,unique_prod,min_down_duration,113,113,0,None,Free +1,unique_prod,min_down_duration,114,114,0,None,Free +1,unique_prod,min_down_duration,115,115,0,None,Free +1,unique_prod,min_down_duration,116,116,0,None,Free +1,unique_prod,min_down_duration,117,117,0,None,Free +1,unique_prod,min_down_duration,118,118,0,None,Free +1,unique_prod,min_down_duration,119,119,0,None,Free +1,unique_prod,min_down_duration,120,120,0,None,Free +1,unique_prod,min_down_duration,121,121,0,None,Free +1,unique_prod,min_down_duration,122,122,0,None,Free +1,unique_prod,min_down_duration,123,123,0,None,Free +1,unique_prod,min_down_duration,124,124,0,None,Free +1,unique_prod,min_down_duration,125,125,0,None,Free +1,unique_prod,min_down_duration,126,126,0,None,Free +1,unique_prod,min_down_duration,127,127,0,None,Free +1,unique_prod,min_down_duration,128,128,0,None,Free +1,unique_prod,min_down_duration,129,129,0,None,Free +1,unique_prod,min_down_duration,130,130,0,None,Free +1,unique_prod,min_down_duration,131,131,0,None,Free +1,unique_prod,min_down_duration,132,132,0,None,Free +1,unique_prod,min_down_duration,133,133,0,None,Free +1,unique_prod,min_down_duration,134,134,0,None,Free +1,unique_prod,min_down_duration,135,135,0,None,Free +1,unique_prod,min_down_duration,136,136,0,None,Free +1,unique_prod,min_down_duration,137,137,0,None,Free +1,unique_prod,min_down_duration,138,138,0,None,Free +1,unique_prod,min_down_duration,139,139,0,None,Free +1,unique_prod,min_down_duration,140,140,0,None,Free +1,unique_prod,min_down_duration,141,141,0,None,Free +1,unique_prod,min_down_duration,142,142,0,None,Free +1,unique_prod,min_down_duration,143,143,0,None,Free +1,unique_prod,min_down_duration,144,144,0,None,Free +1,unique_prod,min_down_duration,145,145,0,None,Free +1,unique_prod,min_down_duration,146,146,0,None,Free +1,unique_prod,min_down_duration,147,147,0,None,Free +1,unique_prod,min_down_duration,148,148,0,None,Free +1,unique_prod,min_down_duration,149,149,0,None,Free +1,unique_prod,min_down_duration,150,150,0,None,Free +1,unique_prod,min_down_duration,151,151,0,None,Free +1,unique_prod,min_down_duration,152,152,0,None,Free +1,unique_prod,min_down_duration,153,153,0,None,Free +1,unique_prod,min_down_duration,154,154,0,None,Free +1,unique_prod,min_down_duration,155,155,0,None,Free +1,unique_prod,min_down_duration,156,156,0,None,Free +1,unique_prod,min_down_duration,157,157,0,None,Free +1,unique_prod,min_down_duration,158,158,0,None,Free +1,unique_prod,min_down_duration,159,159,0,None,Free +1,unique_prod,min_down_duration,160,160,0,None,Free +1,unique_prod,min_down_duration,161,161,0,None,Free +1,unique_prod,min_down_duration,162,162,0,None,Free +1,unique_prod,min_down_duration,163,163,0,None,Free +1,unique_prod,min_down_duration,164,164,0,None,Free +1,unique_prod,min_down_duration,165,165,0,None,Free +1,unique_prod,min_down_duration,166,166,0,None,Free +1,unique_prod,min_down_duration,167,167,0,None,Free +1,unique_prod,min_down_duration,168,168,0,None,Free +1,unique_prod,balance_port.flow,1,1,0,150,None +1,unique_prod,balance_port.flow,2,2,0,150,None +1,unique_prod,balance_port.flow,3,3,0,150,None +1,unique_prod,balance_port.flow,4,4,0,150,None +1,unique_prod,balance_port.flow,5,5,0,150,None +1,unique_prod,balance_port.flow,6,6,0,150,None +1,unique_prod,balance_port.flow,7,7,0,150,None +1,unique_prod,balance_port.flow,8,8,0,150,None +1,unique_prod,balance_port.flow,9,9,0,150,None +1,unique_prod,balance_port.flow,10,10,0,150,None +1,unique_prod,balance_port.flow,11,11,0,150,None +1,unique_prod,balance_port.flow,12,12,0,150,None +1,unique_prod,balance_port.flow,13,13,0,150,None +1,unique_prod,balance_port.flow,14,14,0,150,None +1,unique_prod,balance_port.flow,15,15,0,150,None +1,unique_prod,balance_port.flow,16,16,0,150,None +1,unique_prod,balance_port.flow,17,17,0,150,None +1,unique_prod,balance_port.flow,18,18,0,150,None +1,unique_prod,balance_port.flow,19,19,0,150,None +1,unique_prod,balance_port.flow,20,20,0,150,None +1,unique_prod,balance_port.flow,21,21,0,150,None +1,unique_prod,balance_port.flow,22,22,0,150,None +1,unique_prod,balance_port.flow,23,23,0,150,None +1,unique_prod,balance_port.flow,24,24,0,150,None +1,unique_prod,balance_port.flow,25,25,0,150,None +1,unique_prod,balance_port.flow,26,26,0,150,None +1,unique_prod,balance_port.flow,27,27,0,150,None +1,unique_prod,balance_port.flow,28,28,0,150,None +1,unique_prod,balance_port.flow,29,29,0,150,None +1,unique_prod,balance_port.flow,30,30,0,150,None +1,unique_prod,balance_port.flow,31,31,0,150,None +1,unique_prod,balance_port.flow,32,32,0,150,None +1,unique_prod,balance_port.flow,33,33,0,150,None +1,unique_prod,balance_port.flow,34,34,0,150,None +1,unique_prod,balance_port.flow,35,35,0,150,None +1,unique_prod,balance_port.flow,36,36,0,150,None +1,unique_prod,balance_port.flow,37,37,0,0,None +1,unique_prod,balance_port.flow,38,38,0,90,None +1,unique_prod,balance_port.flow,39,39,0,0,None +1,unique_prod,balance_port.flow,40,40,0,90,None +1,unique_prod,balance_port.flow,41,41,0,0,None +1,unique_prod,balance_port.flow,42,42,0,90,None +1,unique_prod,balance_port.flow,43,43,0,0,None +1,unique_prod,balance_port.flow,44,44,0,90,None +1,unique_prod,balance_port.flow,45,45,0,0,None +1,unique_prod,balance_port.flow,46,46,0,150,None +1,unique_prod,balance_port.flow,47,47,0,150,None +1,unique_prod,balance_port.flow,48,48,0,150,None +1,unique_prod,balance_port.flow,49,49,0,150,None +1,unique_prod,balance_port.flow,50,50,0,150,None +1,unique_prod,balance_port.flow,51,51,0,150,None +1,unique_prod,balance_port.flow,52,52,0,150,None +1,unique_prod,balance_port.flow,53,53,0,150,None +1,unique_prod,balance_port.flow,54,54,0,150,None +1,unique_prod,balance_port.flow,55,55,0,150,None +1,unique_prod,balance_port.flow,56,56,0,150,None +1,unique_prod,balance_port.flow,57,57,0,150,None +1,unique_prod,balance_port.flow,58,58,0,150,None +1,unique_prod,balance_port.flow,59,59,0,150,None +1,unique_prod,balance_port.flow,60,60,0,150,None +1,unique_prod,balance_port.flow,61,61,0,150,None +1,unique_prod,balance_port.flow,62,62,0,150,None +1,unique_prod,balance_port.flow,63,63,0,150,None +1,unique_prod,balance_port.flow,64,64,0,90,None +1,unique_prod,balance_port.flow,65,65,0,0,None +1,unique_prod,balance_port.flow,66,66,0,150,None +1,unique_prod,balance_port.flow,67,67,0,0,None +1,unique_prod,balance_port.flow,68,68,0,150,None +1,unique_prod,balance_port.flow,69,69,0,150,None +1,unique_prod,balance_port.flow,70,70,0,150,None +1,unique_prod,balance_port.flow,71,71,0,150,None +1,unique_prod,balance_port.flow,72,72,0,150,None +1,unique_prod,balance_port.flow,73,73,0,150,None +1,unique_prod,balance_port.flow,74,74,0,150,None +1,unique_prod,balance_port.flow,75,75,0,150,None +1,unique_prod,balance_port.flow,76,76,0,150,None +1,unique_prod,balance_port.flow,77,77,0,150,None +1,unique_prod,balance_port.flow,78,78,0,150,None +1,unique_prod,balance_port.flow,79,79,0,150,None +1,unique_prod,balance_port.flow,80,80,0,150,None +1,unique_prod,balance_port.flow,81,81,0,150,None +1,unique_prod,balance_port.flow,82,82,0,150,None +1,unique_prod,balance_port.flow,83,83,0,150,None +1,unique_prod,balance_port.flow,84,84,0,150,None +1,unique_prod,balance_port.flow,85,85,0,150,None +1,unique_prod,balance_port.flow,86,86,0,90,None +1,unique_prod,balance_port.flow,87,87,0,0,None +1,unique_prod,balance_port.flow,88,88,0,90,None +1,unique_prod,balance_port.flow,89,89,0,150,None +1,unique_prod,balance_port.flow,90,90,0,150,None +1,unique_prod,balance_port.flow,91,91,0,150,None +1,unique_prod,balance_port.flow,92,92,0,150,None +1,unique_prod,balance_port.flow,93,93,0,150,None +1,unique_prod,balance_port.flow,94,94,0,150,None +1,unique_prod,balance_port.flow,95,95,0,150,None +1,unique_prod,balance_port.flow,96,96,0,150,None +1,unique_prod,balance_port.flow,97,97,0,150,None +1,unique_prod,balance_port.flow,98,98,0,150,None +1,unique_prod,balance_port.flow,99,99,0,150,None +1,unique_prod,balance_port.flow,100,100,0,90,None +1,unique_prod,balance_port.flow,101,101,0,150,None +1,unique_prod,balance_port.flow,102,102,0,150,None +1,unique_prod,balance_port.flow,103,103,0,150,None +1,unique_prod,balance_port.flow,104,104,0,150,None +1,unique_prod,balance_port.flow,105,105,0,150,None +1,unique_prod,balance_port.flow,106,106,0,150,None +1,unique_prod,balance_port.flow,107,107,0,150,None +1,unique_prod,balance_port.flow,108,108,0,150,None +1,unique_prod,balance_port.flow,109,109,0,150,None +1,unique_prod,balance_port.flow,110,110,0,150,None +1,unique_prod,balance_port.flow,111,111,0,150,None +1,unique_prod,balance_port.flow,112,112,0,150,None +1,unique_prod,balance_port.flow,113,113,0,150,None +1,unique_prod,balance_port.flow,114,114,0,150,None +1,unique_prod,balance_port.flow,115,115,0,150,None +1,unique_prod,balance_port.flow,116,116,0,90,None +1,unique_prod,balance_port.flow,117,117,0,0,None +1,unique_prod,balance_port.flow,118,118,0,90,None +1,unique_prod,balance_port.flow,119,119,0,0,None +1,unique_prod,balance_port.flow,120,120,0,90,None +1,unique_prod,balance_port.flow,121,121,0,0,None +1,unique_prod,balance_port.flow,122,122,0,90,None +1,unique_prod,balance_port.flow,123,123,0,0,None +1,unique_prod,balance_port.flow,124,124,0,90,None +1,unique_prod,balance_port.flow,125,125,0,150,None +1,unique_prod,balance_port.flow,126,126,0,150,None +1,unique_prod,balance_port.flow,127,127,0,150,None +1,unique_prod,balance_port.flow,128,128,0,150,None +1,unique_prod,balance_port.flow,129,129,0,150,None +1,unique_prod,balance_port.flow,130,130,0,150,None +1,unique_prod,balance_port.flow,131,131,0,150,None +1,unique_prod,balance_port.flow,132,132,0,150,None +1,unique_prod,balance_port.flow,133,133,0,150,None +1,unique_prod,balance_port.flow,134,134,0,150,None +1,unique_prod,balance_port.flow,135,135,0,150,None +1,unique_prod,balance_port.flow,136,136,0,150,None +1,unique_prod,balance_port.flow,137,137,0,150,None +1,unique_prod,balance_port.flow,138,138,0,150,None +1,unique_prod,balance_port.flow,139,139,0,150,None +1,unique_prod,balance_port.flow,140,140,0,150,None +1,unique_prod,balance_port.flow,141,141,0,150,None +1,unique_prod,balance_port.flow,142,142,0,150,None +1,unique_prod,balance_port.flow,143,143,0,150,None +1,unique_prod,balance_port.flow,144,144,0,150,None +1,unique_prod,balance_port.flow,145,145,0,150,None +1,unique_prod,balance_port.flow,146,146,0,90,None +1,unique_prod,balance_port.flow,147,147,0,0,None +1,unique_prod,balance_port.flow,148,148,0,90,None +1,unique_prod,balance_port.flow,149,149,0,0,None +1,unique_prod,balance_port.flow,150,150,0,90,None +1,unique_prod,balance_port.flow,151,151,0,0,None +1,unique_prod,balance_port.flow,152,152,0,90,None +1,unique_prod,balance_port.flow,153,153,0,0,None +1,unique_prod,balance_port.flow,154,154,0,90,None +1,unique_prod,balance_port.flow,155,155,0,150,None +1,unique_prod,balance_port.flow,156,156,0,150,None +1,unique_prod,balance_port.flow,157,157,0,150,None +1,unique_prod,balance_port.flow,158,158,0,150,None +1,unique_prod,balance_port.flow,159,159,0,150,None +1,unique_prod,balance_port.flow,160,160,0,150,None +1,unique_prod,balance_port.flow,161,161,0,150,None +1,unique_prod,balance_port.flow,162,162,0,150,None +1,unique_prod,balance_port.flow,163,163,0,150,None +1,unique_prod,balance_port.flow,164,164,0,150,None +1,unique_prod,balance_port.flow,165,165,0,0,None +1,unique_prod,balance_port.flow,166,166,0,150,None +1,unique_prod,balance_port.flow,167,167,0,150,None +1,unique_prod,balance_port.flow,168,168,0,150,None +1,unique_prod2,generation,1,1,0,200,Free +1,unique_prod2,generation,2,2,0,200,Free +1,unique_prod2,generation,3,3,0,200,Free +1,unique_prod2,generation,4,4,0,200,Free +1,unique_prod2,generation,5,5,0,200,Free +1,unique_prod2,generation,6,6,0,200,Free +1,unique_prod2,generation,7,7,0,200,Free +1,unique_prod2,generation,8,8,0,200,Free +1,unique_prod2,generation,9,9,0,200,Free +1,unique_prod2,generation,10,10,0,200,Free +1,unique_prod2,generation,11,11,0,200,Free +1,unique_prod2,generation,12,12,0,200,Free +1,unique_prod2,generation,13,13,0,200,Free +1,unique_prod2,generation,14,14,0,200,Free +1,unique_prod2,generation,15,15,0,200,Free +1,unique_prod2,generation,16,16,0,200,Free +1,unique_prod2,generation,17,17,0,200,Free +1,unique_prod2,generation,18,18,0,200,Free +1,unique_prod2,generation,19,19,0,200,Free +1,unique_prod2,generation,20,20,0,200,Free +1,unique_prod2,generation,21,21,0,200,Free +1,unique_prod2,generation,22,22,0,178.015361478573,Free +1,unique_prod2,generation,23,23,0,200,Free +1,unique_prod2,generation,24,24,0,200,Free +1,unique_prod2,generation,25,25,0,200,Free +1,unique_prod2,generation,26,26,0,200,Free +1,unique_prod2,generation,27,27,0,200,Free +1,unique_prod2,generation,28,28,0,200,Free +1,unique_prod2,generation,29,29,0,200,Free +1,unique_prod2,generation,30,30,0,200,Free +1,unique_prod2,generation,31,31,0,200,Free +1,unique_prod2,generation,32,32,0,200,Free +1,unique_prod2,generation,33,33,0,200,Free +1,unique_prod2,generation,34,34,0,200,Free +1,unique_prod2,generation,35,35,0,200,Free +1,unique_prod2,generation,36,36,0,200,Free +1,unique_prod2,generation,37,37,0,0,Free +1,unique_prod2,generation,38,38,0,60,Free +1,unique_prod2,generation,39,39,0,0,Free +1,unique_prod2,generation,40,40,0,60,Free +1,unique_prod2,generation,41,41,0,0,Free +1,unique_prod2,generation,42,42,0,60,Free +1,unique_prod2,generation,43,43,0,0,Free +1,unique_prod2,generation,44,44,0,60,Free +1,unique_prod2,generation,45,45,0,0,Free +1,unique_prod2,generation,46,46,0,200,Free +1,unique_prod2,generation,47,47,0,200,Free +1,unique_prod2,generation,48,48,0,200,Free +1,unique_prod2,generation,49,49,0,200,Free +1,unique_prod2,generation,50,50,0,200,Free +1,unique_prod2,generation,51,51,0,200,Free +1,unique_prod2,generation,52,52,0,200,Free +1,unique_prod2,generation,53,53,0,200,Free +1,unique_prod2,generation,54,54,0,200,Free +1,unique_prod2,generation,55,55,0,200,Free +1,unique_prod2,generation,56,56,0,200,Free +1,unique_prod2,generation,57,57,0,200,Free +1,unique_prod2,generation,58,58,0,200,Free +1,unique_prod2,generation,59,59,0,200,Free +1,unique_prod2,generation,60,60,0,200,Free +1,unique_prod2,generation,61,61,0,200,Free +1,unique_prod2,generation,62,62,0,200,Free +1,unique_prod2,generation,63,63,0,200,Free +1,unique_prod2,generation,64,64,0,60,Free +1,unique_prod2,generation,65,65,0,0,Free +1,unique_prod2,generation,66,66,0,200,Free +1,unique_prod2,generation,67,67,0,0,Free +1,unique_prod2,generation,68,68,0,200,Free +1,unique_prod2,generation,69,69,0,200,Free +1,unique_prod2,generation,70,70,0,200,Free +1,unique_prod2,generation,71,71,0,200,Free +1,unique_prod2,generation,72,72,0,200,Free +1,unique_prod2,generation,73,73,0,200,Free +1,unique_prod2,generation,74,74,0,200,Free +1,unique_prod2,generation,75,75,0,200,Free +1,unique_prod2,generation,76,76,0,200,Free +1,unique_prod2,generation,77,77,0,200,Free +1,unique_prod2,generation,78,78,0,200,Free +1,unique_prod2,generation,79,79,0,200,Free +1,unique_prod2,generation,80,80,0,200,Free +1,unique_prod2,generation,81,81,0,200,Free +1,unique_prod2,generation,82,82,0,200,Free +1,unique_prod2,generation,83,83,0,200,Free +1,unique_prod2,generation,84,84,0,200,Free +1,unique_prod2,generation,85,85,0,200,Free +1,unique_prod2,generation,86,86,0,60,Free +1,unique_prod2,generation,87,87,0,0,Free +1,unique_prod2,generation,88,88,0,60,Free +1,unique_prod2,generation,89,89,0,200,Free +1,unique_prod2,generation,90,90,0,200,Free +1,unique_prod2,generation,91,91,0,200,Free +1,unique_prod2,generation,92,92,0,200,Free +1,unique_prod2,generation,93,93,0,200,Free +1,unique_prod2,generation,94,94,0,200,Free +1,unique_prod2,generation,95,95,0,200,Free +1,unique_prod2,generation,96,96,0,200,Free +1,unique_prod2,generation,97,97,0,200,Free +1,unique_prod2,generation,98,98,0,200,Free +1,unique_prod2,generation,99,99,0,200,Free +1,unique_prod2,generation,100,100,0,60,Free +1,unique_prod2,generation,101,101,0,200,Free +1,unique_prod2,generation,102,102,0,200,Free +1,unique_prod2,generation,103,103,0,200,Free +1,unique_prod2,generation,104,104,0,200,Free +1,unique_prod2,generation,105,105,0,200,Free +1,unique_prod2,generation,106,106,0,200,Free +1,unique_prod2,generation,107,107,0,200,Free +1,unique_prod2,generation,108,108,0,200,Free +1,unique_prod2,generation,109,109,0,200,Free +1,unique_prod2,generation,110,110,0,200,Free +1,unique_prod2,generation,111,111,0,200,Free +1,unique_prod2,generation,112,112,0,200,Free +1,unique_prod2,generation,113,113,0,200,Free +1,unique_prod2,generation,114,114,0,200,Free +1,unique_prod2,generation,115,115,0,200,Free +1,unique_prod2,generation,116,116,0,60,Free +1,unique_prod2,generation,117,117,0,0,Free +1,unique_prod2,generation,118,118,0,60,Free +1,unique_prod2,generation,119,119,0,0,Free +1,unique_prod2,generation,120,120,0,60,Free +1,unique_prod2,generation,121,121,0,0,Free +1,unique_prod2,generation,122,122,0,60,Free +1,unique_prod2,generation,123,123,0,0,Free +1,unique_prod2,generation,124,124,0,60,Free +1,unique_prod2,generation,125,125,0,200,Free +1,unique_prod2,generation,126,126,0,200,Free +1,unique_prod2,generation,127,127,0,200,Free +1,unique_prod2,generation,128,128,0,200,Free +1,unique_prod2,generation,129,129,0,200,Free +1,unique_prod2,generation,130,130,0,200,Free +1,unique_prod2,generation,131,131,0,200,Free +1,unique_prod2,generation,132,132,0,200,Free +1,unique_prod2,generation,133,133,0,200,Free +1,unique_prod2,generation,134,134,0,200,Free +1,unique_prod2,generation,135,135,0,200,Free +1,unique_prod2,generation,136,136,0,200,Free +1,unique_prod2,generation,137,137,0,200,Free +1,unique_prod2,generation,138,138,0,200,Free +1,unique_prod2,generation,139,139,0,200,Free +1,unique_prod2,generation,140,140,0,200,Free +1,unique_prod2,generation,141,141,0,200,Free +1,unique_prod2,generation,142,142,0,200,Free +1,unique_prod2,generation,143,143,0,200,Free +1,unique_prod2,generation,144,144,0,200,Free +1,unique_prod2,generation,145,145,0,200,Free +1,unique_prod2,generation,146,146,0,60,Free +1,unique_prod2,generation,147,147,0,0,Free +1,unique_prod2,generation,148,148,0,60,Free +1,unique_prod2,generation,149,149,0,0,Free +1,unique_prod2,generation,150,150,0,60,Free +1,unique_prod2,generation,151,151,0,0,Free +1,unique_prod2,generation,152,152,0,60,Free +1,unique_prod2,generation,153,153,0,0,Free +1,unique_prod2,generation,154,154,0,60,Free +1,unique_prod2,generation,155,155,0,200,Free +1,unique_prod2,generation,156,156,0,200,Free +1,unique_prod2,generation,157,157,0,200,Free +1,unique_prod2,generation,158,158,0,200,Free +1,unique_prod2,generation,159,159,0,200,Free +1,unique_prod2,generation,160,160,0,200,Free +1,unique_prod2,generation,161,161,0,200,Free +1,unique_prod2,generation,162,162,0,200,Free +1,unique_prod2,generation,163,163,0,200,Free +1,unique_prod2,generation,164,164,0,200,Free +1,unique_prod2,generation,165,165,0,0,Free +1,unique_prod2,generation,166,166,0,200,Free +1,unique_prod2,generation,167,167,0,200,Free +1,unique_prod2,generation,168,168,0,200,Free +1,unique_prod2,nb_units_on,1,1,0,1,Free +1,unique_prod2,nb_units_on,2,2,0,1,Free +1,unique_prod2,nb_units_on,3,3,0,1,Free +1,unique_prod2,nb_units_on,4,4,0,1,Free +1,unique_prod2,nb_units_on,5,5,0,1,Free +1,unique_prod2,nb_units_on,6,6,0,1,Free +1,unique_prod2,nb_units_on,7,7,0,1,Free +1,unique_prod2,nb_units_on,8,8,0,1,Free +1,unique_prod2,nb_units_on,9,9,0,1,Free +1,unique_prod2,nb_units_on,10,10,0,1,Free +1,unique_prod2,nb_units_on,11,11,0,1,Free +1,unique_prod2,nb_units_on,12,12,0,1,Free +1,unique_prod2,nb_units_on,13,13,0,1,Free +1,unique_prod2,nb_units_on,14,14,0,1,Free +1,unique_prod2,nb_units_on,15,15,0,1,Free +1,unique_prod2,nb_units_on,16,16,0,1,Free +1,unique_prod2,nb_units_on,17,17,0,1,Free +1,unique_prod2,nb_units_on,18,18,0,1,Free +1,unique_prod2,nb_units_on,19,19,0,1,Free +1,unique_prod2,nb_units_on,20,20,0,1,Free +1,unique_prod2,nb_units_on,21,21,0,1,Free +1,unique_prod2,nb_units_on,22,22,0,1,Free +1,unique_prod2,nb_units_on,23,23,0,1,Free +1,unique_prod2,nb_units_on,24,24,0,1,Free +1,unique_prod2,nb_units_on,25,25,0,1,Free +1,unique_prod2,nb_units_on,26,26,0,1,Free +1,unique_prod2,nb_units_on,27,27,0,1,Free +1,unique_prod2,nb_units_on,28,28,0,1,Free +1,unique_prod2,nb_units_on,29,29,0,1,Free +1,unique_prod2,nb_units_on,30,30,0,1,Free +1,unique_prod2,nb_units_on,31,31,0,1,Free +1,unique_prod2,nb_units_on,32,32,0,1,Free +1,unique_prod2,nb_units_on,33,33,0,1,Free +1,unique_prod2,nb_units_on,34,34,0,1,Free +1,unique_prod2,nb_units_on,35,35,0,1,Free +1,unique_prod2,nb_units_on,36,36,0,1,Free +1,unique_prod2,nb_units_on,37,37,0,0,Free +1,unique_prod2,nb_units_on,38,38,0,1,Free +1,unique_prod2,nb_units_on,39,39,0,0,Free +1,unique_prod2,nb_units_on,40,40,0,1,Free +1,unique_prod2,nb_units_on,41,41,0,0,Free +1,unique_prod2,nb_units_on,42,42,0,1,Free +1,unique_prod2,nb_units_on,43,43,0,0,Free +1,unique_prod2,nb_units_on,44,44,0,1,Free +1,unique_prod2,nb_units_on,45,45,0,0,Free +1,unique_prod2,nb_units_on,46,46,0,1,Free +1,unique_prod2,nb_units_on,47,47,0,1,Free +1,unique_prod2,nb_units_on,48,48,0,1,Free +1,unique_prod2,nb_units_on,49,49,0,1,Free +1,unique_prod2,nb_units_on,50,50,0,1,Free +1,unique_prod2,nb_units_on,51,51,0,1,Free +1,unique_prod2,nb_units_on,52,52,0,1,Free +1,unique_prod2,nb_units_on,53,53,0,1,Free +1,unique_prod2,nb_units_on,54,54,0,1,Free +1,unique_prod2,nb_units_on,55,55,0,1,Free +1,unique_prod2,nb_units_on,56,56,0,1,Free +1,unique_prod2,nb_units_on,57,57,0,1,Free +1,unique_prod2,nb_units_on,58,58,0,1,Free +1,unique_prod2,nb_units_on,59,59,0,1,Free +1,unique_prod2,nb_units_on,60,60,0,1,Free +1,unique_prod2,nb_units_on,61,61,0,1,Free +1,unique_prod2,nb_units_on,62,62,0,1,Free +1,unique_prod2,nb_units_on,63,63,0,1,Free +1,unique_prod2,nb_units_on,64,64,0,1,Free +1,unique_prod2,nb_units_on,65,65,0,0,Free +1,unique_prod2,nb_units_on,66,66,0,1,Free +1,unique_prod2,nb_units_on,67,67,0,0,Free +1,unique_prod2,nb_units_on,68,68,0,1,Free +1,unique_prod2,nb_units_on,69,69,0,1,Free +1,unique_prod2,nb_units_on,70,70,0,1,Free +1,unique_prod2,nb_units_on,71,71,0,1,Free +1,unique_prod2,nb_units_on,72,72,0,1,Free +1,unique_prod2,nb_units_on,73,73,0,1,Free +1,unique_prod2,nb_units_on,74,74,0,1,Free +1,unique_prod2,nb_units_on,75,75,0,1,Free +1,unique_prod2,nb_units_on,76,76,0,1,Free +1,unique_prod2,nb_units_on,77,77,0,1,Free +1,unique_prod2,nb_units_on,78,78,0,1,Free +1,unique_prod2,nb_units_on,79,79,0,1,Free +1,unique_prod2,nb_units_on,80,80,0,1,Free +1,unique_prod2,nb_units_on,81,81,0,1,Free +1,unique_prod2,nb_units_on,82,82,0,1,Free +1,unique_prod2,nb_units_on,83,83,0,1,Free +1,unique_prod2,nb_units_on,84,84,0,1,Free +1,unique_prod2,nb_units_on,85,85,0,1,Free +1,unique_prod2,nb_units_on,86,86,0,1,Free +1,unique_prod2,nb_units_on,87,87,0,0,Free +1,unique_prod2,nb_units_on,88,88,0,1,Free +1,unique_prod2,nb_units_on,89,89,0,1,Free +1,unique_prod2,nb_units_on,90,90,0,1,Free +1,unique_prod2,nb_units_on,91,91,0,1,Free +1,unique_prod2,nb_units_on,92,92,0,1,Free +1,unique_prod2,nb_units_on,93,93,0,1,Free +1,unique_prod2,nb_units_on,94,94,0,1,Free +1,unique_prod2,nb_units_on,95,95,0,1,Free +1,unique_prod2,nb_units_on,96,96,0,1,Free +1,unique_prod2,nb_units_on,97,97,0,1,Free +1,unique_prod2,nb_units_on,98,98,0,1,Free +1,unique_prod2,nb_units_on,99,99,0,1,Free +1,unique_prod2,nb_units_on,100,100,0,1,Free +1,unique_prod2,nb_units_on,101,101,0,1,Free +1,unique_prod2,nb_units_on,102,102,0,1,Free +1,unique_prod2,nb_units_on,103,103,0,1,Free +1,unique_prod2,nb_units_on,104,104,0,1,Free +1,unique_prod2,nb_units_on,105,105,0,1,Free +1,unique_prod2,nb_units_on,106,106,0,1,Free +1,unique_prod2,nb_units_on,107,107,0,1,Free +1,unique_prod2,nb_units_on,108,108,0,1,Free +1,unique_prod2,nb_units_on,109,109,0,1,Free +1,unique_prod2,nb_units_on,110,110,0,1,Free +1,unique_prod2,nb_units_on,111,111,0,1,Free +1,unique_prod2,nb_units_on,112,112,0,1,Free +1,unique_prod2,nb_units_on,113,113,0,1,Free +1,unique_prod2,nb_units_on,114,114,0,1,Free +1,unique_prod2,nb_units_on,115,115,0,1,Free +1,unique_prod2,nb_units_on,116,116,0,1,Free +1,unique_prod2,nb_units_on,117,117,0,0,Free +1,unique_prod2,nb_units_on,118,118,0,1,Free +1,unique_prod2,nb_units_on,119,119,0,0,Free +1,unique_prod2,nb_units_on,120,120,0,1,Free +1,unique_prod2,nb_units_on,121,121,0,0,Free +1,unique_prod2,nb_units_on,122,122,0,1,Free +1,unique_prod2,nb_units_on,123,123,0,0,Free +1,unique_prod2,nb_units_on,124,124,0,1,Free +1,unique_prod2,nb_units_on,125,125,0,1,Free +1,unique_prod2,nb_units_on,126,126,0,1,Free +1,unique_prod2,nb_units_on,127,127,0,1,Free +1,unique_prod2,nb_units_on,128,128,0,1,Free +1,unique_prod2,nb_units_on,129,129,0,1,Free +1,unique_prod2,nb_units_on,130,130,0,1,Free +1,unique_prod2,nb_units_on,131,131,0,1,Free +1,unique_prod2,nb_units_on,132,132,0,1,Free +1,unique_prod2,nb_units_on,133,133,0,1,Free +1,unique_prod2,nb_units_on,134,134,0,1,Free +1,unique_prod2,nb_units_on,135,135,0,1,Free +1,unique_prod2,nb_units_on,136,136,0,1,Free +1,unique_prod2,nb_units_on,137,137,0,1,Free +1,unique_prod2,nb_units_on,138,138,0,1,Free +1,unique_prod2,nb_units_on,139,139,0,1,Free +1,unique_prod2,nb_units_on,140,140,0,1,Free +1,unique_prod2,nb_units_on,141,141,0,1,Free +1,unique_prod2,nb_units_on,142,142,0,1,Free +1,unique_prod2,nb_units_on,143,143,0,1,Free +1,unique_prod2,nb_units_on,144,144,0,1,Free +1,unique_prod2,nb_units_on,145,145,0,1,Free +1,unique_prod2,nb_units_on,146,146,0,1,Free +1,unique_prod2,nb_units_on,147,147,0,0,Free +1,unique_prod2,nb_units_on,148,148,0,1,Free +1,unique_prod2,nb_units_on,149,149,0,0,Free +1,unique_prod2,nb_units_on,150,150,0,1,Free +1,unique_prod2,nb_units_on,151,151,0,0,Free +1,unique_prod2,nb_units_on,152,152,0,1,Free +1,unique_prod2,nb_units_on,153,153,0,0,Free +1,unique_prod2,nb_units_on,154,154,0,1,Free +1,unique_prod2,nb_units_on,155,155,0,1,Free +1,unique_prod2,nb_units_on,156,156,0,1,Free +1,unique_prod2,nb_units_on,157,157,0,1,Free +1,unique_prod2,nb_units_on,158,158,0,1,Free +1,unique_prod2,nb_units_on,159,159,0,1,Free +1,unique_prod2,nb_units_on,160,160,0,1,Free +1,unique_prod2,nb_units_on,161,161,0,1,Free +1,unique_prod2,nb_units_on,162,162,0,1,Free +1,unique_prod2,nb_units_on,163,163,0,1,Free +1,unique_prod2,nb_units_on,164,164,0,1,Free +1,unique_prod2,nb_units_on,165,165,0,0,Free +1,unique_prod2,nb_units_on,166,166,0,1,Free +1,unique_prod2,nb_units_on,167,167,0,1,Free +1,unique_prod2,nb_units_on,168,168,0,1,Free +1,unique_prod2,nb_starting,1,1,0,0,Free +1,unique_prod2,nb_starting,2,2,0,-0,Free +1,unique_prod2,nb_starting,3,3,0,0,Free +1,unique_prod2,nb_starting,4,4,0,-0,Free +1,unique_prod2,nb_starting,5,5,0,0,Free +1,unique_prod2,nb_starting,6,6,0,-0,Free +1,unique_prod2,nb_starting,7,7,0,0,Free +1,unique_prod2,nb_starting,8,8,0,-0,Free +1,unique_prod2,nb_starting,9,9,0,0,Free +1,unique_prod2,nb_starting,10,10,0,-0,Free +1,unique_prod2,nb_starting,11,11,0,0,Free +1,unique_prod2,nb_starting,12,12,0,-0,Free +1,unique_prod2,nb_starting,13,13,0,0,Free +1,unique_prod2,nb_starting,14,14,0,-0,Free +1,unique_prod2,nb_starting,15,15,0,0,Free +1,unique_prod2,nb_starting,16,16,0,-0,Free +1,unique_prod2,nb_starting,17,17,0,0,Free +1,unique_prod2,nb_starting,18,18,0,-0,Free +1,unique_prod2,nb_starting,19,19,0,0,Free +1,unique_prod2,nb_starting,20,20,0,-0,Free +1,unique_prod2,nb_starting,21,21,0,0,Free +1,unique_prod2,nb_starting,22,22,0,-0,Free +1,unique_prod2,nb_starting,23,23,0,0,Free +1,unique_prod2,nb_starting,24,24,0,-0,Free +1,unique_prod2,nb_starting,25,25,0,0,Free +1,unique_prod2,nb_starting,26,26,0,-0,Free +1,unique_prod2,nb_starting,27,27,0,0,Free +1,unique_prod2,nb_starting,28,28,0,-0,Free +1,unique_prod2,nb_starting,29,29,0,0,Free +1,unique_prod2,nb_starting,30,30,0,-0,Free +1,unique_prod2,nb_starting,31,31,0,0,Free +1,unique_prod2,nb_starting,32,32,0,-0,Free +1,unique_prod2,nb_starting,33,33,0,0,Free +1,unique_prod2,nb_starting,34,34,0,-0,Free +1,unique_prod2,nb_starting,35,35,0,0,Free +1,unique_prod2,nb_starting,36,36,0,-0,Free +1,unique_prod2,nb_starting,37,37,0,0,Free +1,unique_prod2,nb_starting,38,38,0,1,Free +1,unique_prod2,nb_starting,39,39,0,0,Free +1,unique_prod2,nb_starting,40,40,0,1,Free +1,unique_prod2,nb_starting,41,41,0,0,Free +1,unique_prod2,nb_starting,42,42,0,1,Free +1,unique_prod2,nb_starting,43,43,0,0,Free +1,unique_prod2,nb_starting,44,44,0,1,Free +1,unique_prod2,nb_starting,45,45,0,0,Free +1,unique_prod2,nb_starting,46,46,0,1,Free +1,unique_prod2,nb_starting,47,47,0,0,Free +1,unique_prod2,nb_starting,48,48,0,-0,Free +1,unique_prod2,nb_starting,49,49,0,0,Free +1,unique_prod2,nb_starting,50,50,0,-0,Free +1,unique_prod2,nb_starting,51,51,0,0,Free +1,unique_prod2,nb_starting,52,52,0,-0,Free +1,unique_prod2,nb_starting,53,53,0,0,Free +1,unique_prod2,nb_starting,54,54,0,-0,Free +1,unique_prod2,nb_starting,55,55,0,0,Free +1,unique_prod2,nb_starting,56,56,0,-0,Free +1,unique_prod2,nb_starting,57,57,0,0,Free +1,unique_prod2,nb_starting,58,58,0,-0,Free +1,unique_prod2,nb_starting,59,59,0,0,Free +1,unique_prod2,nb_starting,60,60,0,-0,Free +1,unique_prod2,nb_starting,61,61,0,0,Free +1,unique_prod2,nb_starting,62,62,0,-0,Free +1,unique_prod2,nb_starting,63,63,0,0,Free +1,unique_prod2,nb_starting,64,64,0,-0,Free +1,unique_prod2,nb_starting,65,65,0,0,Free +1,unique_prod2,nb_starting,66,66,0,1,Free +1,unique_prod2,nb_starting,67,67,0,0,Free +1,unique_prod2,nb_starting,68,68,0,1,Free +1,unique_prod2,nb_starting,69,69,0,0,Free +1,unique_prod2,nb_starting,70,70,0,-0,Free +1,unique_prod2,nb_starting,71,71,0,0,Free +1,unique_prod2,nb_starting,72,72,0,-0,Free +1,unique_prod2,nb_starting,73,73,0,0,Free +1,unique_prod2,nb_starting,74,74,0,-0,Free +1,unique_prod2,nb_starting,75,75,0,0,Free +1,unique_prod2,nb_starting,76,76,0,-0,Free +1,unique_prod2,nb_starting,77,77,0,0,Free +1,unique_prod2,nb_starting,78,78,0,-0,Free +1,unique_prod2,nb_starting,79,79,0,0,Free +1,unique_prod2,nb_starting,80,80,0,-0,Free +1,unique_prod2,nb_starting,81,81,0,0,Free +1,unique_prod2,nb_starting,82,82,0,-0,Free +1,unique_prod2,nb_starting,83,83,0,0,Free +1,unique_prod2,nb_starting,84,84,0,-0,Free +1,unique_prod2,nb_starting,85,85,0,0,Free +1,unique_prod2,nb_starting,86,86,0,-0,Free +1,unique_prod2,nb_starting,87,87,0,0,Free +1,unique_prod2,nb_starting,88,88,0,1,Free +1,unique_prod2,nb_starting,89,89,0,0,Free +1,unique_prod2,nb_starting,90,90,0,-0,Free +1,unique_prod2,nb_starting,91,91,0,0,Free +1,unique_prod2,nb_starting,92,92,0,-0,Free +1,unique_prod2,nb_starting,93,93,0,0,Free +1,unique_prod2,nb_starting,94,94,0,-0,Free +1,unique_prod2,nb_starting,95,95,0,0,Free +1,unique_prod2,nb_starting,96,96,0,-0,Free +1,unique_prod2,nb_starting,97,97,0,0,Free +1,unique_prod2,nb_starting,98,98,0,-0,Free +1,unique_prod2,nb_starting,99,99,0,0,Free +1,unique_prod2,nb_starting,100,100,0,-0,Free +1,unique_prod2,nb_starting,101,101,0,0,Free +1,unique_prod2,nb_starting,102,102,0,-0,Free +1,unique_prod2,nb_starting,103,103,0,0,Free +1,unique_prod2,nb_starting,104,104,0,-0,Free +1,unique_prod2,nb_starting,105,105,0,0,Free +1,unique_prod2,nb_starting,106,106,0,-0,Free +1,unique_prod2,nb_starting,107,107,0,0,Free +1,unique_prod2,nb_starting,108,108,0,-0,Free +1,unique_prod2,nb_starting,109,109,0,0,Free +1,unique_prod2,nb_starting,110,110,0,-0,Free +1,unique_prod2,nb_starting,111,111,0,0,Free +1,unique_prod2,nb_starting,112,112,0,-0,Free +1,unique_prod2,nb_starting,113,113,0,0,Free +1,unique_prod2,nb_starting,114,114,0,-0,Free +1,unique_prod2,nb_starting,115,115,0,0,Free +1,unique_prod2,nb_starting,116,116,0,-0,Free +1,unique_prod2,nb_starting,117,117,0,0,Free +1,unique_prod2,nb_starting,118,118,0,1,Free +1,unique_prod2,nb_starting,119,119,0,0,Free +1,unique_prod2,nb_starting,120,120,0,1,Free +1,unique_prod2,nb_starting,121,121,0,0,Free +1,unique_prod2,nb_starting,122,122,0,1,Free +1,unique_prod2,nb_starting,123,123,0,0,Free +1,unique_prod2,nb_starting,124,124,0,1,Free +1,unique_prod2,nb_starting,125,125,0,0,Free +1,unique_prod2,nb_starting,126,126,0,-0,Free +1,unique_prod2,nb_starting,127,127,0,0,Free +1,unique_prod2,nb_starting,128,128,0,-0,Free +1,unique_prod2,nb_starting,129,129,0,0,Free +1,unique_prod2,nb_starting,130,130,0,-0,Free +1,unique_prod2,nb_starting,131,131,0,0,Free +1,unique_prod2,nb_starting,132,132,0,-0,Free +1,unique_prod2,nb_starting,133,133,0,0,Free +1,unique_prod2,nb_starting,134,134,0,-0,Free +1,unique_prod2,nb_starting,135,135,0,0,Free +1,unique_prod2,nb_starting,136,136,0,-0,Free +1,unique_prod2,nb_starting,137,137,0,0,Free +1,unique_prod2,nb_starting,138,138,0,-0,Free +1,unique_prod2,nb_starting,139,139,0,0,Free +1,unique_prod2,nb_starting,140,140,0,-0,Free +1,unique_prod2,nb_starting,141,141,0,0,Free +1,unique_prod2,nb_starting,142,142,0,-0,Free +1,unique_prod2,nb_starting,143,143,0,0,Free +1,unique_prod2,nb_starting,144,144,0,-0,Free +1,unique_prod2,nb_starting,145,145,0,0,Free +1,unique_prod2,nb_starting,146,146,0,-0,Free +1,unique_prod2,nb_starting,147,147,0,0,Free +1,unique_prod2,nb_starting,148,148,0,1,Free +1,unique_prod2,nb_starting,149,149,0,0,Free +1,unique_prod2,nb_starting,150,150,0,1,Free +1,unique_prod2,nb_starting,151,151,0,0,Free +1,unique_prod2,nb_starting,152,152,0,1,Free +1,unique_prod2,nb_starting,153,153,0,0,Free +1,unique_prod2,nb_starting,154,154,0,1,Free +1,unique_prod2,nb_starting,155,155,0,0,Free +1,unique_prod2,nb_starting,156,156,0,-0,Free +1,unique_prod2,nb_starting,157,157,0,0,Free +1,unique_prod2,nb_starting,158,158,0,-0,Free +1,unique_prod2,nb_starting,159,159,0,0,Free +1,unique_prod2,nb_starting,160,160,0,-0,Free +1,unique_prod2,nb_starting,161,161,0,0,Free +1,unique_prod2,nb_starting,162,162,0,-0,Free +1,unique_prod2,nb_starting,163,163,0,0,Free +1,unique_prod2,nb_starting,164,164,0,-0,Free +1,unique_prod2,nb_starting,165,165,0,0,Free +1,unique_prod2,nb_starting,166,166,0,1,Free +1,unique_prod2,nb_starting,167,167,0,0,Free +1,unique_prod2,nb_starting,168,168,0,-0,Free +1,unique_prod2,nb_stopping,1,1,0,-0,Free +1,unique_prod2,nb_stopping,2,2,0,0,Free +1,unique_prod2,nb_stopping,3,3,0,-0,Free +1,unique_prod2,nb_stopping,4,4,0,0,Free +1,unique_prod2,nb_stopping,5,5,0,-0,Free +1,unique_prod2,nb_stopping,6,6,0,0,Free +1,unique_prod2,nb_stopping,7,7,0,-0,Free +1,unique_prod2,nb_stopping,8,8,0,0,Free +1,unique_prod2,nb_stopping,9,9,0,-0,Free +1,unique_prod2,nb_stopping,10,10,0,0,Free +1,unique_prod2,nb_stopping,11,11,0,-0,Free +1,unique_prod2,nb_stopping,12,12,0,0,Free +1,unique_prod2,nb_stopping,13,13,0,-0,Free +1,unique_prod2,nb_stopping,14,14,0,0,Free +1,unique_prod2,nb_stopping,15,15,0,-0,Free +1,unique_prod2,nb_stopping,16,16,0,0,Free +1,unique_prod2,nb_stopping,17,17,0,-0,Free +1,unique_prod2,nb_stopping,18,18,0,0,Free +1,unique_prod2,nb_stopping,19,19,0,-0,Free +1,unique_prod2,nb_stopping,20,20,0,0,Free +1,unique_prod2,nb_stopping,21,21,0,-0,Free +1,unique_prod2,nb_stopping,22,22,0,0,Free +1,unique_prod2,nb_stopping,23,23,0,-0,Free +1,unique_prod2,nb_stopping,24,24,0,0,Free +1,unique_prod2,nb_stopping,25,25,0,-0,Free +1,unique_prod2,nb_stopping,26,26,0,0,Free +1,unique_prod2,nb_stopping,27,27,0,-0,Free +1,unique_prod2,nb_stopping,28,28,0,0,Free +1,unique_prod2,nb_stopping,29,29,0,-0,Free +1,unique_prod2,nb_stopping,30,30,0,0,Free +1,unique_prod2,nb_stopping,31,31,0,-0,Free +1,unique_prod2,nb_stopping,32,32,0,0,Free +1,unique_prod2,nb_stopping,33,33,0,-0,Free +1,unique_prod2,nb_stopping,34,34,0,0,Free +1,unique_prod2,nb_stopping,35,35,0,-0,Free +1,unique_prod2,nb_stopping,36,36,0,0,Free +1,unique_prod2,nb_stopping,37,37,0,1,Free +1,unique_prod2,nb_stopping,38,38,0,0,Free +1,unique_prod2,nb_stopping,39,39,0,1,Free +1,unique_prod2,nb_stopping,40,40,0,0,Free +1,unique_prod2,nb_stopping,41,41,0,1,Free +1,unique_prod2,nb_stopping,42,42,0,0,Free +1,unique_prod2,nb_stopping,43,43,0,1,Free +1,unique_prod2,nb_stopping,44,44,0,0,Free +1,unique_prod2,nb_stopping,45,45,0,1,Free +1,unique_prod2,nb_stopping,46,46,0,0,Free +1,unique_prod2,nb_stopping,47,47,0,-0,Free +1,unique_prod2,nb_stopping,48,48,0,0,Free +1,unique_prod2,nb_stopping,49,49,0,-0,Free +1,unique_prod2,nb_stopping,50,50,0,0,Free +1,unique_prod2,nb_stopping,51,51,0,-0,Free +1,unique_prod2,nb_stopping,52,52,0,0,Free +1,unique_prod2,nb_stopping,53,53,0,-0,Free +1,unique_prod2,nb_stopping,54,54,0,0,Free +1,unique_prod2,nb_stopping,55,55,0,-0,Free +1,unique_prod2,nb_stopping,56,56,0,0,Free +1,unique_prod2,nb_stopping,57,57,0,-0,Free +1,unique_prod2,nb_stopping,58,58,0,0,Free +1,unique_prod2,nb_stopping,59,59,0,-0,Free +1,unique_prod2,nb_stopping,60,60,0,0,Free +1,unique_prod2,nb_stopping,61,61,0,-0,Free +1,unique_prod2,nb_stopping,62,62,0,0,Free +1,unique_prod2,nb_stopping,63,63,0,-0,Free +1,unique_prod2,nb_stopping,64,64,0,0,Free +1,unique_prod2,nb_stopping,65,65,0,1,Free +1,unique_prod2,nb_stopping,66,66,0,0,Free +1,unique_prod2,nb_stopping,67,67,0,1,Free +1,unique_prod2,nb_stopping,68,68,0,0,Free +1,unique_prod2,nb_stopping,69,69,0,-0,Free +1,unique_prod2,nb_stopping,70,70,0,0,Free +1,unique_prod2,nb_stopping,71,71,0,-0,Free +1,unique_prod2,nb_stopping,72,72,0,0,Free +1,unique_prod2,nb_stopping,73,73,0,-0,Free +1,unique_prod2,nb_stopping,74,74,0,0,Free +1,unique_prod2,nb_stopping,75,75,0,-0,Free +1,unique_prod2,nb_stopping,76,76,0,0,Free +1,unique_prod2,nb_stopping,77,77,0,-0,Free +1,unique_prod2,nb_stopping,78,78,0,0,Free +1,unique_prod2,nb_stopping,79,79,0,-0,Free +1,unique_prod2,nb_stopping,80,80,0,0,Free +1,unique_prod2,nb_stopping,81,81,0,-0,Free +1,unique_prod2,nb_stopping,82,82,0,0,Free +1,unique_prod2,nb_stopping,83,83,0,-0,Free +1,unique_prod2,nb_stopping,84,84,0,0,Free +1,unique_prod2,nb_stopping,85,85,0,-0,Free +1,unique_prod2,nb_stopping,86,86,0,0,Free +1,unique_prod2,nb_stopping,87,87,0,1,Free +1,unique_prod2,nb_stopping,88,88,0,0,Free +1,unique_prod2,nb_stopping,89,89,0,-0,Free +1,unique_prod2,nb_stopping,90,90,0,0,Free +1,unique_prod2,nb_stopping,91,91,0,-0,Free +1,unique_prod2,nb_stopping,92,92,0,0,Free +1,unique_prod2,nb_stopping,93,93,0,-0,Free +1,unique_prod2,nb_stopping,94,94,0,0,Free +1,unique_prod2,nb_stopping,95,95,0,-0,Free +1,unique_prod2,nb_stopping,96,96,0,0,Free +1,unique_prod2,nb_stopping,97,97,0,-0,Free +1,unique_prod2,nb_stopping,98,98,0,0,Free +1,unique_prod2,nb_stopping,99,99,0,-0,Free +1,unique_prod2,nb_stopping,100,100,0,0,Free +1,unique_prod2,nb_stopping,101,101,0,-0,Free +1,unique_prod2,nb_stopping,102,102,0,0,Free +1,unique_prod2,nb_stopping,103,103,0,-0,Free +1,unique_prod2,nb_stopping,104,104,0,0,Free +1,unique_prod2,nb_stopping,105,105,0,-0,Free +1,unique_prod2,nb_stopping,106,106,0,0,Free +1,unique_prod2,nb_stopping,107,107,0,-0,Free +1,unique_prod2,nb_stopping,108,108,0,0,Free +1,unique_prod2,nb_stopping,109,109,0,-0,Free +1,unique_prod2,nb_stopping,110,110,0,0,Free +1,unique_prod2,nb_stopping,111,111,0,-0,Free +1,unique_prod2,nb_stopping,112,112,0,0,Free +1,unique_prod2,nb_stopping,113,113,0,-0,Free +1,unique_prod2,nb_stopping,114,114,0,0,Free +1,unique_prod2,nb_stopping,115,115,0,-0,Free +1,unique_prod2,nb_stopping,116,116,0,0,Free +1,unique_prod2,nb_stopping,117,117,0,1,Free +1,unique_prod2,nb_stopping,118,118,0,0,Free +1,unique_prod2,nb_stopping,119,119,0,1,Free +1,unique_prod2,nb_stopping,120,120,0,0,Free +1,unique_prod2,nb_stopping,121,121,0,1,Free +1,unique_prod2,nb_stopping,122,122,0,0,Free +1,unique_prod2,nb_stopping,123,123,0,1,Free +1,unique_prod2,nb_stopping,124,124,0,0,Free +1,unique_prod2,nb_stopping,125,125,0,-0,Free +1,unique_prod2,nb_stopping,126,126,0,0,Free +1,unique_prod2,nb_stopping,127,127,0,-0,Free +1,unique_prod2,nb_stopping,128,128,0,0,Free +1,unique_prod2,nb_stopping,129,129,0,-0,Free +1,unique_prod2,nb_stopping,130,130,0,0,Free +1,unique_prod2,nb_stopping,131,131,0,-0,Free +1,unique_prod2,nb_stopping,132,132,0,0,Free +1,unique_prod2,nb_stopping,133,133,0,-0,Free +1,unique_prod2,nb_stopping,134,134,0,0,Free +1,unique_prod2,nb_stopping,135,135,0,-0,Free +1,unique_prod2,nb_stopping,136,136,0,0,Free +1,unique_prod2,nb_stopping,137,137,0,-0,Free +1,unique_prod2,nb_stopping,138,138,0,0,Free +1,unique_prod2,nb_stopping,139,139,0,-0,Free +1,unique_prod2,nb_stopping,140,140,0,0,Free +1,unique_prod2,nb_stopping,141,141,0,-0,Free +1,unique_prod2,nb_stopping,142,142,0,0,Free +1,unique_prod2,nb_stopping,143,143,0,-0,Free +1,unique_prod2,nb_stopping,144,144,0,0,Free +1,unique_prod2,nb_stopping,145,145,0,-0,Free +1,unique_prod2,nb_stopping,146,146,0,0,Free +1,unique_prod2,nb_stopping,147,147,0,1,Free +1,unique_prod2,nb_stopping,148,148,0,0,Free +1,unique_prod2,nb_stopping,149,149,0,1,Free +1,unique_prod2,nb_stopping,150,150,0,0,Free +1,unique_prod2,nb_stopping,151,151,0,1,Free +1,unique_prod2,nb_stopping,152,152,0,0,Free +1,unique_prod2,nb_stopping,153,153,0,1,Free +1,unique_prod2,nb_stopping,154,154,0,0,Free +1,unique_prod2,nb_stopping,155,155,0,-0,Free +1,unique_prod2,nb_stopping,156,156,0,0,Free +1,unique_prod2,nb_stopping,157,157,0,-0,Free +1,unique_prod2,nb_stopping,158,158,0,0,Free +1,unique_prod2,nb_stopping,159,159,0,-0,Free +1,unique_prod2,nb_stopping,160,160,0,0,Free +1,unique_prod2,nb_stopping,161,161,0,-0,Free +1,unique_prod2,nb_stopping,162,162,0,0,Free +1,unique_prod2,nb_stopping,163,163,0,-0,Free +1,unique_prod2,nb_stopping,164,164,0,0,Free +1,unique_prod2,nb_stopping,165,165,0,1,Free +1,unique_prod2,nb_stopping,166,166,0,0,Free +1,unique_prod2,nb_stopping,167,167,0,-0,Free +1,unique_prod2,nb_stopping,168,168,0,0,Free +1,unique_prod2,nb_failing,1,1,0,0,Free +1,unique_prod2,nb_failing,2,2,0,0,Free +1,unique_prod2,nb_failing,3,3,0,0,Free +1,unique_prod2,nb_failing,4,4,0,0,Free +1,unique_prod2,nb_failing,5,5,0,0,Free +1,unique_prod2,nb_failing,6,6,0,0,Free +1,unique_prod2,nb_failing,7,7,0,0,Free +1,unique_prod2,nb_failing,8,8,0,0,Free +1,unique_prod2,nb_failing,9,9,0,0,Free +1,unique_prod2,nb_failing,10,10,0,0,Free +1,unique_prod2,nb_failing,11,11,0,0,Free +1,unique_prod2,nb_failing,12,12,0,0,Free +1,unique_prod2,nb_failing,13,13,0,0,Free +1,unique_prod2,nb_failing,14,14,0,0,Free +1,unique_prod2,nb_failing,15,15,0,0,Free +1,unique_prod2,nb_failing,16,16,0,0,Free +1,unique_prod2,nb_failing,17,17,0,0,Free +1,unique_prod2,nb_failing,18,18,0,0,Free +1,unique_prod2,nb_failing,19,19,0,0,Free +1,unique_prod2,nb_failing,20,20,0,0,Free +1,unique_prod2,nb_failing,21,21,0,0,Free +1,unique_prod2,nb_failing,22,22,0,0,Free +1,unique_prod2,nb_failing,23,23,0,0,Free +1,unique_prod2,nb_failing,24,24,0,0,Free +1,unique_prod2,nb_failing,25,25,0,0,Free +1,unique_prod2,nb_failing,26,26,0,0,Free +1,unique_prod2,nb_failing,27,27,0,0,Free +1,unique_prod2,nb_failing,28,28,0,0,Free +1,unique_prod2,nb_failing,29,29,0,0,Free +1,unique_prod2,nb_failing,30,30,0,0,Free +1,unique_prod2,nb_failing,31,31,0,0,Free +1,unique_prod2,nb_failing,32,32,0,0,Free +1,unique_prod2,nb_failing,33,33,0,0,Free +1,unique_prod2,nb_failing,34,34,0,0,Free +1,unique_prod2,nb_failing,35,35,0,0,Free +1,unique_prod2,nb_failing,36,36,0,0,Free +1,unique_prod2,nb_failing,37,37,0,0,Free +1,unique_prod2,nb_failing,38,38,0,0,Free +1,unique_prod2,nb_failing,39,39,0,0,Free +1,unique_prod2,nb_failing,40,40,0,0,Free +1,unique_prod2,nb_failing,41,41,0,0,Free +1,unique_prod2,nb_failing,42,42,0,0,Free +1,unique_prod2,nb_failing,43,43,0,0,Free +1,unique_prod2,nb_failing,44,44,0,0,Free +1,unique_prod2,nb_failing,45,45,0,0,Free +1,unique_prod2,nb_failing,46,46,0,0,Free +1,unique_prod2,nb_failing,47,47,0,0,Free +1,unique_prod2,nb_failing,48,48,0,0,Free +1,unique_prod2,nb_failing,49,49,0,0,Free +1,unique_prod2,nb_failing,50,50,0,0,Free +1,unique_prod2,nb_failing,51,51,0,0,Free +1,unique_prod2,nb_failing,52,52,0,0,Free +1,unique_prod2,nb_failing,53,53,0,0,Free +1,unique_prod2,nb_failing,54,54,0,0,Free +1,unique_prod2,nb_failing,55,55,0,0,Free +1,unique_prod2,nb_failing,56,56,0,0,Free +1,unique_prod2,nb_failing,57,57,0,0,Free +1,unique_prod2,nb_failing,58,58,0,0,Free +1,unique_prod2,nb_failing,59,59,0,0,Free +1,unique_prod2,nb_failing,60,60,0,0,Free +1,unique_prod2,nb_failing,61,61,0,0,Free +1,unique_prod2,nb_failing,62,62,0,0,Free +1,unique_prod2,nb_failing,63,63,0,0,Free +1,unique_prod2,nb_failing,64,64,0,0,Free +1,unique_prod2,nb_failing,65,65,0,0,Free +1,unique_prod2,nb_failing,66,66,0,0,Free +1,unique_prod2,nb_failing,67,67,0,0,Free +1,unique_prod2,nb_failing,68,68,0,0,Free +1,unique_prod2,nb_failing,69,69,0,0,Free +1,unique_prod2,nb_failing,70,70,0,0,Free +1,unique_prod2,nb_failing,71,71,0,0,Free +1,unique_prod2,nb_failing,72,72,0,0,Free +1,unique_prod2,nb_failing,73,73,0,0,Free +1,unique_prod2,nb_failing,74,74,0,0,Free +1,unique_prod2,nb_failing,75,75,0,0,Free +1,unique_prod2,nb_failing,76,76,0,0,Free +1,unique_prod2,nb_failing,77,77,0,0,Free +1,unique_prod2,nb_failing,78,78,0,0,Free +1,unique_prod2,nb_failing,79,79,0,0,Free +1,unique_prod2,nb_failing,80,80,0,0,Free +1,unique_prod2,nb_failing,81,81,0,0,Free +1,unique_prod2,nb_failing,82,82,0,0,Free +1,unique_prod2,nb_failing,83,83,0,0,Free +1,unique_prod2,nb_failing,84,84,0,0,Free +1,unique_prod2,nb_failing,85,85,0,0,Free +1,unique_prod2,nb_failing,86,86,0,0,Free +1,unique_prod2,nb_failing,87,87,0,0,Free +1,unique_prod2,nb_failing,88,88,0,0,Free +1,unique_prod2,nb_failing,89,89,0,0,Free +1,unique_prod2,nb_failing,90,90,0,0,Free +1,unique_prod2,nb_failing,91,91,0,0,Free +1,unique_prod2,nb_failing,92,92,0,0,Free +1,unique_prod2,nb_failing,93,93,0,0,Free +1,unique_prod2,nb_failing,94,94,0,0,Free +1,unique_prod2,nb_failing,95,95,0,0,Free +1,unique_prod2,nb_failing,96,96,0,0,Free +1,unique_prod2,nb_failing,97,97,0,0,Free +1,unique_prod2,nb_failing,98,98,0,0,Free +1,unique_prod2,nb_failing,99,99,0,0,Free +1,unique_prod2,nb_failing,100,100,0,0,Free +1,unique_prod2,nb_failing,101,101,0,0,Free +1,unique_prod2,nb_failing,102,102,0,0,Free +1,unique_prod2,nb_failing,103,103,0,0,Free +1,unique_prod2,nb_failing,104,104,0,0,Free +1,unique_prod2,nb_failing,105,105,0,0,Free +1,unique_prod2,nb_failing,106,106,0,0,Free +1,unique_prod2,nb_failing,107,107,0,0,Free +1,unique_prod2,nb_failing,108,108,0,0,Free +1,unique_prod2,nb_failing,109,109,0,0,Free +1,unique_prod2,nb_failing,110,110,0,0,Free +1,unique_prod2,nb_failing,111,111,0,0,Free +1,unique_prod2,nb_failing,112,112,0,0,Free +1,unique_prod2,nb_failing,113,113,0,0,Free +1,unique_prod2,nb_failing,114,114,0,0,Free +1,unique_prod2,nb_failing,115,115,0,0,Free +1,unique_prod2,nb_failing,116,116,0,0,Free +1,unique_prod2,nb_failing,117,117,0,0,Free +1,unique_prod2,nb_failing,118,118,0,0,Free +1,unique_prod2,nb_failing,119,119,0,0,Free +1,unique_prod2,nb_failing,120,120,0,0,Free +1,unique_prod2,nb_failing,121,121,0,0,Free +1,unique_prod2,nb_failing,122,122,0,0,Free +1,unique_prod2,nb_failing,123,123,0,0,Free +1,unique_prod2,nb_failing,124,124,0,0,Free +1,unique_prod2,nb_failing,125,125,0,0,Free +1,unique_prod2,nb_failing,126,126,0,0,Free +1,unique_prod2,nb_failing,127,127,0,0,Free +1,unique_prod2,nb_failing,128,128,0,0,Free +1,unique_prod2,nb_failing,129,129,0,0,Free +1,unique_prod2,nb_failing,130,130,0,0,Free +1,unique_prod2,nb_failing,131,131,0,0,Free +1,unique_prod2,nb_failing,132,132,0,0,Free +1,unique_prod2,nb_failing,133,133,0,0,Free +1,unique_prod2,nb_failing,134,134,0,0,Free +1,unique_prod2,nb_failing,135,135,0,0,Free +1,unique_prod2,nb_failing,136,136,0,0,Free +1,unique_prod2,nb_failing,137,137,0,0,Free +1,unique_prod2,nb_failing,138,138,0,0,Free +1,unique_prod2,nb_failing,139,139,0,0,Free +1,unique_prod2,nb_failing,140,140,0,0,Free +1,unique_prod2,nb_failing,141,141,0,0,Free +1,unique_prod2,nb_failing,142,142,0,0,Free +1,unique_prod2,nb_failing,143,143,0,0,Free +1,unique_prod2,nb_failing,144,144,0,0,Free +1,unique_prod2,nb_failing,145,145,0,0,Free +1,unique_prod2,nb_failing,146,146,0,0,Free +1,unique_prod2,nb_failing,147,147,0,0,Free +1,unique_prod2,nb_failing,148,148,0,0,Free +1,unique_prod2,nb_failing,149,149,0,0,Free +1,unique_prod2,nb_failing,150,150,0,0,Free +1,unique_prod2,nb_failing,151,151,0,0,Free +1,unique_prod2,nb_failing,152,152,0,0,Free +1,unique_prod2,nb_failing,153,153,0,0,Free +1,unique_prod2,nb_failing,154,154,0,0,Free +1,unique_prod2,nb_failing,155,155,0,0,Free +1,unique_prod2,nb_failing,156,156,0,0,Free +1,unique_prod2,nb_failing,157,157,0,0,Free +1,unique_prod2,nb_failing,158,158,0,0,Free +1,unique_prod2,nb_failing,159,159,0,0,Free +1,unique_prod2,nb_failing,160,160,0,0,Free +1,unique_prod2,nb_failing,161,161,0,0,Free +1,unique_prod2,nb_failing,162,162,0,0,Free +1,unique_prod2,nb_failing,163,163,0,0,Free +1,unique_prod2,nb_failing,164,164,0,0,Free +1,unique_prod2,nb_failing,165,165,0,0,Free +1,unique_prod2,nb_failing,166,166,0,0,Free +1,unique_prod2,nb_failing,167,167,0,0,Free +1,unique_prod2,nb_failing,168,168,0,0,Free +1,unique_prod2,max_generation,1,1,0,None,Free +1,unique_prod2,max_generation,2,2,0,None,Free +1,unique_prod2,max_generation,3,3,0,None,Free +1,unique_prod2,max_generation,4,4,0,None,Free +1,unique_prod2,max_generation,5,5,0,None,Free +1,unique_prod2,max_generation,6,6,0,None,Free +1,unique_prod2,max_generation,7,7,0,None,Free +1,unique_prod2,max_generation,8,8,0,None,Free +1,unique_prod2,max_generation,9,9,0,None,Free +1,unique_prod2,max_generation,10,10,0,None,Free +1,unique_prod2,max_generation,11,11,0,None,Free +1,unique_prod2,max_generation,12,12,0,None,Free +1,unique_prod2,max_generation,13,13,0,None,Free +1,unique_prod2,max_generation,14,14,0,None,Free +1,unique_prod2,max_generation,15,15,0,None,Free +1,unique_prod2,max_generation,16,16,0,None,Free +1,unique_prod2,max_generation,17,17,0,None,Free +1,unique_prod2,max_generation,18,18,0,None,Free +1,unique_prod2,max_generation,19,19,0,None,Free +1,unique_prod2,max_generation,20,20,0,None,Free +1,unique_prod2,max_generation,21,21,0,None,Free +1,unique_prod2,max_generation,22,22,0,None,Free +1,unique_prod2,max_generation,23,23,0,None,Free +1,unique_prod2,max_generation,24,24,0,None,Free +1,unique_prod2,max_generation,25,25,0,None,Free +1,unique_prod2,max_generation,26,26,0,None,Free +1,unique_prod2,max_generation,27,27,0,None,Free +1,unique_prod2,max_generation,28,28,0,None,Free +1,unique_prod2,max_generation,29,29,0,None,Free +1,unique_prod2,max_generation,30,30,0,None,Free +1,unique_prod2,max_generation,31,31,0,None,Free +1,unique_prod2,max_generation,32,32,0,None,Free +1,unique_prod2,max_generation,33,33,0,None,Free +1,unique_prod2,max_generation,34,34,0,None,Free +1,unique_prod2,max_generation,35,35,0,None,Free +1,unique_prod2,max_generation,36,36,0,None,Free +1,unique_prod2,max_generation,37,37,0,None,Free +1,unique_prod2,max_generation,38,38,0,None,Free +1,unique_prod2,max_generation,39,39,0,None,Free +1,unique_prod2,max_generation,40,40,0,None,Free +1,unique_prod2,max_generation,41,41,0,None,Free +1,unique_prod2,max_generation,42,42,0,None,Free +1,unique_prod2,max_generation,43,43,0,None,Free +1,unique_prod2,max_generation,44,44,0,None,Free +1,unique_prod2,max_generation,45,45,0,None,Free +1,unique_prod2,max_generation,46,46,0,None,Free +1,unique_prod2,max_generation,47,47,0,None,Free +1,unique_prod2,max_generation,48,48,0,None,Free +1,unique_prod2,max_generation,49,49,0,None,Free +1,unique_prod2,max_generation,50,50,0,None,Free +1,unique_prod2,max_generation,51,51,0,None,Free +1,unique_prod2,max_generation,52,52,0,None,Free +1,unique_prod2,max_generation,53,53,0,None,Free +1,unique_prod2,max_generation,54,54,0,None,Free +1,unique_prod2,max_generation,55,55,0,None,Free +1,unique_prod2,max_generation,56,56,0,None,Free +1,unique_prod2,max_generation,57,57,0,None,Free +1,unique_prod2,max_generation,58,58,0,None,Free +1,unique_prod2,max_generation,59,59,0,None,Free +1,unique_prod2,max_generation,60,60,0,None,Free +1,unique_prod2,max_generation,61,61,0,None,Free +1,unique_prod2,max_generation,62,62,0,None,Free +1,unique_prod2,max_generation,63,63,0,None,Free +1,unique_prod2,max_generation,64,64,0,None,Free +1,unique_prod2,max_generation,65,65,0,None,Free +1,unique_prod2,max_generation,66,66,0,None,Free +1,unique_prod2,max_generation,67,67,0,None,Free +1,unique_prod2,max_generation,68,68,0,None,Free +1,unique_prod2,max_generation,69,69,0,None,Free +1,unique_prod2,max_generation,70,70,0,None,Free +1,unique_prod2,max_generation,71,71,0,None,Free +1,unique_prod2,max_generation,72,72,0,None,Free +1,unique_prod2,max_generation,73,73,0,None,Free +1,unique_prod2,max_generation,74,74,0,None,Free +1,unique_prod2,max_generation,75,75,0,None,Free +1,unique_prod2,max_generation,76,76,0,None,Free +1,unique_prod2,max_generation,77,77,0,None,Free +1,unique_prod2,max_generation,78,78,0,None,Free +1,unique_prod2,max_generation,79,79,0,None,Free +1,unique_prod2,max_generation,80,80,0,None,Free +1,unique_prod2,max_generation,81,81,0,None,Free +1,unique_prod2,max_generation,82,82,0,None,Free +1,unique_prod2,max_generation,83,83,0,None,Free +1,unique_prod2,max_generation,84,84,0,None,Free +1,unique_prod2,max_generation,85,85,0,None,Free +1,unique_prod2,max_generation,86,86,0,None,Free +1,unique_prod2,max_generation,87,87,0,None,Free +1,unique_prod2,max_generation,88,88,0,None,Free +1,unique_prod2,max_generation,89,89,0,None,Free +1,unique_prod2,max_generation,90,90,0,None,Free +1,unique_prod2,max_generation,91,91,0,None,Free +1,unique_prod2,max_generation,92,92,0,None,Free +1,unique_prod2,max_generation,93,93,0,None,Free +1,unique_prod2,max_generation,94,94,0,None,Free +1,unique_prod2,max_generation,95,95,0,None,Free +1,unique_prod2,max_generation,96,96,0,None,Free +1,unique_prod2,max_generation,97,97,0,None,Free +1,unique_prod2,max_generation,98,98,0,None,Free +1,unique_prod2,max_generation,99,99,0,None,Free +1,unique_prod2,max_generation,100,100,0,None,Free +1,unique_prod2,max_generation,101,101,0,None,Free +1,unique_prod2,max_generation,102,102,0,None,Free +1,unique_prod2,max_generation,103,103,0,None,Free +1,unique_prod2,max_generation,104,104,0,None,Free +1,unique_prod2,max_generation,105,105,0,None,Free +1,unique_prod2,max_generation,106,106,0,None,Free +1,unique_prod2,max_generation,107,107,0,None,Free +1,unique_prod2,max_generation,108,108,0,None,Free +1,unique_prod2,max_generation,109,109,0,None,Free +1,unique_prod2,max_generation,110,110,0,None,Free +1,unique_prod2,max_generation,111,111,0,None,Free +1,unique_prod2,max_generation,112,112,0,None,Free +1,unique_prod2,max_generation,113,113,0,None,Free +1,unique_prod2,max_generation,114,114,0,None,Free +1,unique_prod2,max_generation,115,115,0,None,Free +1,unique_prod2,max_generation,116,116,0,None,Free +1,unique_prod2,max_generation,117,117,0,None,Free +1,unique_prod2,max_generation,118,118,0,None,Free +1,unique_prod2,max_generation,119,119,0,None,Free +1,unique_prod2,max_generation,120,120,0,None,Free +1,unique_prod2,max_generation,121,121,0,None,Free +1,unique_prod2,max_generation,122,122,0,None,Free +1,unique_prod2,max_generation,123,123,0,None,Free +1,unique_prod2,max_generation,124,124,0,None,Free +1,unique_prod2,max_generation,125,125,0,None,Free +1,unique_prod2,max_generation,126,126,0,None,Free +1,unique_prod2,max_generation,127,127,0,None,Free +1,unique_prod2,max_generation,128,128,0,None,Free +1,unique_prod2,max_generation,129,129,0,None,Free +1,unique_prod2,max_generation,130,130,0,None,Free +1,unique_prod2,max_generation,131,131,0,None,Free +1,unique_prod2,max_generation,132,132,0,None,Free +1,unique_prod2,max_generation,133,133,0,None,Free +1,unique_prod2,max_generation,134,134,0,None,Free +1,unique_prod2,max_generation,135,135,0,None,Free +1,unique_prod2,max_generation,136,136,0,None,Free +1,unique_prod2,max_generation,137,137,0,None,Free +1,unique_prod2,max_generation,138,138,0,None,Free +1,unique_prod2,max_generation,139,139,0,None,Free +1,unique_prod2,max_generation,140,140,0,None,Free +1,unique_prod2,max_generation,141,141,0,None,Free +1,unique_prod2,max_generation,142,142,0,None,Free +1,unique_prod2,max_generation,143,143,0,None,Free +1,unique_prod2,max_generation,144,144,0,None,Free +1,unique_prod2,max_generation,145,145,0,None,Free +1,unique_prod2,max_generation,146,146,0,None,Free +1,unique_prod2,max_generation,147,147,0,None,Free +1,unique_prod2,max_generation,148,148,0,None,Free +1,unique_prod2,max_generation,149,149,0,None,Free +1,unique_prod2,max_generation,150,150,0,None,Free +1,unique_prod2,max_generation,151,151,0,None,Free +1,unique_prod2,max_generation,152,152,0,None,Free +1,unique_prod2,max_generation,153,153,0,None,Free +1,unique_prod2,max_generation,154,154,0,None,Free +1,unique_prod2,max_generation,155,155,0,None,Free +1,unique_prod2,max_generation,156,156,0,None,Free +1,unique_prod2,max_generation,157,157,0,None,Free +1,unique_prod2,max_generation,158,158,0,None,Free +1,unique_prod2,max_generation,159,159,0,None,Free +1,unique_prod2,max_generation,160,160,0,None,Free +1,unique_prod2,max_generation,161,161,0,None,Free +1,unique_prod2,max_generation,162,162,0,None,Free +1,unique_prod2,max_generation,163,163,0,None,Free +1,unique_prod2,max_generation,164,164,0,None,Free +1,unique_prod2,max_generation,165,165,0,None,Free +1,unique_prod2,max_generation,166,166,0,None,Free +1,unique_prod2,max_generation,167,167,0,None,Free +1,unique_prod2,max_generation,168,168,0,None,Free +1,unique_prod2,min_generation,1,1,0,None,Free +1,unique_prod2,min_generation,2,2,0,None,Free +1,unique_prod2,min_generation,3,3,0,None,Free +1,unique_prod2,min_generation,4,4,0,None,Free +1,unique_prod2,min_generation,5,5,0,None,Free +1,unique_prod2,min_generation,6,6,0,None,Free +1,unique_prod2,min_generation,7,7,0,None,Free +1,unique_prod2,min_generation,8,8,0,None,Free +1,unique_prod2,min_generation,9,9,0,None,Free +1,unique_prod2,min_generation,10,10,0,None,Free +1,unique_prod2,min_generation,11,11,0,None,Free +1,unique_prod2,min_generation,12,12,0,None,Free +1,unique_prod2,min_generation,13,13,0,None,Free +1,unique_prod2,min_generation,14,14,0,None,Free +1,unique_prod2,min_generation,15,15,0,None,Free +1,unique_prod2,min_generation,16,16,0,None,Free +1,unique_prod2,min_generation,17,17,0,None,Free +1,unique_prod2,min_generation,18,18,0,None,Free +1,unique_prod2,min_generation,19,19,0,None,Free +1,unique_prod2,min_generation,20,20,0,None,Free +1,unique_prod2,min_generation,21,21,0,None,Free +1,unique_prod2,min_generation,22,22,0,None,Free +1,unique_prod2,min_generation,23,23,0,None,Free +1,unique_prod2,min_generation,24,24,0,None,Free +1,unique_prod2,min_generation,25,25,0,None,Free +1,unique_prod2,min_generation,26,26,0,None,Free +1,unique_prod2,min_generation,27,27,0,None,Free +1,unique_prod2,min_generation,28,28,0,None,Free +1,unique_prod2,min_generation,29,29,0,None,Free +1,unique_prod2,min_generation,30,30,0,None,Free +1,unique_prod2,min_generation,31,31,0,None,Free +1,unique_prod2,min_generation,32,32,0,None,Free +1,unique_prod2,min_generation,33,33,0,None,Free +1,unique_prod2,min_generation,34,34,0,None,Free +1,unique_prod2,min_generation,35,35,0,None,Free +1,unique_prod2,min_generation,36,36,0,None,Free +1,unique_prod2,min_generation,37,37,0,None,Free +1,unique_prod2,min_generation,38,38,0,None,Free +1,unique_prod2,min_generation,39,39,0,None,Free +1,unique_prod2,min_generation,40,40,0,None,Free +1,unique_prod2,min_generation,41,41,0,None,Free +1,unique_prod2,min_generation,42,42,0,None,Free +1,unique_prod2,min_generation,43,43,0,None,Free +1,unique_prod2,min_generation,44,44,0,None,Free +1,unique_prod2,min_generation,45,45,0,None,Free +1,unique_prod2,min_generation,46,46,0,None,Free +1,unique_prod2,min_generation,47,47,0,None,Free +1,unique_prod2,min_generation,48,48,0,None,Free +1,unique_prod2,min_generation,49,49,0,None,Free +1,unique_prod2,min_generation,50,50,0,None,Free +1,unique_prod2,min_generation,51,51,0,None,Free +1,unique_prod2,min_generation,52,52,0,None,Free +1,unique_prod2,min_generation,53,53,0,None,Free +1,unique_prod2,min_generation,54,54,0,None,Free +1,unique_prod2,min_generation,55,55,0,None,Free +1,unique_prod2,min_generation,56,56,0,None,Free +1,unique_prod2,min_generation,57,57,0,None,Free +1,unique_prod2,min_generation,58,58,0,None,Free +1,unique_prod2,min_generation,59,59,0,None,Free +1,unique_prod2,min_generation,60,60,0,None,Free +1,unique_prod2,min_generation,61,61,0,None,Free +1,unique_prod2,min_generation,62,62,0,None,Free +1,unique_prod2,min_generation,63,63,0,None,Free +1,unique_prod2,min_generation,64,64,0,None,Free +1,unique_prod2,min_generation,65,65,0,None,Free +1,unique_prod2,min_generation,66,66,0,None,Free +1,unique_prod2,min_generation,67,67,0,None,Free +1,unique_prod2,min_generation,68,68,0,None,Free +1,unique_prod2,min_generation,69,69,0,None,Free +1,unique_prod2,min_generation,70,70,0,None,Free +1,unique_prod2,min_generation,71,71,0,None,Free +1,unique_prod2,min_generation,72,72,0,None,Free +1,unique_prod2,min_generation,73,73,0,None,Free +1,unique_prod2,min_generation,74,74,0,None,Free +1,unique_prod2,min_generation,75,75,0,None,Free +1,unique_prod2,min_generation,76,76,0,None,Free +1,unique_prod2,min_generation,77,77,0,None,Free +1,unique_prod2,min_generation,78,78,0,None,Free +1,unique_prod2,min_generation,79,79,0,None,Free +1,unique_prod2,min_generation,80,80,0,None,Free +1,unique_prod2,min_generation,81,81,0,None,Free +1,unique_prod2,min_generation,82,82,0,None,Free +1,unique_prod2,min_generation,83,83,0,None,Free +1,unique_prod2,min_generation,84,84,0,None,Free +1,unique_prod2,min_generation,85,85,0,None,Free +1,unique_prod2,min_generation,86,86,0,None,Free +1,unique_prod2,min_generation,87,87,0,None,Free +1,unique_prod2,min_generation,88,88,0,None,Free +1,unique_prod2,min_generation,89,89,0,None,Free +1,unique_prod2,min_generation,90,90,0,None,Free +1,unique_prod2,min_generation,91,91,0,None,Free +1,unique_prod2,min_generation,92,92,0,None,Free +1,unique_prod2,min_generation,93,93,0,None,Free +1,unique_prod2,min_generation,94,94,0,None,Free +1,unique_prod2,min_generation,95,95,0,None,Free +1,unique_prod2,min_generation,96,96,0,None,Free +1,unique_prod2,min_generation,97,97,0,None,Free +1,unique_prod2,min_generation,98,98,0,None,Free +1,unique_prod2,min_generation,99,99,0,None,Free +1,unique_prod2,min_generation,100,100,0,None,Free +1,unique_prod2,min_generation,101,101,0,None,Free +1,unique_prod2,min_generation,102,102,0,None,Free +1,unique_prod2,min_generation,103,103,0,None,Free +1,unique_prod2,min_generation,104,104,0,None,Free +1,unique_prod2,min_generation,105,105,0,None,Free +1,unique_prod2,min_generation,106,106,0,None,Free +1,unique_prod2,min_generation,107,107,0,None,Free +1,unique_prod2,min_generation,108,108,0,None,Free +1,unique_prod2,min_generation,109,109,0,None,Free +1,unique_prod2,min_generation,110,110,0,None,Free +1,unique_prod2,min_generation,111,111,0,None,Free +1,unique_prod2,min_generation,112,112,0,None,Free +1,unique_prod2,min_generation,113,113,0,None,Free +1,unique_prod2,min_generation,114,114,0,None,Free +1,unique_prod2,min_generation,115,115,0,None,Free +1,unique_prod2,min_generation,116,116,0,None,Free +1,unique_prod2,min_generation,117,117,0,None,Free +1,unique_prod2,min_generation,118,118,0,None,Free +1,unique_prod2,min_generation,119,119,0,None,Free +1,unique_prod2,min_generation,120,120,0,None,Free +1,unique_prod2,min_generation,121,121,0,None,Free +1,unique_prod2,min_generation,122,122,0,None,Free +1,unique_prod2,min_generation,123,123,0,None,Free +1,unique_prod2,min_generation,124,124,0,None,Free +1,unique_prod2,min_generation,125,125,0,None,Free +1,unique_prod2,min_generation,126,126,0,None,Free +1,unique_prod2,min_generation,127,127,0,None,Free +1,unique_prod2,min_generation,128,128,0,None,Free +1,unique_prod2,min_generation,129,129,0,None,Free +1,unique_prod2,min_generation,130,130,0,None,Free +1,unique_prod2,min_generation,131,131,0,None,Free +1,unique_prod2,min_generation,132,132,0,None,Free +1,unique_prod2,min_generation,133,133,0,None,Free +1,unique_prod2,min_generation,134,134,0,None,Free +1,unique_prod2,min_generation,135,135,0,None,Free +1,unique_prod2,min_generation,136,136,0,None,Free +1,unique_prod2,min_generation,137,137,0,None,Free +1,unique_prod2,min_generation,138,138,0,None,Free +1,unique_prod2,min_generation,139,139,0,None,Free +1,unique_prod2,min_generation,140,140,0,None,Free +1,unique_prod2,min_generation,141,141,0,None,Free +1,unique_prod2,min_generation,142,142,0,None,Free +1,unique_prod2,min_generation,143,143,0,None,Free +1,unique_prod2,min_generation,144,144,0,None,Free +1,unique_prod2,min_generation,145,145,0,None,Free +1,unique_prod2,min_generation,146,146,0,None,Free +1,unique_prod2,min_generation,147,147,0,None,Free +1,unique_prod2,min_generation,148,148,0,None,Free +1,unique_prod2,min_generation,149,149,0,None,Free +1,unique_prod2,min_generation,150,150,0,None,Free +1,unique_prod2,min_generation,151,151,0,None,Free +1,unique_prod2,min_generation,152,152,0,None,Free +1,unique_prod2,min_generation,153,153,0,None,Free +1,unique_prod2,min_generation,154,154,0,None,Free +1,unique_prod2,min_generation,155,155,0,None,Free +1,unique_prod2,min_generation,156,156,0,None,Free +1,unique_prod2,min_generation,157,157,0,None,Free +1,unique_prod2,min_generation,158,158,0,None,Free +1,unique_prod2,min_generation,159,159,0,None,Free +1,unique_prod2,min_generation,160,160,0,None,Free +1,unique_prod2,min_generation,161,161,0,None,Free +1,unique_prod2,min_generation,162,162,0,None,Free +1,unique_prod2,min_generation,163,163,0,None,Free +1,unique_prod2,min_generation,164,164,0,None,Free +1,unique_prod2,min_generation,165,165,0,None,Free +1,unique_prod2,min_generation,166,166,0,None,Free +1,unique_prod2,min_generation,167,167,0,None,Free +1,unique_prod2,min_generation,168,168,0,None,Free +1,unique_prod2,on_units_dynamics,1,1,0,None,Free +1,unique_prod2,on_units_dynamics,2,2,0,None,Free +1,unique_prod2,on_units_dynamics,3,3,0,None,Free +1,unique_prod2,on_units_dynamics,4,4,0,None,Free +1,unique_prod2,on_units_dynamics,5,5,0,None,Free +1,unique_prod2,on_units_dynamics,6,6,0,None,Free +1,unique_prod2,on_units_dynamics,7,7,0,None,Free +1,unique_prod2,on_units_dynamics,8,8,0,None,Free +1,unique_prod2,on_units_dynamics,9,9,0,None,Free +1,unique_prod2,on_units_dynamics,10,10,0,None,Free +1,unique_prod2,on_units_dynamics,11,11,0,None,Free +1,unique_prod2,on_units_dynamics,12,12,0,None,Free +1,unique_prod2,on_units_dynamics,13,13,0,None,Free +1,unique_prod2,on_units_dynamics,14,14,0,None,Free +1,unique_prod2,on_units_dynamics,15,15,0,None,Free +1,unique_prod2,on_units_dynamics,16,16,0,None,Free +1,unique_prod2,on_units_dynamics,17,17,0,None,Free +1,unique_prod2,on_units_dynamics,18,18,0,None,Free +1,unique_prod2,on_units_dynamics,19,19,0,None,Free +1,unique_prod2,on_units_dynamics,20,20,0,None,Free +1,unique_prod2,on_units_dynamics,21,21,0,None,Free +1,unique_prod2,on_units_dynamics,22,22,0,None,Free +1,unique_prod2,on_units_dynamics,23,23,0,None,Free +1,unique_prod2,on_units_dynamics,24,24,0,None,Free +1,unique_prod2,on_units_dynamics,25,25,0,None,Free +1,unique_prod2,on_units_dynamics,26,26,0,None,Free +1,unique_prod2,on_units_dynamics,27,27,0,None,Free +1,unique_prod2,on_units_dynamics,28,28,0,None,Free +1,unique_prod2,on_units_dynamics,29,29,0,None,Free +1,unique_prod2,on_units_dynamics,30,30,0,None,Free +1,unique_prod2,on_units_dynamics,31,31,0,None,Free +1,unique_prod2,on_units_dynamics,32,32,0,None,Free +1,unique_prod2,on_units_dynamics,33,33,0,None,Free +1,unique_prod2,on_units_dynamics,34,34,0,None,Free +1,unique_prod2,on_units_dynamics,35,35,0,None,Free +1,unique_prod2,on_units_dynamics,36,36,0,None,Free +1,unique_prod2,on_units_dynamics,37,37,0,None,Free +1,unique_prod2,on_units_dynamics,38,38,0,None,Free +1,unique_prod2,on_units_dynamics,39,39,0,None,Free +1,unique_prod2,on_units_dynamics,40,40,0,None,Free +1,unique_prod2,on_units_dynamics,41,41,0,None,Free +1,unique_prod2,on_units_dynamics,42,42,0,None,Free +1,unique_prod2,on_units_dynamics,43,43,0,None,Free +1,unique_prod2,on_units_dynamics,44,44,0,None,Free +1,unique_prod2,on_units_dynamics,45,45,0,None,Free +1,unique_prod2,on_units_dynamics,46,46,0,None,Free +1,unique_prod2,on_units_dynamics,47,47,0,None,Free +1,unique_prod2,on_units_dynamics,48,48,0,None,Free +1,unique_prod2,on_units_dynamics,49,49,0,None,Free +1,unique_prod2,on_units_dynamics,50,50,0,None,Free +1,unique_prod2,on_units_dynamics,51,51,0,None,Free +1,unique_prod2,on_units_dynamics,52,52,0,None,Free +1,unique_prod2,on_units_dynamics,53,53,0,None,Free +1,unique_prod2,on_units_dynamics,54,54,0,None,Free +1,unique_prod2,on_units_dynamics,55,55,0,None,Free +1,unique_prod2,on_units_dynamics,56,56,0,None,Free +1,unique_prod2,on_units_dynamics,57,57,0,None,Free +1,unique_prod2,on_units_dynamics,58,58,0,None,Free +1,unique_prod2,on_units_dynamics,59,59,0,None,Free +1,unique_prod2,on_units_dynamics,60,60,0,None,Free +1,unique_prod2,on_units_dynamics,61,61,0,None,Free +1,unique_prod2,on_units_dynamics,62,62,0,None,Free +1,unique_prod2,on_units_dynamics,63,63,0,None,Free +1,unique_prod2,on_units_dynamics,64,64,0,None,Free +1,unique_prod2,on_units_dynamics,65,65,0,None,Free +1,unique_prod2,on_units_dynamics,66,66,0,None,Free +1,unique_prod2,on_units_dynamics,67,67,0,None,Free +1,unique_prod2,on_units_dynamics,68,68,0,None,Free +1,unique_prod2,on_units_dynamics,69,69,0,None,Free +1,unique_prod2,on_units_dynamics,70,70,0,None,Free +1,unique_prod2,on_units_dynamics,71,71,0,None,Free +1,unique_prod2,on_units_dynamics,72,72,0,None,Free +1,unique_prod2,on_units_dynamics,73,73,0,None,Free +1,unique_prod2,on_units_dynamics,74,74,0,None,Free +1,unique_prod2,on_units_dynamics,75,75,0,None,Free +1,unique_prod2,on_units_dynamics,76,76,0,None,Free +1,unique_prod2,on_units_dynamics,77,77,0,None,Free +1,unique_prod2,on_units_dynamics,78,78,0,None,Free +1,unique_prod2,on_units_dynamics,79,79,0,None,Free +1,unique_prod2,on_units_dynamics,80,80,0,None,Free +1,unique_prod2,on_units_dynamics,81,81,0,None,Free +1,unique_prod2,on_units_dynamics,82,82,0,None,Free +1,unique_prod2,on_units_dynamics,83,83,0,None,Free +1,unique_prod2,on_units_dynamics,84,84,0,None,Free +1,unique_prod2,on_units_dynamics,85,85,0,None,Free +1,unique_prod2,on_units_dynamics,86,86,0,None,Free +1,unique_prod2,on_units_dynamics,87,87,0,None,Free +1,unique_prod2,on_units_dynamics,88,88,0,None,Free +1,unique_prod2,on_units_dynamics,89,89,0,None,Free +1,unique_prod2,on_units_dynamics,90,90,0,None,Free +1,unique_prod2,on_units_dynamics,91,91,0,None,Free +1,unique_prod2,on_units_dynamics,92,92,0,None,Free +1,unique_prod2,on_units_dynamics,93,93,0,None,Free +1,unique_prod2,on_units_dynamics,94,94,0,None,Free +1,unique_prod2,on_units_dynamics,95,95,0,None,Free +1,unique_prod2,on_units_dynamics,96,96,0,None,Free +1,unique_prod2,on_units_dynamics,97,97,0,None,Free +1,unique_prod2,on_units_dynamics,98,98,0,None,Free +1,unique_prod2,on_units_dynamics,99,99,0,None,Free +1,unique_prod2,on_units_dynamics,100,100,0,None,Free +1,unique_prod2,on_units_dynamics,101,101,0,None,Free +1,unique_prod2,on_units_dynamics,102,102,0,None,Free +1,unique_prod2,on_units_dynamics,103,103,0,None,Free +1,unique_prod2,on_units_dynamics,104,104,0,None,Free +1,unique_prod2,on_units_dynamics,105,105,0,None,Free +1,unique_prod2,on_units_dynamics,106,106,0,None,Free +1,unique_prod2,on_units_dynamics,107,107,0,None,Free +1,unique_prod2,on_units_dynamics,108,108,0,None,Free +1,unique_prod2,on_units_dynamics,109,109,0,None,Free +1,unique_prod2,on_units_dynamics,110,110,0,None,Free +1,unique_prod2,on_units_dynamics,111,111,0,None,Free +1,unique_prod2,on_units_dynamics,112,112,0,None,Free +1,unique_prod2,on_units_dynamics,113,113,0,None,Free +1,unique_prod2,on_units_dynamics,114,114,0,None,Free +1,unique_prod2,on_units_dynamics,115,115,0,None,Free +1,unique_prod2,on_units_dynamics,116,116,0,None,Free +1,unique_prod2,on_units_dynamics,117,117,0,None,Free +1,unique_prod2,on_units_dynamics,118,118,0,None,Free +1,unique_prod2,on_units_dynamics,119,119,0,None,Free +1,unique_prod2,on_units_dynamics,120,120,0,None,Free +1,unique_prod2,on_units_dynamics,121,121,0,None,Free +1,unique_prod2,on_units_dynamics,122,122,0,None,Free +1,unique_prod2,on_units_dynamics,123,123,0,None,Free +1,unique_prod2,on_units_dynamics,124,124,0,None,Free +1,unique_prod2,on_units_dynamics,125,125,0,None,Free +1,unique_prod2,on_units_dynamics,126,126,0,None,Free +1,unique_prod2,on_units_dynamics,127,127,0,None,Free +1,unique_prod2,on_units_dynamics,128,128,0,None,Free +1,unique_prod2,on_units_dynamics,129,129,0,None,Free +1,unique_prod2,on_units_dynamics,130,130,0,None,Free +1,unique_prod2,on_units_dynamics,131,131,0,None,Free +1,unique_prod2,on_units_dynamics,132,132,0,None,Free +1,unique_prod2,on_units_dynamics,133,133,0,None,Free +1,unique_prod2,on_units_dynamics,134,134,0,None,Free +1,unique_prod2,on_units_dynamics,135,135,0,None,Free +1,unique_prod2,on_units_dynamics,136,136,0,None,Free +1,unique_prod2,on_units_dynamics,137,137,0,None,Free +1,unique_prod2,on_units_dynamics,138,138,0,None,Free +1,unique_prod2,on_units_dynamics,139,139,0,None,Free +1,unique_prod2,on_units_dynamics,140,140,0,None,Free +1,unique_prod2,on_units_dynamics,141,141,0,None,Free +1,unique_prod2,on_units_dynamics,142,142,0,None,Free +1,unique_prod2,on_units_dynamics,143,143,0,None,Free +1,unique_prod2,on_units_dynamics,144,144,0,None,Free +1,unique_prod2,on_units_dynamics,145,145,0,None,Free +1,unique_prod2,on_units_dynamics,146,146,0,None,Free +1,unique_prod2,on_units_dynamics,147,147,0,None,Free +1,unique_prod2,on_units_dynamics,148,148,0,None,Free +1,unique_prod2,on_units_dynamics,149,149,0,None,Free +1,unique_prod2,on_units_dynamics,150,150,0,None,Free +1,unique_prod2,on_units_dynamics,151,151,0,None,Free +1,unique_prod2,on_units_dynamics,152,152,0,None,Free +1,unique_prod2,on_units_dynamics,153,153,0,None,Free +1,unique_prod2,on_units_dynamics,154,154,0,None,Free +1,unique_prod2,on_units_dynamics,155,155,0,None,Free +1,unique_prod2,on_units_dynamics,156,156,0,None,Free +1,unique_prod2,on_units_dynamics,157,157,0,None,Free +1,unique_prod2,on_units_dynamics,158,158,0,None,Free +1,unique_prod2,on_units_dynamics,159,159,0,None,Free +1,unique_prod2,on_units_dynamics,160,160,0,None,Free +1,unique_prod2,on_units_dynamics,161,161,0,None,Free +1,unique_prod2,on_units_dynamics,162,162,0,None,Free +1,unique_prod2,on_units_dynamics,163,163,0,None,Free +1,unique_prod2,on_units_dynamics,164,164,0,None,Free +1,unique_prod2,on_units_dynamics,165,165,0,None,Free +1,unique_prod2,on_units_dynamics,166,166,0,None,Free +1,unique_prod2,on_units_dynamics,167,167,0,None,Free +1,unique_prod2,on_units_dynamics,168,168,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,1,1,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,2,2,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,3,3,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,4,4,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,5,5,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,6,6,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,7,7,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,8,8,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,9,9,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,10,10,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,11,11,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,12,12,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,13,13,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,14,14,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,15,15,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,16,16,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,17,17,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,18,18,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,19,19,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,20,20,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,21,21,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,22,22,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,23,23,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,24,24,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,25,25,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,26,26,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,27,27,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,28,28,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,29,29,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,30,30,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,31,31,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,32,32,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,33,33,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,34,34,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,35,35,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,36,36,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,37,37,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,38,38,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,39,39,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,40,40,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,41,41,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,42,42,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,43,43,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,44,44,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,45,45,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,46,46,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,47,47,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,48,48,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,49,49,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,50,50,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,51,51,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,52,52,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,53,53,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,54,54,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,55,55,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,56,56,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,57,57,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,58,58,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,59,59,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,60,60,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,61,61,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,62,62,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,63,63,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,64,64,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,65,65,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,66,66,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,67,67,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,68,68,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,69,69,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,70,70,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,71,71,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,72,72,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,73,73,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,74,74,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,75,75,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,76,76,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,77,77,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,78,78,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,79,79,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,80,80,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,81,81,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,82,82,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,83,83,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,84,84,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,85,85,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,86,86,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,87,87,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,88,88,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,89,89,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,90,90,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,91,91,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,92,92,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,93,93,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,94,94,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,95,95,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,96,96,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,97,97,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,98,98,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,99,99,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,100,100,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,101,101,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,102,102,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,103,103,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,104,104,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,105,105,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,106,106,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,107,107,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,108,108,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,109,109,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,110,110,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,111,111,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,112,112,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,113,113,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,114,114,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,115,115,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,116,116,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,117,117,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,118,118,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,119,119,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,120,120,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,121,121,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,122,122,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,123,123,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,124,124,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,125,125,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,126,126,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,127,127,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,128,128,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,129,129,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,130,130,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,131,131,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,132,132,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,133,133,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,134,134,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,135,135,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,136,136,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,137,137,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,138,138,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,139,139,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,140,140,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,141,141,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,142,142,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,143,143,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,144,144,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,145,145,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,146,146,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,147,147,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,148,148,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,149,149,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,150,150,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,151,151,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,152,152,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,153,153,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,154,154,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,155,155,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,156,156,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,157,157,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,158,158,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,159,159,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,160,160,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,161,161,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,162,162,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,163,163,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,164,164,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,165,165,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,166,166,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,167,167,0,None,Free +1,unique_prod2,nb_failing_lower_than_stopping,168,168,0,None,Free +1,unique_prod2,min_up_duration,1,1,0,None,Free +1,unique_prod2,min_up_duration,2,2,0,None,Free +1,unique_prod2,min_up_duration,3,3,0,None,Free +1,unique_prod2,min_up_duration,4,4,0,None,Free +1,unique_prod2,min_up_duration,5,5,0,None,Free +1,unique_prod2,min_up_duration,6,6,0,None,Free +1,unique_prod2,min_up_duration,7,7,0,None,Free +1,unique_prod2,min_up_duration,8,8,0,None,Free +1,unique_prod2,min_up_duration,9,9,0,None,Free +1,unique_prod2,min_up_duration,10,10,0,None,Free +1,unique_prod2,min_up_duration,11,11,0,None,Free +1,unique_prod2,min_up_duration,12,12,0,None,Free +1,unique_prod2,min_up_duration,13,13,0,None,Free +1,unique_prod2,min_up_duration,14,14,0,None,Free +1,unique_prod2,min_up_duration,15,15,0,None,Free +1,unique_prod2,min_up_duration,16,16,0,None,Free +1,unique_prod2,min_up_duration,17,17,0,None,Free +1,unique_prod2,min_up_duration,18,18,0,None,Free +1,unique_prod2,min_up_duration,19,19,0,None,Free +1,unique_prod2,min_up_duration,20,20,0,None,Free +1,unique_prod2,min_up_duration,21,21,0,None,Free +1,unique_prod2,min_up_duration,22,22,0,None,Free +1,unique_prod2,min_up_duration,23,23,0,None,Free +1,unique_prod2,min_up_duration,24,24,0,None,Free +1,unique_prod2,min_up_duration,25,25,0,None,Free +1,unique_prod2,min_up_duration,26,26,0,None,Free +1,unique_prod2,min_up_duration,27,27,0,None,Free +1,unique_prod2,min_up_duration,28,28,0,None,Free +1,unique_prod2,min_up_duration,29,29,0,None,Free +1,unique_prod2,min_up_duration,30,30,0,None,Free +1,unique_prod2,min_up_duration,31,31,0,None,Free +1,unique_prod2,min_up_duration,32,32,0,None,Free +1,unique_prod2,min_up_duration,33,33,0,None,Free +1,unique_prod2,min_up_duration,34,34,0,None,Free +1,unique_prod2,min_up_duration,35,35,0,None,Free +1,unique_prod2,min_up_duration,36,36,0,None,Free +1,unique_prod2,min_up_duration,37,37,0,None,Free +1,unique_prod2,min_up_duration,38,38,0,None,Free +1,unique_prod2,min_up_duration,39,39,0,None,Free +1,unique_prod2,min_up_duration,40,40,0,None,Free +1,unique_prod2,min_up_duration,41,41,0,None,Free +1,unique_prod2,min_up_duration,42,42,0,None,Free +1,unique_prod2,min_up_duration,43,43,0,None,Free +1,unique_prod2,min_up_duration,44,44,0,None,Free +1,unique_prod2,min_up_duration,45,45,0,None,Free +1,unique_prod2,min_up_duration,46,46,0,None,Free +1,unique_prod2,min_up_duration,47,47,0,None,Free +1,unique_prod2,min_up_duration,48,48,0,None,Free +1,unique_prod2,min_up_duration,49,49,0,None,Free +1,unique_prod2,min_up_duration,50,50,0,None,Free +1,unique_prod2,min_up_duration,51,51,0,None,Free +1,unique_prod2,min_up_duration,52,52,0,None,Free +1,unique_prod2,min_up_duration,53,53,0,None,Free +1,unique_prod2,min_up_duration,54,54,0,None,Free +1,unique_prod2,min_up_duration,55,55,0,None,Free +1,unique_prod2,min_up_duration,56,56,0,None,Free +1,unique_prod2,min_up_duration,57,57,0,None,Free +1,unique_prod2,min_up_duration,58,58,0,None,Free +1,unique_prod2,min_up_duration,59,59,0,None,Free +1,unique_prod2,min_up_duration,60,60,0,None,Free +1,unique_prod2,min_up_duration,61,61,0,None,Free +1,unique_prod2,min_up_duration,62,62,0,None,Free +1,unique_prod2,min_up_duration,63,63,0,None,Free +1,unique_prod2,min_up_duration,64,64,0,None,Free +1,unique_prod2,min_up_duration,65,65,0,None,Free +1,unique_prod2,min_up_duration,66,66,0,None,Free +1,unique_prod2,min_up_duration,67,67,0,None,Free +1,unique_prod2,min_up_duration,68,68,0,None,Free +1,unique_prod2,min_up_duration,69,69,0,None,Free +1,unique_prod2,min_up_duration,70,70,0,None,Free +1,unique_prod2,min_up_duration,71,71,0,None,Free +1,unique_prod2,min_up_duration,72,72,0,None,Free +1,unique_prod2,min_up_duration,73,73,0,None,Free +1,unique_prod2,min_up_duration,74,74,0,None,Free +1,unique_prod2,min_up_duration,75,75,0,None,Free +1,unique_prod2,min_up_duration,76,76,0,None,Free +1,unique_prod2,min_up_duration,77,77,0,None,Free +1,unique_prod2,min_up_duration,78,78,0,None,Free +1,unique_prod2,min_up_duration,79,79,0,None,Free +1,unique_prod2,min_up_duration,80,80,0,None,Free +1,unique_prod2,min_up_duration,81,81,0,None,Free +1,unique_prod2,min_up_duration,82,82,0,None,Free +1,unique_prod2,min_up_duration,83,83,0,None,Free +1,unique_prod2,min_up_duration,84,84,0,None,Free +1,unique_prod2,min_up_duration,85,85,0,None,Free +1,unique_prod2,min_up_duration,86,86,0,None,Free +1,unique_prod2,min_up_duration,87,87,0,None,Free +1,unique_prod2,min_up_duration,88,88,0,None,Free +1,unique_prod2,min_up_duration,89,89,0,None,Free +1,unique_prod2,min_up_duration,90,90,0,None,Free +1,unique_prod2,min_up_duration,91,91,0,None,Free +1,unique_prod2,min_up_duration,92,92,0,None,Free +1,unique_prod2,min_up_duration,93,93,0,None,Free +1,unique_prod2,min_up_duration,94,94,0,None,Free +1,unique_prod2,min_up_duration,95,95,0,None,Free +1,unique_prod2,min_up_duration,96,96,0,None,Free +1,unique_prod2,min_up_duration,97,97,0,None,Free +1,unique_prod2,min_up_duration,98,98,0,None,Free +1,unique_prod2,min_up_duration,99,99,0,None,Free +1,unique_prod2,min_up_duration,100,100,0,None,Free +1,unique_prod2,min_up_duration,101,101,0,None,Free +1,unique_prod2,min_up_duration,102,102,0,None,Free +1,unique_prod2,min_up_duration,103,103,0,None,Free +1,unique_prod2,min_up_duration,104,104,0,None,Free +1,unique_prod2,min_up_duration,105,105,0,None,Free +1,unique_prod2,min_up_duration,106,106,0,None,Free +1,unique_prod2,min_up_duration,107,107,0,None,Free +1,unique_prod2,min_up_duration,108,108,0,None,Free +1,unique_prod2,min_up_duration,109,109,0,None,Free +1,unique_prod2,min_up_duration,110,110,0,None,Free +1,unique_prod2,min_up_duration,111,111,0,None,Free +1,unique_prod2,min_up_duration,112,112,0,None,Free +1,unique_prod2,min_up_duration,113,113,0,None,Free +1,unique_prod2,min_up_duration,114,114,0,None,Free +1,unique_prod2,min_up_duration,115,115,0,None,Free +1,unique_prod2,min_up_duration,116,116,0,None,Free +1,unique_prod2,min_up_duration,117,117,0,None,Free +1,unique_prod2,min_up_duration,118,118,0,None,Free +1,unique_prod2,min_up_duration,119,119,0,None,Free +1,unique_prod2,min_up_duration,120,120,0,None,Free +1,unique_prod2,min_up_duration,121,121,0,None,Free +1,unique_prod2,min_up_duration,122,122,0,None,Free +1,unique_prod2,min_up_duration,123,123,0,None,Free +1,unique_prod2,min_up_duration,124,124,0,None,Free +1,unique_prod2,min_up_duration,125,125,0,None,Free +1,unique_prod2,min_up_duration,126,126,0,None,Free +1,unique_prod2,min_up_duration,127,127,0,None,Free +1,unique_prod2,min_up_duration,128,128,0,None,Free +1,unique_prod2,min_up_duration,129,129,0,None,Free +1,unique_prod2,min_up_duration,130,130,0,None,Free +1,unique_prod2,min_up_duration,131,131,0,None,Free +1,unique_prod2,min_up_duration,132,132,0,None,Free +1,unique_prod2,min_up_duration,133,133,0,None,Free +1,unique_prod2,min_up_duration,134,134,0,None,Free +1,unique_prod2,min_up_duration,135,135,0,None,Free +1,unique_prod2,min_up_duration,136,136,0,None,Free +1,unique_prod2,min_up_duration,137,137,0,None,Free +1,unique_prod2,min_up_duration,138,138,0,None,Free +1,unique_prod2,min_up_duration,139,139,0,None,Free +1,unique_prod2,min_up_duration,140,140,0,None,Free +1,unique_prod2,min_up_duration,141,141,0,None,Free +1,unique_prod2,min_up_duration,142,142,0,None,Free +1,unique_prod2,min_up_duration,143,143,0,None,Free +1,unique_prod2,min_up_duration,144,144,0,None,Free +1,unique_prod2,min_up_duration,145,145,0,None,Free +1,unique_prod2,min_up_duration,146,146,0,None,Free +1,unique_prod2,min_up_duration,147,147,0,None,Free +1,unique_prod2,min_up_duration,148,148,0,None,Free +1,unique_prod2,min_up_duration,149,149,0,None,Free +1,unique_prod2,min_up_duration,150,150,0,None,Free +1,unique_prod2,min_up_duration,151,151,0,None,Free +1,unique_prod2,min_up_duration,152,152,0,None,Free +1,unique_prod2,min_up_duration,153,153,0,None,Free +1,unique_prod2,min_up_duration,154,154,0,None,Free +1,unique_prod2,min_up_duration,155,155,0,None,Free +1,unique_prod2,min_up_duration,156,156,0,None,Free +1,unique_prod2,min_up_duration,157,157,0,None,Free +1,unique_prod2,min_up_duration,158,158,0,None,Free +1,unique_prod2,min_up_duration,159,159,0,None,Free +1,unique_prod2,min_up_duration,160,160,0,None,Free +1,unique_prod2,min_up_duration,161,161,0,None,Free +1,unique_prod2,min_up_duration,162,162,0,None,Free +1,unique_prod2,min_up_duration,163,163,0,None,Free +1,unique_prod2,min_up_duration,164,164,0,None,Free +1,unique_prod2,min_up_duration,165,165,0,None,Free +1,unique_prod2,min_up_duration,166,166,0,None,Free +1,unique_prod2,min_up_duration,167,167,0,None,Free +1,unique_prod2,min_up_duration,168,168,0,None,Free +1,unique_prod2,min_down_duration,1,1,0,None,Free +1,unique_prod2,min_down_duration,2,2,0,None,Free +1,unique_prod2,min_down_duration,3,3,0,None,Free +1,unique_prod2,min_down_duration,4,4,0,None,Free +1,unique_prod2,min_down_duration,5,5,0,None,Free +1,unique_prod2,min_down_duration,6,6,0,None,Free +1,unique_prod2,min_down_duration,7,7,0,None,Free +1,unique_prod2,min_down_duration,8,8,0,None,Free +1,unique_prod2,min_down_duration,9,9,0,None,Free +1,unique_prod2,min_down_duration,10,10,0,None,Free +1,unique_prod2,min_down_duration,11,11,0,None,Free +1,unique_prod2,min_down_duration,12,12,0,None,Free +1,unique_prod2,min_down_duration,13,13,0,None,Free +1,unique_prod2,min_down_duration,14,14,0,None,Free +1,unique_prod2,min_down_duration,15,15,0,None,Free +1,unique_prod2,min_down_duration,16,16,0,None,Free +1,unique_prod2,min_down_duration,17,17,0,None,Free +1,unique_prod2,min_down_duration,18,18,0,None,Free +1,unique_prod2,min_down_duration,19,19,0,None,Free +1,unique_prod2,min_down_duration,20,20,0,None,Free +1,unique_prod2,min_down_duration,21,21,0,None,Free +1,unique_prod2,min_down_duration,22,22,0,None,Free +1,unique_prod2,min_down_duration,23,23,0,None,Free +1,unique_prod2,min_down_duration,24,24,0,None,Free +1,unique_prod2,min_down_duration,25,25,0,None,Free +1,unique_prod2,min_down_duration,26,26,0,None,Free +1,unique_prod2,min_down_duration,27,27,0,None,Free +1,unique_prod2,min_down_duration,28,28,0,None,Free +1,unique_prod2,min_down_duration,29,29,0,None,Free +1,unique_prod2,min_down_duration,30,30,0,None,Free +1,unique_prod2,min_down_duration,31,31,0,None,Free +1,unique_prod2,min_down_duration,32,32,0,None,Free +1,unique_prod2,min_down_duration,33,33,0,None,Free +1,unique_prod2,min_down_duration,34,34,0,None,Free +1,unique_prod2,min_down_duration,35,35,0,None,Free +1,unique_prod2,min_down_duration,36,36,0,None,Free +1,unique_prod2,min_down_duration,37,37,0,None,Free +1,unique_prod2,min_down_duration,38,38,0,None,Free +1,unique_prod2,min_down_duration,39,39,0,None,Free +1,unique_prod2,min_down_duration,40,40,0,None,Free +1,unique_prod2,min_down_duration,41,41,0,None,Free +1,unique_prod2,min_down_duration,42,42,0,None,Free +1,unique_prod2,min_down_duration,43,43,0,None,Free +1,unique_prod2,min_down_duration,44,44,0,None,Free +1,unique_prod2,min_down_duration,45,45,0,None,Free +1,unique_prod2,min_down_duration,46,46,0,None,Free +1,unique_prod2,min_down_duration,47,47,0,None,Free +1,unique_prod2,min_down_duration,48,48,0,None,Free +1,unique_prod2,min_down_duration,49,49,0,None,Free +1,unique_prod2,min_down_duration,50,50,0,None,Free +1,unique_prod2,min_down_duration,51,51,0,None,Free +1,unique_prod2,min_down_duration,52,52,0,None,Free +1,unique_prod2,min_down_duration,53,53,0,None,Free +1,unique_prod2,min_down_duration,54,54,0,None,Free +1,unique_prod2,min_down_duration,55,55,0,None,Free +1,unique_prod2,min_down_duration,56,56,0,None,Free +1,unique_prod2,min_down_duration,57,57,0,None,Free +1,unique_prod2,min_down_duration,58,58,0,None,Free +1,unique_prod2,min_down_duration,59,59,0,None,Free +1,unique_prod2,min_down_duration,60,60,0,None,Free +1,unique_prod2,min_down_duration,61,61,0,None,Free +1,unique_prod2,min_down_duration,62,62,0,None,Free +1,unique_prod2,min_down_duration,63,63,0,None,Free +1,unique_prod2,min_down_duration,64,64,0,None,Free +1,unique_prod2,min_down_duration,65,65,0,None,Free +1,unique_prod2,min_down_duration,66,66,0,None,Free +1,unique_prod2,min_down_duration,67,67,0,None,Free +1,unique_prod2,min_down_duration,68,68,0,None,Free +1,unique_prod2,min_down_duration,69,69,0,None,Free +1,unique_prod2,min_down_duration,70,70,0,None,Free +1,unique_prod2,min_down_duration,71,71,0,None,Free +1,unique_prod2,min_down_duration,72,72,0,None,Free +1,unique_prod2,min_down_duration,73,73,0,None,Free +1,unique_prod2,min_down_duration,74,74,0,None,Free +1,unique_prod2,min_down_duration,75,75,0,None,Free +1,unique_prod2,min_down_duration,76,76,0,None,Free +1,unique_prod2,min_down_duration,77,77,0,None,Free +1,unique_prod2,min_down_duration,78,78,0,None,Free +1,unique_prod2,min_down_duration,79,79,0,None,Free +1,unique_prod2,min_down_duration,80,80,0,None,Free +1,unique_prod2,min_down_duration,81,81,0,None,Free +1,unique_prod2,min_down_duration,82,82,0,None,Free +1,unique_prod2,min_down_duration,83,83,0,None,Free +1,unique_prod2,min_down_duration,84,84,0,None,Free +1,unique_prod2,min_down_duration,85,85,0,None,Free +1,unique_prod2,min_down_duration,86,86,0,None,Free +1,unique_prod2,min_down_duration,87,87,0,None,Free +1,unique_prod2,min_down_duration,88,88,0,None,Free +1,unique_prod2,min_down_duration,89,89,0,None,Free +1,unique_prod2,min_down_duration,90,90,0,None,Free +1,unique_prod2,min_down_duration,91,91,0,None,Free +1,unique_prod2,min_down_duration,92,92,0,None,Free +1,unique_prod2,min_down_duration,93,93,0,None,Free +1,unique_prod2,min_down_duration,94,94,0,None,Free +1,unique_prod2,min_down_duration,95,95,0,None,Free +1,unique_prod2,min_down_duration,96,96,0,None,Free +1,unique_prod2,min_down_duration,97,97,0,None,Free +1,unique_prod2,min_down_duration,98,98,0,None,Free +1,unique_prod2,min_down_duration,99,99,0,None,Free +1,unique_prod2,min_down_duration,100,100,0,None,Free +1,unique_prod2,min_down_duration,101,101,0,None,Free +1,unique_prod2,min_down_duration,102,102,0,None,Free +1,unique_prod2,min_down_duration,103,103,0,None,Free +1,unique_prod2,min_down_duration,104,104,0,None,Free +1,unique_prod2,min_down_duration,105,105,0,None,Free +1,unique_prod2,min_down_duration,106,106,0,None,Free +1,unique_prod2,min_down_duration,107,107,0,None,Free +1,unique_prod2,min_down_duration,108,108,0,None,Free +1,unique_prod2,min_down_duration,109,109,0,None,Free +1,unique_prod2,min_down_duration,110,110,0,None,Free +1,unique_prod2,min_down_duration,111,111,0,None,Free +1,unique_prod2,min_down_duration,112,112,0,None,Free +1,unique_prod2,min_down_duration,113,113,0,None,Free +1,unique_prod2,min_down_duration,114,114,0,None,Free +1,unique_prod2,min_down_duration,115,115,0,None,Free +1,unique_prod2,min_down_duration,116,116,0,None,Free +1,unique_prod2,min_down_duration,117,117,0,None,Free +1,unique_prod2,min_down_duration,118,118,0,None,Free +1,unique_prod2,min_down_duration,119,119,0,None,Free +1,unique_prod2,min_down_duration,120,120,0,None,Free +1,unique_prod2,min_down_duration,121,121,0,None,Free +1,unique_prod2,min_down_duration,122,122,0,None,Free +1,unique_prod2,min_down_duration,123,123,0,None,Free +1,unique_prod2,min_down_duration,124,124,0,None,Free +1,unique_prod2,min_down_duration,125,125,0,None,Free +1,unique_prod2,min_down_duration,126,126,0,None,Free +1,unique_prod2,min_down_duration,127,127,0,None,Free +1,unique_prod2,min_down_duration,128,128,0,None,Free +1,unique_prod2,min_down_duration,129,129,0,None,Free +1,unique_prod2,min_down_duration,130,130,0,None,Free +1,unique_prod2,min_down_duration,131,131,0,None,Free +1,unique_prod2,min_down_duration,132,132,0,None,Free +1,unique_prod2,min_down_duration,133,133,0,None,Free +1,unique_prod2,min_down_duration,134,134,0,None,Free +1,unique_prod2,min_down_duration,135,135,0,None,Free +1,unique_prod2,min_down_duration,136,136,0,None,Free +1,unique_prod2,min_down_duration,137,137,0,None,Free +1,unique_prod2,min_down_duration,138,138,0,None,Free +1,unique_prod2,min_down_duration,139,139,0,None,Free +1,unique_prod2,min_down_duration,140,140,0,None,Free +1,unique_prod2,min_down_duration,141,141,0,None,Free +1,unique_prod2,min_down_duration,142,142,0,None,Free +1,unique_prod2,min_down_duration,143,143,0,None,Free +1,unique_prod2,min_down_duration,144,144,0,None,Free +1,unique_prod2,min_down_duration,145,145,0,None,Free +1,unique_prod2,min_down_duration,146,146,0,None,Free +1,unique_prod2,min_down_duration,147,147,0,None,Free +1,unique_prod2,min_down_duration,148,148,0,None,Free +1,unique_prod2,min_down_duration,149,149,0,None,Free +1,unique_prod2,min_down_duration,150,150,0,None,Free +1,unique_prod2,min_down_duration,151,151,0,None,Free +1,unique_prod2,min_down_duration,152,152,0,None,Free +1,unique_prod2,min_down_duration,153,153,0,None,Free +1,unique_prod2,min_down_duration,154,154,0,None,Free +1,unique_prod2,min_down_duration,155,155,0,None,Free +1,unique_prod2,min_down_duration,156,156,0,None,Free +1,unique_prod2,min_down_duration,157,157,0,None,Free +1,unique_prod2,min_down_duration,158,158,0,None,Free +1,unique_prod2,min_down_duration,159,159,0,None,Free +1,unique_prod2,min_down_duration,160,160,0,None,Free +1,unique_prod2,min_down_duration,161,161,0,None,Free +1,unique_prod2,min_down_duration,162,162,0,None,Free +1,unique_prod2,min_down_duration,163,163,0,None,Free +1,unique_prod2,min_down_duration,164,164,0,None,Free +1,unique_prod2,min_down_duration,165,165,0,None,Free +1,unique_prod2,min_down_duration,166,166,0,None,Free +1,unique_prod2,min_down_duration,167,167,0,None,Free +1,unique_prod2,min_down_duration,168,168,0,None,Free +1,unique_prod2,balance_port.flow,1,1,0,200,None +1,unique_prod2,balance_port.flow,2,2,0,200,None +1,unique_prod2,balance_port.flow,3,3,0,200,None +1,unique_prod2,balance_port.flow,4,4,0,200,None +1,unique_prod2,balance_port.flow,5,5,0,200,None +1,unique_prod2,balance_port.flow,6,6,0,200,None +1,unique_prod2,balance_port.flow,7,7,0,200,None +1,unique_prod2,balance_port.flow,8,8,0,200,None +1,unique_prod2,balance_port.flow,9,9,0,200,None +1,unique_prod2,balance_port.flow,10,10,0,200,None +1,unique_prod2,balance_port.flow,11,11,0,200,None +1,unique_prod2,balance_port.flow,12,12,0,200,None +1,unique_prod2,balance_port.flow,13,13,0,200,None +1,unique_prod2,balance_port.flow,14,14,0,200,None +1,unique_prod2,balance_port.flow,15,15,0,200,None +1,unique_prod2,balance_port.flow,16,16,0,200,None +1,unique_prod2,balance_port.flow,17,17,0,200,None +1,unique_prod2,balance_port.flow,18,18,0,200,None +1,unique_prod2,balance_port.flow,19,19,0,200,None +1,unique_prod2,balance_port.flow,20,20,0,200,None +1,unique_prod2,balance_port.flow,21,21,0,200,None +1,unique_prod2,balance_port.flow,22,22,0,178.015361478573,None +1,unique_prod2,balance_port.flow,23,23,0,200,None +1,unique_prod2,balance_port.flow,24,24,0,200,None +1,unique_prod2,balance_port.flow,25,25,0,200,None +1,unique_prod2,balance_port.flow,26,26,0,200,None +1,unique_prod2,balance_port.flow,27,27,0,200,None +1,unique_prod2,balance_port.flow,28,28,0,200,None +1,unique_prod2,balance_port.flow,29,29,0,200,None +1,unique_prod2,balance_port.flow,30,30,0,200,None +1,unique_prod2,balance_port.flow,31,31,0,200,None +1,unique_prod2,balance_port.flow,32,32,0,200,None +1,unique_prod2,balance_port.flow,33,33,0,200,None +1,unique_prod2,balance_port.flow,34,34,0,200,None +1,unique_prod2,balance_port.flow,35,35,0,200,None +1,unique_prod2,balance_port.flow,36,36,0,200,None +1,unique_prod2,balance_port.flow,37,37,0,0,None +1,unique_prod2,balance_port.flow,38,38,0,60,None +1,unique_prod2,balance_port.flow,39,39,0,0,None +1,unique_prod2,balance_port.flow,40,40,0,60,None +1,unique_prod2,balance_port.flow,41,41,0,0,None +1,unique_prod2,balance_port.flow,42,42,0,60,None +1,unique_prod2,balance_port.flow,43,43,0,0,None +1,unique_prod2,balance_port.flow,44,44,0,60,None +1,unique_prod2,balance_port.flow,45,45,0,0,None +1,unique_prod2,balance_port.flow,46,46,0,200,None +1,unique_prod2,balance_port.flow,47,47,0,200,None +1,unique_prod2,balance_port.flow,48,48,0,200,None +1,unique_prod2,balance_port.flow,49,49,0,200,None +1,unique_prod2,balance_port.flow,50,50,0,200,None +1,unique_prod2,balance_port.flow,51,51,0,200,None +1,unique_prod2,balance_port.flow,52,52,0,200,None +1,unique_prod2,balance_port.flow,53,53,0,200,None +1,unique_prod2,balance_port.flow,54,54,0,200,None +1,unique_prod2,balance_port.flow,55,55,0,200,None +1,unique_prod2,balance_port.flow,56,56,0,200,None +1,unique_prod2,balance_port.flow,57,57,0,200,None +1,unique_prod2,balance_port.flow,58,58,0,200,None +1,unique_prod2,balance_port.flow,59,59,0,200,None +1,unique_prod2,balance_port.flow,60,60,0,200,None +1,unique_prod2,balance_port.flow,61,61,0,200,None +1,unique_prod2,balance_port.flow,62,62,0,200,None +1,unique_prod2,balance_port.flow,63,63,0,200,None +1,unique_prod2,balance_port.flow,64,64,0,60,None +1,unique_prod2,balance_port.flow,65,65,0,0,None +1,unique_prod2,balance_port.flow,66,66,0,200,None +1,unique_prod2,balance_port.flow,67,67,0,0,None +1,unique_prod2,balance_port.flow,68,68,0,200,None +1,unique_prod2,balance_port.flow,69,69,0,200,None +1,unique_prod2,balance_port.flow,70,70,0,200,None +1,unique_prod2,balance_port.flow,71,71,0,200,None +1,unique_prod2,balance_port.flow,72,72,0,200,None +1,unique_prod2,balance_port.flow,73,73,0,200,None +1,unique_prod2,balance_port.flow,74,74,0,200,None +1,unique_prod2,balance_port.flow,75,75,0,200,None +1,unique_prod2,balance_port.flow,76,76,0,200,None +1,unique_prod2,balance_port.flow,77,77,0,200,None +1,unique_prod2,balance_port.flow,78,78,0,200,None +1,unique_prod2,balance_port.flow,79,79,0,200,None +1,unique_prod2,balance_port.flow,80,80,0,200,None +1,unique_prod2,balance_port.flow,81,81,0,200,None +1,unique_prod2,balance_port.flow,82,82,0,200,None +1,unique_prod2,balance_port.flow,83,83,0,200,None +1,unique_prod2,balance_port.flow,84,84,0,200,None +1,unique_prod2,balance_port.flow,85,85,0,200,None +1,unique_prod2,balance_port.flow,86,86,0,60,None +1,unique_prod2,balance_port.flow,87,87,0,0,None +1,unique_prod2,balance_port.flow,88,88,0,60,None +1,unique_prod2,balance_port.flow,89,89,0,200,None +1,unique_prod2,balance_port.flow,90,90,0,200,None +1,unique_prod2,balance_port.flow,91,91,0,200,None +1,unique_prod2,balance_port.flow,92,92,0,200,None +1,unique_prod2,balance_port.flow,93,93,0,200,None +1,unique_prod2,balance_port.flow,94,94,0,200,None +1,unique_prod2,balance_port.flow,95,95,0,200,None +1,unique_prod2,balance_port.flow,96,96,0,200,None +1,unique_prod2,balance_port.flow,97,97,0,200,None +1,unique_prod2,balance_port.flow,98,98,0,200,None +1,unique_prod2,balance_port.flow,99,99,0,200,None +1,unique_prod2,balance_port.flow,100,100,0,60,None +1,unique_prod2,balance_port.flow,101,101,0,200,None +1,unique_prod2,balance_port.flow,102,102,0,200,None +1,unique_prod2,balance_port.flow,103,103,0,200,None +1,unique_prod2,balance_port.flow,104,104,0,200,None +1,unique_prod2,balance_port.flow,105,105,0,200,None +1,unique_prod2,balance_port.flow,106,106,0,200,None +1,unique_prod2,balance_port.flow,107,107,0,200,None +1,unique_prod2,balance_port.flow,108,108,0,200,None +1,unique_prod2,balance_port.flow,109,109,0,200,None +1,unique_prod2,balance_port.flow,110,110,0,200,None +1,unique_prod2,balance_port.flow,111,111,0,200,None +1,unique_prod2,balance_port.flow,112,112,0,200,None +1,unique_prod2,balance_port.flow,113,113,0,200,None +1,unique_prod2,balance_port.flow,114,114,0,200,None +1,unique_prod2,balance_port.flow,115,115,0,200,None +1,unique_prod2,balance_port.flow,116,116,0,60,None +1,unique_prod2,balance_port.flow,117,117,0,0,None +1,unique_prod2,balance_port.flow,118,118,0,60,None +1,unique_prod2,balance_port.flow,119,119,0,0,None +1,unique_prod2,balance_port.flow,120,120,0,60,None +1,unique_prod2,balance_port.flow,121,121,0,0,None +1,unique_prod2,balance_port.flow,122,122,0,60,None +1,unique_prod2,balance_port.flow,123,123,0,0,None +1,unique_prod2,balance_port.flow,124,124,0,60,None +1,unique_prod2,balance_port.flow,125,125,0,200,None +1,unique_prod2,balance_port.flow,126,126,0,200,None +1,unique_prod2,balance_port.flow,127,127,0,200,None +1,unique_prod2,balance_port.flow,128,128,0,200,None +1,unique_prod2,balance_port.flow,129,129,0,200,None +1,unique_prod2,balance_port.flow,130,130,0,200,None +1,unique_prod2,balance_port.flow,131,131,0,200,None +1,unique_prod2,balance_port.flow,132,132,0,200,None +1,unique_prod2,balance_port.flow,133,133,0,200,None +1,unique_prod2,balance_port.flow,134,134,0,200,None +1,unique_prod2,balance_port.flow,135,135,0,200,None +1,unique_prod2,balance_port.flow,136,136,0,200,None +1,unique_prod2,balance_port.flow,137,137,0,200,None +1,unique_prod2,balance_port.flow,138,138,0,200,None +1,unique_prod2,balance_port.flow,139,139,0,200,None +1,unique_prod2,balance_port.flow,140,140,0,200,None +1,unique_prod2,balance_port.flow,141,141,0,200,None +1,unique_prod2,balance_port.flow,142,142,0,200,None +1,unique_prod2,balance_port.flow,143,143,0,200,None +1,unique_prod2,balance_port.flow,144,144,0,200,None +1,unique_prod2,balance_port.flow,145,145,0,200,None +1,unique_prod2,balance_port.flow,146,146,0,60,None +1,unique_prod2,balance_port.flow,147,147,0,0,None +1,unique_prod2,balance_port.flow,148,148,0,60,None +1,unique_prod2,balance_port.flow,149,149,0,0,None +1,unique_prod2,balance_port.flow,150,150,0,60,None +1,unique_prod2,balance_port.flow,151,151,0,0,None +1,unique_prod2,balance_port.flow,152,152,0,60,None +1,unique_prod2,balance_port.flow,153,153,0,0,None +1,unique_prod2,balance_port.flow,154,154,0,60,None +1,unique_prod2,balance_port.flow,155,155,0,200,None +1,unique_prod2,balance_port.flow,156,156,0,200,None +1,unique_prod2,balance_port.flow,157,157,0,200,None +1,unique_prod2,balance_port.flow,158,158,0,200,None +1,unique_prod2,balance_port.flow,159,159,0,200,None +1,unique_prod2,balance_port.flow,160,160,0,200,None +1,unique_prod2,balance_port.flow,161,161,0,200,None +1,unique_prod2,balance_port.flow,162,162,0,200,None +1,unique_prod2,balance_port.flow,163,163,0,200,None +1,unique_prod2,balance_port.flow,164,164,0,200,None +1,unique_prod2,balance_port.flow,165,165,0,0,None +1,unique_prod2,balance_port.flow,166,166,0,200,None +1,unique_prod2,balance_port.flow,167,167,0,200,None +1,unique_prod2,balance_port.flow,168,168,0,200,None +1,unique_prod3,generation,1,1,0,86.7446002046025,Free +1,unique_prod3,generation,2,2,0,35.0406665399429,Free +1,unique_prod3,generation,3,3,0,115.227154977506,Free +1,unique_prod3,generation,4,4,0,70.2903745370878,Free +1,unique_prod3,generation,5,5,0,107.757555377475,Free +1,unique_prod3,generation,6,6,0,44.3839373685811,Free +1,unique_prod3,generation,7,7,0,27.9081239778326,Free +1,unique_prod3,generation,8,8,0,62.1306682891878,Free +1,unique_prod3,generation,9,9,0,46.3806909454445,Free +1,unique_prod3,generation,10,10,0,104.807109528294,Free +1,unique_prod3,generation,11,11,0,43.8799050611277,Free +1,unique_prod3,generation,12,12,0,95.2770752239112,Free +1,unique_prod3,generation,13,13,0,61.6467962696488,Free +1,unique_prod3,generation,14,14,0,41.4966259154289,Free +1,unique_prod3,generation,15,15,0,95.3797838008454,Free +1,unique_prod3,generation,16,16,0,30.6798797731156,Free +1,unique_prod3,generation,17,17,0,108.992371620552,Free +1,unique_prod3,generation,18,18,0,115.453862325268,Free +1,unique_prod3,generation,19,19,0,113.389769729041,Free +1,unique_prod3,generation,20,20,0,63.8813714923112,Free +1,unique_prod3,generation,21,21,0,26.7822393011396,Free +1,unique_prod3,generation,22,22,0,118.274638521427,Free +1,unique_prod3,generation,23,23,0,56.6052176289877,Free +1,unique_prod3,generation,24,24,0,91.8419706133252,Free +1,unique_prod3,generation,25,25,0,58.7401984903259,Free +1,unique_prod3,generation,26,26,0,27.3701662033307,Free +1,unique_prod3,generation,27,27,0,106.085592311079,Free +1,unique_prod3,generation,28,28,0,87.0963367814873,Free +1,unique_prod3,generation,29,29,0,97.5055674379429,Free +1,unique_prod3,generation,30,30,0,77.1923748484218,Free +1,unique_prod3,generation,31,31,0,108.968921914618,Free +1,unique_prod3,generation,32,32,0,110.802971078147,Free +1,unique_prod3,generation,33,33,0,25.0004832536596,Free +1,unique_prod3,generation,34,34,0,31.1574466696731,Free +1,unique_prod3,generation,35,35,0,47.4844041369394,Free +1,unique_prod3,generation,36,36,0,36.7972562353426,Free +1,unique_prod3,generation,37,37,0,0,Free +1,unique_prod3,generation,38,38,0,36,Free +1,unique_prod3,generation,39,39,0,0,Free +1,unique_prod3,generation,40,40,0,36,Free +1,unique_prod3,generation,41,41,0,0,Free +1,unique_prod3,generation,42,42,0,25.7417474644486,Free +1,unique_prod3,generation,43,43,0,0,Free +1,unique_prod3,generation,44,44,0,36,Free +1,unique_prod3,generation,45,45,0,0,Free +1,unique_prod3,generation,46,46,0,109.568356923788,Free +1,unique_prod3,generation,47,47,0,112.147547949227,Free +1,unique_prod3,generation,48,48,0,31.0196153931618,Free +1,unique_prod3,generation,49,49,0,28.3630025500843,Free +1,unique_prod3,generation,50,50,0,66.1029979911987,Free +1,unique_prod3,generation,51,51,0,81.7652092704441,Free +1,unique_prod3,generation,52,52,0,53.7817952606976,Free +1,unique_prod3,generation,53,53,0,89.4631912305743,Free +1,unique_prod3,generation,54,54,0,44.0652624807855,Free +1,unique_prod3,generation,55,55,0,73.8820125334593,Free +1,unique_prod3,generation,56,56,0,78.3349277346308,Free +1,unique_prod3,generation,57,57,0,66.3520696884074,Free +1,unique_prod3,generation,58,58,0,37.2053910920897,Free +1,unique_prod3,generation,59,59,0,44.5001463491964,Free +1,unique_prod3,generation,60,60,0,36.8037016421897,Free +1,unique_prod3,generation,61,61,0,54.9404833438378,Free +1,unique_prod3,generation,62,62,0,46.4531630567644,Free +1,unique_prod3,generation,63,63,0,74.6398229779028,Free +1,unique_prod3,generation,64,64,0,36,Free +1,unique_prod3,generation,65,65,0,0,Free +1,unique_prod3,generation,66,66,0,54.8997719116905,Free +1,unique_prod3,generation,67,67,0,0,Free +1,unique_prod3,generation,68,68,0,107.073175793713,Free +1,unique_prod3,generation,69,69,0,105.339156228021,Free +1,unique_prod3,generation,70,70,0,34.3504479345707,Free +1,unique_prod3,generation,71,71,0,54.3666704615316,Free +1,unique_prod3,generation,72,72,0,63.3099512449237,Free +1,unique_prod3,generation,73,73,0,89.9605453365551,Free +1,unique_prod3,generation,74,74,0,91.4145875974346,Free +1,unique_prod3,generation,75,75,0,66.8350026386077,Free +1,unique_prod3,generation,76,76,0,64.3509194975245,Free +1,unique_prod3,generation,77,77,0,101.052536742714,Free +1,unique_prod3,generation,78,78,0,75.7072862705928,Free +1,unique_prod3,generation,79,79,0,112.656046983748,Free +1,unique_prod3,generation,80,80,0,29.9431235735704,Free +1,unique_prod3,generation,81,81,0,59.2880115205255,Free +1,unique_prod3,generation,82,82,0,117.15828215524,Free +1,unique_prod3,generation,83,83,0,91.1139790426458,Free +1,unique_prod3,generation,84,84,0,109.533655673015,Free +1,unique_prod3,generation,85,85,0,48.3410609659152,Free +1,unique_prod3,generation,86,86,0,36,Free +1,unique_prod3,generation,87,87,0,0,Free +1,unique_prod3,generation,88,88,0,36,Free +1,unique_prod3,generation,89,89,0,46.7465283577889,Free +1,unique_prod3,generation,90,90,0,65.9032473378589,Free +1,unique_prod3,generation,91,91,0,100.610353298105,Free +1,unique_prod3,generation,92,92,0,53.6072798246664,Free +1,unique_prod3,generation,93,93,0,83.6655963362103,Free +1,unique_prod3,generation,94,94,0,66.2062192939545,Free +1,unique_prod3,generation,95,95,0,111.67907572244,Free +1,unique_prod3,generation,96,96,0,87.2338467025124,Free +1,unique_prod3,generation,97,97,0,86.7486190190613,Free +1,unique_prod3,generation,98,98,0,58.4119472044511,Free +1,unique_prod3,generation,99,99,0,74.202390451498,Free +1,unique_prod3,generation,100,100,0,28.3283819831479,Free +1,unique_prod3,generation,101,101,0,33.0537275427542,Free +1,unique_prod3,generation,102,102,0,112.474770533572,Free +1,unique_prod3,generation,103,103,0,31.0122721342572,Free +1,unique_prod3,generation,104,104,0,113.224066035942,Free +1,unique_prod3,generation,105,105,0,92.8334531637361,Free +1,unique_prod3,generation,106,106,0,83.7418123909771,Free +1,unique_prod3,generation,107,107,0,96.4790125551072,Free +1,unique_prod3,generation,108,108,0,37.6003112609882,Free +1,unique_prod3,generation,109,109,0,100.206930978955,Free +1,unique_prod3,generation,110,110,0,93.6387023892842,Free +1,unique_prod3,generation,111,111,0,75.9781709392689,Free +1,unique_prod3,generation,112,112,0,70.3281113436892,Free +1,unique_prod3,generation,113,113,0,69.7418505957587,Free +1,unique_prod3,generation,114,114,0,66.819151722241,Free +1,unique_prod3,generation,115,115,0,102.342725991086,Free +1,unique_prod3,generation,116,116,0,36,Free +1,unique_prod3,generation,117,117,0,0,Free +1,unique_prod3,generation,118,118,0,36,Free +1,unique_prod3,generation,119,119,0,0,Free +1,unique_prod3,generation,120,120,0,36,Free +1,unique_prod3,generation,121,121,0,0,Free +1,unique_prod3,generation,122,122,0,36,Free +1,unique_prod3,generation,123,123,0,0,Free +1,unique_prod3,generation,124,124,0,36,Free +1,unique_prod3,generation,125,125,0,66.6101369349591,Free +1,unique_prod3,generation,126,126,0,77.6399910403183,Free +1,unique_prod3,generation,127,127,0,102.13097804562,Free +1,unique_prod3,generation,128,128,0,54.5115202814303,Free +1,unique_prod3,generation,129,129,0,62.1163289447044,Free +1,unique_prod3,generation,130,130,0,109.456151836167,Free +1,unique_prod3,generation,131,131,0,103.239955948888,Free +1,unique_prod3,generation,132,132,0,34.2419599708293,Free +1,unique_prod3,generation,133,133,0,39.3384840545482,Free +1,unique_prod3,generation,134,134,0,86.5170536292929,Free +1,unique_prod3,generation,135,135,0,78.630164495394,Free +1,unique_prod3,generation,136,136,0,118.47203262961,Free +1,unique_prod3,generation,137,137,0,95.9192652152842,Free +1,unique_prod3,generation,138,138,0,101.030364562023,Free +1,unique_prod3,generation,139,139,0,45.917748480726,Free +1,unique_prod3,generation,140,140,0,73.7964908115012,Free +1,unique_prod3,generation,141,141,0,84.4904066982728,Free +1,unique_prod3,generation,142,142,0,24.2097946987361,Free +1,unique_prod3,generation,143,143,0,73.2686175039337,Free +1,unique_prod3,generation,144,144,0,67.1222562088379,Free +1,unique_prod3,generation,145,145,0,57.5382096376846,Free +1,unique_prod3,generation,146,146,0,36,Free +1,unique_prod3,generation,147,147,0,0,Free +1,unique_prod3,generation,148,148,0,26.9735481599307,Free +1,unique_prod3,generation,149,149,0,0,Free +1,unique_prod3,generation,150,150,0,36,Free +1,unique_prod3,generation,151,151,0,0,Free +1,unique_prod3,generation,152,152,0,25.5871743423072,Free +1,unique_prod3,generation,153,153,0,0,Free +1,unique_prod3,generation,154,154,0,36,Free +1,unique_prod3,generation,155,155,0,90.8604860185884,Free +1,unique_prod3,generation,156,156,0,90.5039771947081,Free +1,unique_prod3,generation,157,157,0,89.5340226483247,Free +1,unique_prod3,generation,158,158,0,109.644523969925,Free +1,unique_prod3,generation,159,159,0,114.775488277451,Free +1,unique_prod3,generation,160,160,0,31.8535224357824,Free +1,unique_prod3,generation,161,161,0,35.2213651729531,Free +1,unique_prod3,generation,162,162,0,27.5394208534916,Free +1,unique_prod3,generation,163,163,0,112.683859789937,Free +1,unique_prod3,generation,164,164,0,118.262476071676,Free +1,unique_prod3,generation,165,165,0,0,Free +1,unique_prod3,generation,166,166,0,30.0936966993773,Free +1,unique_prod3,generation,167,167,0,36.2742204680014,Free +1,unique_prod3,generation,168,168,0,108.469331993674,Free +1,unique_prod3,nb_units_on,1,1,0,2.71173329984184,Free +1,unique_prod3,nb_units_on,2,2,0,1,Free +1,unique_prod3,nb_units_on,3,3,0,2.88067887443766,Free +1,unique_prod3,nb_units_on,4,4,0,2,Free +1,unique_prod3,nb_units_on,5,5,0,2.69393888443688,Free +1,unique_prod3,nb_units_on,6,6,0,1.10959843421453,Free +1,unique_prod3,nb_units_on,7,7,0,0.697703099445814,Free +1,unique_prod3,nb_units_on,8,8,0,1.55326670722969,Free +1,unique_prod3,nb_units_on,9,9,0,1.55326670722969,Free +1,unique_prod3,nb_units_on,10,10,0,2.62017773820735,Free +1,unique_prod3,nb_units_on,11,11,0,2,Free +1,unique_prod3,nb_units_on,12,12,0,2.38192688059778,Free +1,unique_prod3,nb_units_on,13,13,0,1.54116990674122,Free +1,unique_prod3,nb_units_on,14,14,0,1.54116990674122,Free +1,unique_prod3,nb_units_on,15,15,0,2.38449459502114,Free +1,unique_prod3,nb_units_on,16,16,0,1,Free +1,unique_prod3,nb_units_on,17,17,0,2.8863465581317,Free +1,unique_prod3,nb_units_on,18,18,0,2.8863465581317,Free +1,unique_prod3,nb_units_on,19,19,0,2.8863465581317,Free +1,unique_prod3,nb_units_on,20,20,0,1.59703428730778,Free +1,unique_prod3,nb_units_on,21,21,0,0.669555982528491,Free +1,unique_prod3,nb_units_on,22,22,0,2.95686596303567,Free +1,unique_prod3,nb_units_on,23,23,0,1.41513044072469,Free +1,unique_prod3,nb_units_on,24,24,0,2.29604926533313,Free +1,unique_prod3,nb_units_on,25,25,0,1.46850496225815,Free +1,unique_prod3,nb_units_on,26,26,0,1,Free +1,unique_prod3,nb_units_on,27,27,0,2.65213980777698,Free +1,unique_prod3,nb_units_on,28,28,0,2.43763918594857,Free +1,unique_prod3,nb_units_on,29,29,0,2.43763918594857,Free +1,unique_prod3,nb_units_on,30,30,0,1.92980937121055,Free +1,unique_prod3,nb_units_on,31,31,0,2.77007427695367,Free +1,unique_prod3,nb_units_on,32,32,0,2.77007427695367,Free +1,unique_prod3,nb_units_on,33,33,0,0.62501208134149,Free +1,unique_prod3,nb_units_on,34,34,0,1,Free +1,unique_prod3,nb_units_on,35,35,0,1.18711010342348,Free +1,unique_prod3,nb_units_on,36,36,0,1,Free +1,unique_prod3,nb_units_on,37,37,0,1,Free +1,unique_prod3,nb_units_on,38,38,0,1,Free +1,unique_prod3,nb_units_on,39,39,0,1,Free +1,unique_prod3,nb_units_on,40,40,0,1,Free +1,unique_prod3,nb_units_on,41,41,0,1,Free +1,unique_prod3,nb_units_on,42,42,0,1,Free +1,unique_prod3,nb_units_on,43,43,0,1,Free +1,unique_prod3,nb_units_on,44,44,0,1,Free +1,unique_prod3,nb_units_on,45,45,0,0,Free +1,unique_prod3,nb_units_on,46,46,0,2.80368869873068,Free +1,unique_prod3,nb_units_on,47,47,0,2.80368869873068,Free +1,unique_prod3,nb_units_on,48,48,0,1,Free +1,unique_prod3,nb_units_on,49,49,0,1,Free +1,unique_prod3,nb_units_on,50,50,0,1.65257494977997,Free +1,unique_prod3,nb_units_on,51,51,0,2.0441302317611,Free +1,unique_prod3,nb_units_on,52,52,0,2,Free +1,unique_prod3,nb_units_on,53,53,0,2.23657978076436,Free +1,unique_prod3,nb_units_on,54,54,0,1.10163156201964,Free +1,unique_prod3,nb_units_on,55,55,0,1.95837319336577,Free +1,unique_prod3,nb_units_on,56,56,0,1.95837319336577,Free +1,unique_prod3,nb_units_on,57,57,0,1.95837319336577,Free +1,unique_prod3,nb_units_on,58,58,0,1,Free +1,unique_prod3,nb_units_on,59,59,0,2,Free +1,unique_prod3,nb_units_on,60,60,0,1,Free +1,unique_prod3,nb_units_on,61,61,0,1.37351208359595,Free +1,unique_prod3,nb_units_on,62,62,0,1.37351208359595,Free +1,unique_prod3,nb_units_on,63,63,0,1.86599557444757,Free +1,unique_prod3,nb_units_on,64,64,0,1,Free +1,unique_prod3,nb_units_on,65,65,0,1,Free +1,unique_prod3,nb_units_on,66,66,0,1.37249429779226,Free +1,unique_prod3,nb_units_on,67,67,0,1.37249429779226,Free +1,unique_prod3,nb_units_on,68,68,0,2.67682939484282,Free +1,unique_prod3,nb_units_on,69,69,0,2.67682939484282,Free +1,unique_prod3,nb_units_on,70,70,0,1,Free +1,unique_prod3,nb_units_on,71,71,0,1.58274878112309,Free +1,unique_prod3,nb_units_on,72,72,0,1.58274878112309,Free +1,unique_prod3,nb_units_on,73,73,0,2.28536468993586,Free +1,unique_prod3,nb_units_on,74,74,0,2.28536468993586,Free +1,unique_prod3,nb_units_on,75,75,0,1.67087506596519,Free +1,unique_prod3,nb_units_on,76,76,0,1.67087506596519,Free +1,unique_prod3,nb_units_on,77,77,0,2.52631341856785,Free +1,unique_prod3,nb_units_on,78,78,0,1.89268215676482,Free +1,unique_prod3,nb_units_on,79,79,0,2.81640117459371,Free +1,unique_prod3,nb_units_on,80,80,0,1,Free +1,unique_prod3,nb_units_on,81,81,0,1.48220028801314,Free +1,unique_prod3,nb_units_on,82,82,0,2.928957053881,Free +1,unique_prod3,nb_units_on,83,83,0,2.928957053881,Free +1,unique_prod3,nb_units_on,84,84,0,2.928957053881,Free +1,unique_prod3,nb_units_on,85,85,0,1.20852652414788,Free +1,unique_prod3,nb_units_on,86,86,0,1.20852652414788,Free +1,unique_prod3,nb_units_on,87,87,0,0,Free +1,unique_prod3,nb_units_on,88,88,0,1,Free +1,unique_prod3,nb_units_on,89,89,0,1.64758118344647,Free +1,unique_prod3,nb_units_on,90,90,0,1.64758118344647,Free +1,unique_prod3,nb_units_on,91,91,0,2.51525883245262,Free +1,unique_prod3,nb_units_on,92,92,0,2,Free +1,unique_prod3,nb_units_on,93,93,0,2.09163990840526,Free +1,unique_prod3,nb_units_on,94,94,0,1.65515548234886,Free +1,unique_prod3,nb_units_on,95,95,0,2.79197689306101,Free +1,unique_prod3,nb_units_on,96,96,0,2.18084616756281,Free +1,unique_prod3,nb_units_on,97,97,0,2.18084616756281,Free +1,unique_prod3,nb_units_on,98,98,0,1.85505976128745,Free +1,unique_prod3,nb_units_on,99,99,0,1.85505976128745,Free +1,unique_prod3,nb_units_on,100,100,0,1,Free +1,unique_prod3,nb_units_on,101,101,0,1,Free +1,unique_prod3,nb_units_on,102,102,0,2.81186926333929,Free +1,unique_prod3,nb_units_on,103,103,0,0.77530680335643,Free +1,unique_prod3,nb_units_on,104,104,0,2.83060165089854,Free +1,unique_prod3,nb_units_on,105,105,0,2.41197531387768,Free +1,unique_prod3,nb_units_on,106,106,0,2.41197531387768,Free +1,unique_prod3,nb_units_on,107,107,0,2.41197531387768,Free +1,unique_prod3,nb_units_on,108,108,0,1,Free +1,unique_prod3,nb_units_on,109,109,0,2.50517327447388,Free +1,unique_prod3,nb_units_on,110,110,0,2.50517327447388,Free +1,unique_prod3,nb_units_on,111,111,0,1.89945427348172,Free +1,unique_prod3,nb_units_on,112,112,0,1.89945427348172,Free +1,unique_prod3,nb_units_on,113,113,0,1.89945427348172,Free +1,unique_prod3,nb_units_on,114,114,0,1.89945427348172,Free +1,unique_prod3,nb_units_on,115,115,0,2.55856814977714,Free +1,unique_prod3,nb_units_on,116,116,0,1,Free +1,unique_prod3,nb_units_on,117,117,0,0,Free +1,unique_prod3,nb_units_on,118,118,0,1,Free +1,unique_prod3,nb_units_on,119,119,0,0,Free +1,unique_prod3,nb_units_on,120,120,0,1,Free +1,unique_prod3,nb_units_on,121,121,0,1,Free +1,unique_prod3,nb_units_on,122,122,0,1,Free +1,unique_prod3,nb_units_on,123,123,0,0,Free +1,unique_prod3,nb_units_on,124,124,0,1.66525342337398,Free +1,unique_prod3,nb_units_on,125,125,0,1.66525342337398,Free +1,unique_prod3,nb_units_on,126,126,0,1.94099977600796,Free +1,unique_prod3,nb_units_on,127,127,0,2.55327445114049,Free +1,unique_prod3,nb_units_on,128,128,0,1.36278800703576,Free +1,unique_prod3,nb_units_on,129,129,0,2,Free +1,unique_prod3,nb_units_on,130,130,0,2.73640379590417,Free +1,unique_prod3,nb_units_on,131,131,0,2.73640379590417,Free +1,unique_prod3,nb_units_on,132,132,0,1,Free +1,unique_prod3,nb_units_on,133,133,0,1,Free +1,unique_prod3,nb_units_on,134,134,0,2.16292634073232,Free +1,unique_prod3,nb_units_on,135,135,0,2,Free +1,unique_prod3,nb_units_on,136,136,0,2.96180081574026,Free +1,unique_prod3,nb_units_on,137,137,0,2.96180081574026,Free +1,unique_prod3,nb_units_on,138,138,0,2.96180081574026,Free +1,unique_prod3,nb_units_on,139,139,0,1.84491227028753,Free +1,unique_prod3,nb_units_on,140,140,0,1.84491227028753,Free +1,unique_prod3,nb_units_on,141,141,0,2.11226016745682,Free +1,unique_prod3,nb_units_on,142,142,0,1,Free +1,unique_prod3,nb_units_on,143,143,0,1.83171543759834,Free +1,unique_prod3,nb_units_on,144,144,0,1.67805640522095,Free +1,unique_prod3,nb_units_on,145,145,0,1.67805640522095,Free +1,unique_prod3,nb_units_on,146,146,0,1,Free +1,unique_prod3,nb_units_on,147,147,0,1,Free +1,unique_prod3,nb_units_on,148,148,0,1,Free +1,unique_prod3,nb_units_on,149,149,0,1,Free +1,unique_prod3,nb_units_on,150,150,0,1,Free +1,unique_prod3,nb_units_on,151,151,0,1,Free +1,unique_prod3,nb_units_on,152,152,0,1,Free +1,unique_prod3,nb_units_on,153,153,0,1,Free +1,unique_prod3,nb_units_on,154,154,0,2,Free +1,unique_prod3,nb_units_on,155,155,0,2.27151215046471,Free +1,unique_prod3,nb_units_on,156,156,0,2.27151215046471,Free +1,unique_prod3,nb_units_on,157,157,0,2.74111309924814,Free +1,unique_prod3,nb_units_on,158,158,0,2.74111309924814,Free +1,unique_prod3,nb_units_on,159,159,0,2.86938720693627,Free +1,unique_prod3,nb_units_on,160,160,0,1,Free +1,unique_prod3,nb_units_on,161,161,0,1,Free +1,unique_prod3,nb_units_on,162,162,0,1,Free +1,unique_prod3,nb_units_on,163,163,0,2.81709649474843,Free +1,unique_prod3,nb_units_on,164,164,0,2.95656190179189,Free +1,unique_prod3,nb_units_on,165,165,0,0,Free +1,unique_prod3,nb_units_on,166,166,0,1,Free +1,unique_prod3,nb_units_on,167,167,0,1,Free +1,unique_prod3,nb_units_on,168,168,0,2.71173329984184,Free +1,unique_prod3,nb_starting,1,1,0,0,Free +1,unique_prod3,nb_starting,2,2,0,0,Free +1,unique_prod3,nb_starting,3,3,0,1.88067887443766,Free +1,unique_prod3,nb_starting,4,4,0,0,Free +1,unique_prod3,nb_starting,5,5,0,0.693938884436885,Free +1,unique_prod3,nb_starting,6,6,0,0,Free +1,unique_prod3,nb_starting,7,7,0,0,Free +1,unique_prod3,nb_starting,8,8,0,0.85556360778388,Free +1,unique_prod3,nb_starting,9,9,0,0,Free +1,unique_prod3,nb_starting,10,10,0,1.06691103097765,Free +1,unique_prod3,nb_starting,11,11,0,0,Free +1,unique_prod3,nb_starting,12,12,0,0.381926880597781,Free +1,unique_prod3,nb_starting,13,13,0,0,Free +1,unique_prod3,nb_starting,14,14,0,0,Free +1,unique_prod3,nb_starting,15,15,0,0.843324688279917,Free +1,unique_prod3,nb_starting,16,16,0,0,Free +1,unique_prod3,nb_starting,17,17,0,1.8863465581317,Free +1,unique_prod3,nb_starting,18,18,0,0,Free +1,unique_prod3,nb_starting,19,19,0,0,Free +1,unique_prod3,nb_starting,20,20,0,0,Free +1,unique_prod3,nb_starting,21,21,0,0,Free +1,unique_prod3,nb_starting,22,22,0,2.28730998050718,Free +1,unique_prod3,nb_starting,23,23,0,0,Free +1,unique_prod3,nb_starting,24,24,0,0.880918824608437,Free +1,unique_prod3,nb_starting,25,25,0,0,Free +1,unique_prod3,nb_starting,26,26,0,0,Free +1,unique_prod3,nb_starting,27,27,0,1.65213980777698,Free +1,unique_prod3,nb_starting,28,28,0,0,Free +1,unique_prod3,nb_starting,29,29,0,0,Free +1,unique_prod3,nb_starting,30,30,0,0,Free +1,unique_prod3,nb_starting,31,31,0,0.840264905743124,Free +1,unique_prod3,nb_starting,32,32,0,0,Free +1,unique_prod3,nb_starting,33,33,0,0,Free +1,unique_prod3,nb_starting,34,34,0,0.37498791865851,Free +1,unique_prod3,nb_starting,35,35,0,0.187110103423485,Free +1,unique_prod3,nb_starting,36,36,0,0,Free +1,unique_prod3,nb_starting,37,37,0,0,Free +1,unique_prod3,nb_starting,38,38,0,0,Free +1,unique_prod3,nb_starting,39,39,0,0,Free +1,unique_prod3,nb_starting,40,40,0,0,Free +1,unique_prod3,nb_starting,41,41,0,0,Free +1,unique_prod3,nb_starting,42,42,0,0,Free +1,unique_prod3,nb_starting,43,43,0,0,Free +1,unique_prod3,nb_starting,44,44,0,0,Free +1,unique_prod3,nb_starting,45,45,0,0,Free +1,unique_prod3,nb_starting,46,46,0,2.80368869873068,Free +1,unique_prod3,nb_starting,47,47,0,0,Free +1,unique_prod3,nb_starting,48,48,0,0,Free +1,unique_prod3,nb_starting,49,49,0,0,Free +1,unique_prod3,nb_starting,50,50,0,0.652574949779968,Free +1,unique_prod3,nb_starting,51,51,0,0.391555281981135,Free +1,unique_prod3,nb_starting,52,52,0,0,Free +1,unique_prod3,nb_starting,53,53,0,0.236579780764358,Free +1,unique_prod3,nb_starting,54,54,0,0,Free +1,unique_prod3,nb_starting,55,55,0,0.856741631346133,Free +1,unique_prod3,nb_starting,56,56,0,0,Free +1,unique_prod3,nb_starting,57,57,0,0,Free +1,unique_prod3,nb_starting,58,58,0,0,Free +1,unique_prod3,nb_starting,59,59,0,1,Free +1,unique_prod3,nb_starting,60,60,0,0,Free +1,unique_prod3,nb_starting,61,61,0,0.373512083595946,Free +1,unique_prod3,nb_starting,62,62,0,0,Free +1,unique_prod3,nb_starting,63,63,0,0.492483490851624,Free +1,unique_prod3,nb_starting,64,64,0,0,Free +1,unique_prod3,nb_starting,65,65,0,0,Free +1,unique_prod3,nb_starting,66,66,0,0.372494297792262,Free +1,unique_prod3,nb_starting,67,67,0,0,Free +1,unique_prod3,nb_starting,68,68,0,1.30433509705055,Free +1,unique_prod3,nb_starting,69,69,0,0,Free +1,unique_prod3,nb_starting,70,70,0,0,Free +1,unique_prod3,nb_starting,71,71,0,0.582748781123092,Free +1,unique_prod3,nb_starting,72,72,0,0,Free +1,unique_prod3,nb_starting,73,73,0,0.702615908812772,Free +1,unique_prod3,nb_starting,74,74,0,0,Free +1,unique_prod3,nb_starting,75,75,0,0,Free +1,unique_prod3,nb_starting,76,76,0,0,Free +1,unique_prod3,nb_starting,77,77,0,0.855438352602659,Free +1,unique_prod3,nb_starting,78,78,0,0,Free +1,unique_prod3,nb_starting,79,79,0,0.92371901782889,Free +1,unique_prod3,nb_starting,80,80,0,0,Free +1,unique_prod3,nb_starting,81,81,0,0.482200288013137,Free +1,unique_prod3,nb_starting,82,82,0,1.44675676586787,Free +1,unique_prod3,nb_starting,83,83,0,0,Free +1,unique_prod3,nb_starting,84,84,0,0,Free +1,unique_prod3,nb_starting,85,85,0,0,Free +1,unique_prod3,nb_starting,86,86,0,0,Free +1,unique_prod3,nb_starting,87,87,0,0,Free +1,unique_prod3,nb_starting,88,88,0,1,Free +1,unique_prod3,nb_starting,89,89,0,0.647581183446473,Free +1,unique_prod3,nb_starting,90,90,0,0,Free +1,unique_prod3,nb_starting,91,91,0,0.867677649006147,Free +1,unique_prod3,nb_starting,92,92,0,0,Free +1,unique_prod3,nb_starting,93,93,0,0.091639908405257,Free +1,unique_prod3,nb_starting,94,94,0,0,Free +1,unique_prod3,nb_starting,95,95,0,1.13682141071214,Free +1,unique_prod3,nb_starting,96,96,0,0,Free +1,unique_prod3,nb_starting,97,97,0,0,Free +1,unique_prod3,nb_starting,98,98,0,0,Free +1,unique_prod3,nb_starting,99,99,0,0,Free +1,unique_prod3,nb_starting,100,100,0,0,Free +1,unique_prod3,nb_starting,101,101,0,0,Free +1,unique_prod3,nb_starting,102,102,0,1.81186926333929,Free +1,unique_prod3,nb_starting,103,103,0,0,Free +1,unique_prod3,nb_starting,104,104,0,2.05529484754211,Free +1,unique_prod3,nb_starting,105,105,0,0,Free +1,unique_prod3,nb_starting,106,106,0,0,Free +1,unique_prod3,nb_starting,107,107,0,0,Free +1,unique_prod3,nb_starting,108,108,0,0,Free +1,unique_prod3,nb_starting,109,109,0,1.50517327447388,Free +1,unique_prod3,nb_starting,110,110,0,0,Free +1,unique_prod3,nb_starting,111,111,0,0,Free +1,unique_prod3,nb_starting,112,112,0,0,Free +1,unique_prod3,nb_starting,113,113,0,0,Free +1,unique_prod3,nb_starting,114,114,0,0,Free +1,unique_prod3,nb_starting,115,115,0,0.65911387629542,Free +1,unique_prod3,nb_starting,116,116,0,0,Free +1,unique_prod3,nb_starting,117,117,0,0,Free +1,unique_prod3,nb_starting,118,118,0,1,Free +1,unique_prod3,nb_starting,119,119,0,0,Free +1,unique_prod3,nb_starting,120,120,0,1,Free +1,unique_prod3,nb_starting,121,121,0,0,Free +1,unique_prod3,nb_starting,122,122,0,0,Free +1,unique_prod3,nb_starting,123,123,0,0,Free +1,unique_prod3,nb_starting,124,124,0,1.66525342337398,Free +1,unique_prod3,nb_starting,125,125,0,0,Free +1,unique_prod3,nb_starting,126,126,0,0.275746352633979,Free +1,unique_prod3,nb_starting,127,127,0,0.612274675132531,Free +1,unique_prod3,nb_starting,128,128,0,0,Free +1,unique_prod3,nb_starting,129,129,0,0.637211992964243,Free +1,unique_prod3,nb_starting,130,130,0,0.736403795904169,Free +1,unique_prod3,nb_starting,131,131,0,0,Free +1,unique_prod3,nb_starting,132,132,0,0,Free +1,unique_prod3,nb_starting,133,133,0,0,Free +1,unique_prod3,nb_starting,134,134,0,1.16292634073232,Free +1,unique_prod3,nb_starting,135,135,0,0,Free +1,unique_prod3,nb_starting,136,136,0,0.96180081574026,Free +1,unique_prod3,nb_starting,137,137,0,0,Free +1,unique_prod3,nb_starting,138,138,0,0,Free +1,unique_prod3,nb_starting,139,139,0,0,Free +1,unique_prod3,nb_starting,140,140,0,0,Free +1,unique_prod3,nb_starting,141,141,0,0.26734789716929,Free +1,unique_prod3,nb_starting,142,142,0,0,Free +1,unique_prod3,nb_starting,143,143,0,0.831715437598342,Free +1,unique_prod3,nb_starting,144,144,0,0,Free +1,unique_prod3,nb_starting,145,145,0,0,Free +1,unique_prod3,nb_starting,146,146,0,0,Free +1,unique_prod3,nb_starting,147,147,0,0,Free +1,unique_prod3,nb_starting,148,148,0,0,Free +1,unique_prod3,nb_starting,149,149,0,0,Free +1,unique_prod3,nb_starting,150,150,0,0,Free +1,unique_prod3,nb_starting,151,151,0,0,Free +1,unique_prod3,nb_starting,152,152,0,0,Free +1,unique_prod3,nb_starting,153,153,0,0,Free +1,unique_prod3,nb_starting,154,154,0,1,Free +1,unique_prod3,nb_starting,155,155,0,0.27151215046471,Free +1,unique_prod3,nb_starting,156,156,0,0,Free +1,unique_prod3,nb_starting,157,157,0,0.469600948783428,Free +1,unique_prod3,nb_starting,158,158,0,0,Free +1,unique_prod3,nb_starting,159,159,0,0.128274107688132,Free +1,unique_prod3,nb_starting,160,160,0,0,Free +1,unique_prod3,nb_starting,161,161,0,0,Free +1,unique_prod3,nb_starting,162,162,0,-0,Free +1,unique_prod3,nb_starting,163,163,0,1.81709649474844,Free +1,unique_prod3,nb_starting,164,164,0,0.139465407043454,Free +1,unique_prod3,nb_starting,165,165,0,0,Free +1,unique_prod3,nb_starting,166,166,0,1,Free +1,unique_prod3,nb_starting,167,167,0,0,Free +1,unique_prod3,nb_starting,168,168,0,1.71173329984184,Free +1,unique_prod3,nb_stopping,1,1,0,0,Free +1,unique_prod3,nb_stopping,2,2,0,1.71173329984184,Free +1,unique_prod3,nb_stopping,3,3,0,0,Free +1,unique_prod3,nb_stopping,4,4,0,0.880678874437659,Free +1,unique_prod3,nb_stopping,5,5,0,0,Free +1,unique_prod3,nb_stopping,6,6,0,1.58434045022236,Free +1,unique_prod3,nb_stopping,7,7,0,0.411895334768714,Free +1,unique_prod3,nb_stopping,8,8,0,0,Free +1,unique_prod3,nb_stopping,9,9,0,0,Free +1,unique_prod3,nb_stopping,10,10,0,0,Free +1,unique_prod3,nb_stopping,11,11,0,0.620177738207348,Free +1,unique_prod3,nb_stopping,12,12,0,0,Free +1,unique_prod3,nb_stopping,13,13,0,0.840756973856562,Free +1,unique_prod3,nb_stopping,14,14,0,0,Free +1,unique_prod3,nb_stopping,15,15,0,0,Free +1,unique_prod3,nb_stopping,16,16,0,1.38449459502114,Free +1,unique_prod3,nb_stopping,17,17,0,0,Free +1,unique_prod3,nb_stopping,18,18,0,0,Free +1,unique_prod3,nb_stopping,19,19,0,0,Free +1,unique_prod3,nb_stopping,20,20,0,1.28931227082392,Free +1,unique_prod3,nb_stopping,21,21,0,0.927478304779288,Free +1,unique_prod3,nb_stopping,22,22,0,0,Free +1,unique_prod3,nb_stopping,23,23,0,1.54173552231098,Free +1,unique_prod3,nb_stopping,24,24,0,0,Free +1,unique_prod3,nb_stopping,25,25,0,0.827544303074981,Free +1,unique_prod3,nb_stopping,26,26,0,0.468504962258149,Free +1,unique_prod3,nb_stopping,27,27,0,0,Free +1,unique_prod3,nb_stopping,28,28,0,0.214500621828406,Free +1,unique_prod3,nb_stopping,29,29,0,0,Free +1,unique_prod3,nb_stopping,30,30,0,0.507829814738026,Free +1,unique_prod3,nb_stopping,31,31,0,0,Free +1,unique_prod3,nb_stopping,32,32,0,0,Free +1,unique_prod3,nb_stopping,33,33,0,2.14506219561218,Free +1,unique_prod3,nb_stopping,34,34,0,0,Free +1,unique_prod3,nb_stopping,35,35,0,0,Free +1,unique_prod3,nb_stopping,36,36,0,0.187110103423485,Free +1,unique_prod3,nb_stopping,37,37,0,0,Free +1,unique_prod3,nb_stopping,38,38,0,-0,Free +1,unique_prod3,nb_stopping,39,39,0,0,Free +1,unique_prod3,nb_stopping,40,40,0,-0,Free +1,unique_prod3,nb_stopping,41,41,0,-0,Free +1,unique_prod3,nb_stopping,42,42,0,0,Free +1,unique_prod3,nb_stopping,43,43,0,0,Free +1,unique_prod3,nb_stopping,44,44,0,-0,Free +1,unique_prod3,nb_stopping,45,45,0,1,Free +1,unique_prod3,nb_stopping,46,46,0,0,Free +1,unique_prod3,nb_stopping,47,47,0,0,Free +1,unique_prod3,nb_stopping,48,48,0,1.80368869873068,Free +1,unique_prod3,nb_stopping,49,49,0,-0,Free +1,unique_prod3,nb_stopping,50,50,0,0,Free +1,unique_prod3,nb_stopping,51,51,0,0,Free +1,unique_prod3,nb_stopping,52,52,0,0.0441302317611028,Free +1,unique_prod3,nb_stopping,53,53,0,0,Free +1,unique_prod3,nb_stopping,54,54,0,1.13494821874472,Free +1,unique_prod3,nb_stopping,55,55,0,0,Free +1,unique_prod3,nb_stopping,56,56,0,0,Free +1,unique_prod3,nb_stopping,57,57,0,0,Free +1,unique_prod3,nb_stopping,58,58,0,0.958373193365769,Free +1,unique_prod3,nb_stopping,59,59,0,0,Free +1,unique_prod3,nb_stopping,60,60,0,1,Free +1,unique_prod3,nb_stopping,61,61,0,0,Free +1,unique_prod3,nb_stopping,62,62,0,0,Free +1,unique_prod3,nb_stopping,63,63,0,0,Free +1,unique_prod3,nb_stopping,64,64,0,0.86599557444757,Free +1,unique_prod3,nb_stopping,65,65,0,0,Free +1,unique_prod3,nb_stopping,66,66,0,0,Free +1,unique_prod3,nb_stopping,67,67,0,0,Free +1,unique_prod3,nb_stopping,68,68,0,0,Free +1,unique_prod3,nb_stopping,69,69,0,0,Free +1,unique_prod3,nb_stopping,70,70,0,1.67682939484282,Free +1,unique_prod3,nb_stopping,71,71,0,0,Free +1,unique_prod3,nb_stopping,72,72,0,0,Free +1,unique_prod3,nb_stopping,73,73,0,0,Free +1,unique_prod3,nb_stopping,74,74,0,0,Free +1,unique_prod3,nb_stopping,75,75,0,0.614489623970671,Free +1,unique_prod3,nb_stopping,76,76,0,0,Free +1,unique_prod3,nb_stopping,77,77,0,0,Free +1,unique_prod3,nb_stopping,78,78,0,0.633631261803032,Free +1,unique_prod3,nb_stopping,79,79,0,0,Free +1,unique_prod3,nb_stopping,80,80,0,1.81640117459371,Free +1,unique_prod3,nb_stopping,81,81,0,0,Free +1,unique_prod3,nb_stopping,82,82,0,0,Free +1,unique_prod3,nb_stopping,83,83,0,0,Free +1,unique_prod3,nb_stopping,84,84,0,0,Free +1,unique_prod3,nb_stopping,85,85,0,1.72043052973312,Free +1,unique_prod3,nb_stopping,86,86,0,0,Free +1,unique_prod3,nb_stopping,87,87,0,1.20852652414788,Free +1,unique_prod3,nb_stopping,88,88,0,0,Free +1,unique_prod3,nb_stopping,89,89,0,0,Free +1,unique_prod3,nb_stopping,90,90,0,0,Free +1,unique_prod3,nb_stopping,91,91,0,0,Free +1,unique_prod3,nb_stopping,92,92,0,0.51525883245262,Free +1,unique_prod3,nb_stopping,93,93,0,0,Free +1,unique_prod3,nb_stopping,94,94,0,0.436484426056394,Free +1,unique_prod3,nb_stopping,95,95,0,0,Free +1,unique_prod3,nb_stopping,96,96,0,0.611130725498198,Free +1,unique_prod3,nb_stopping,97,97,0,0,Free +1,unique_prod3,nb_stopping,98,98,0,0.325786406275359,Free +1,unique_prod3,nb_stopping,99,99,0,0,Free +1,unique_prod3,nb_stopping,100,100,0,0.855059761287451,Free +1,unique_prod3,nb_stopping,101,101,0,-0,Free +1,unique_prod3,nb_stopping,102,102,0,0,Free +1,unique_prod3,nb_stopping,103,103,0,2.03656245998286,Free +1,unique_prod3,nb_stopping,104,104,0,0,Free +1,unique_prod3,nb_stopping,105,105,0,0.418626337020862,Free +1,unique_prod3,nb_stopping,106,106,0,0,Free +1,unique_prod3,nb_stopping,107,107,0,0,Free +1,unique_prod3,nb_stopping,108,108,0,1.41197531387768,Free +1,unique_prod3,nb_stopping,109,109,0,0,Free +1,unique_prod3,nb_stopping,110,110,0,0,Free +1,unique_prod3,nb_stopping,111,111,0,0.605719000992157,Free +1,unique_prod3,nb_stopping,112,112,0,0,Free +1,unique_prod3,nb_stopping,113,113,0,0,Free +1,unique_prod3,nb_stopping,114,114,0,0,Free +1,unique_prod3,nb_stopping,115,115,0,0,Free +1,unique_prod3,nb_stopping,116,116,0,1.55856814977714,Free +1,unique_prod3,nb_stopping,117,117,0,1,Free +1,unique_prod3,nb_stopping,118,118,0,0,Free +1,unique_prod3,nb_stopping,119,119,0,1,Free +1,unique_prod3,nb_stopping,120,120,0,0,Free +1,unique_prod3,nb_stopping,121,121,0,0,Free +1,unique_prod3,nb_stopping,122,122,0,-0,Free +1,unique_prod3,nb_stopping,123,123,0,1,Free +1,unique_prod3,nb_stopping,124,124,0,0,Free +1,unique_prod3,nb_stopping,125,125,0,0,Free +1,unique_prod3,nb_stopping,126,126,0,0,Free +1,unique_prod3,nb_stopping,127,127,0,0,Free +1,unique_prod3,nb_stopping,128,128,0,1.19048644410473,Free +1,unique_prod3,nb_stopping,129,129,0,0,Free +1,unique_prod3,nb_stopping,130,130,0,0,Free +1,unique_prod3,nb_stopping,131,131,0,0,Free +1,unique_prod3,nb_stopping,132,132,0,1.73640379590417,Free +1,unique_prod3,nb_stopping,133,133,0,-0,Free +1,unique_prod3,nb_stopping,134,134,0,0,Free +1,unique_prod3,nb_stopping,135,135,0,0.162926340732324,Free +1,unique_prod3,nb_stopping,136,136,0,0,Free +1,unique_prod3,nb_stopping,137,137,0,0,Free +1,unique_prod3,nb_stopping,138,138,0,0,Free +1,unique_prod3,nb_stopping,139,139,0,1.11688854545273,Free +1,unique_prod3,nb_stopping,140,140,0,0,Free +1,unique_prod3,nb_stopping,141,141,0,0,Free +1,unique_prod3,nb_stopping,142,142,0,1.11226016745682,Free +1,unique_prod3,nb_stopping,143,143,0,0,Free +1,unique_prod3,nb_stopping,144,144,0,0.153659032377395,Free +1,unique_prod3,nb_stopping,145,145,0,0,Free +1,unique_prod3,nb_stopping,146,146,0,0.678056405220946,Free +1,unique_prod3,nb_stopping,147,147,0,0,Free +1,unique_prod3,nb_stopping,148,148,0,0,Free +1,unique_prod3,nb_stopping,149,149,0,-0,Free +1,unique_prod3,nb_stopping,150,150,0,0,Free +1,unique_prod3,nb_stopping,151,151,0,-0,Free +1,unique_prod3,nb_stopping,152,152,0,0,Free +1,unique_prod3,nb_stopping,153,153,0,0,Free +1,unique_prod3,nb_stopping,154,154,0,0,Free +1,unique_prod3,nb_stopping,155,155,0,0,Free +1,unique_prod3,nb_stopping,156,156,0,0,Free +1,unique_prod3,nb_stopping,157,157,0,0,Free +1,unique_prod3,nb_stopping,158,158,0,0,Free +1,unique_prod3,nb_stopping,159,159,0,0,Free +1,unique_prod3,nb_stopping,160,160,0,1.86938720693627,Free +1,unique_prod3,nb_stopping,161,161,0,-0,Free +1,unique_prod3,nb_stopping,162,162,0,0,Free +1,unique_prod3,nb_stopping,163,163,0,0,Free +1,unique_prod3,nb_stopping,164,164,0,0,Free +1,unique_prod3,nb_stopping,165,165,0,2.95656190179189,Free +1,unique_prod3,nb_stopping,166,166,0,0,Free +1,unique_prod3,nb_stopping,167,167,0,-0,Free +1,unique_prod3,nb_stopping,168,168,0,0,Free +1,unique_prod3,nb_failing,1,1,0,0,Free +1,unique_prod3,nb_failing,2,2,0,0,Free +1,unique_prod3,nb_failing,3,3,0,0,Free +1,unique_prod3,nb_failing,4,4,0,0,Free +1,unique_prod3,nb_failing,5,5,0,0,Free +1,unique_prod3,nb_failing,6,6,0,0,Free +1,unique_prod3,nb_failing,7,7,0,0,Free +1,unique_prod3,nb_failing,8,8,0,0,Free +1,unique_prod3,nb_failing,9,9,0,0,Free +1,unique_prod3,nb_failing,10,10,0,0,Free +1,unique_prod3,nb_failing,11,11,0,0,Free +1,unique_prod3,nb_failing,12,12,0,0,Free +1,unique_prod3,nb_failing,13,13,0,0,Free +1,unique_prod3,nb_failing,14,14,0,0,Free +1,unique_prod3,nb_failing,15,15,0,0,Free +1,unique_prod3,nb_failing,16,16,0,0,Free +1,unique_prod3,nb_failing,17,17,0,0,Free +1,unique_prod3,nb_failing,18,18,0,0,Free +1,unique_prod3,nb_failing,19,19,0,0,Free +1,unique_prod3,nb_failing,20,20,0,0,Free +1,unique_prod3,nb_failing,21,21,0,0,Free +1,unique_prod3,nb_failing,22,22,0,0,Free +1,unique_prod3,nb_failing,23,23,0,0,Free +1,unique_prod3,nb_failing,24,24,0,0,Free +1,unique_prod3,nb_failing,25,25,0,0,Free +1,unique_prod3,nb_failing,26,26,0,0,Free +1,unique_prod3,nb_failing,27,27,0,0,Free +1,unique_prod3,nb_failing,28,28,0,0,Free +1,unique_prod3,nb_failing,29,29,0,0,Free +1,unique_prod3,nb_failing,30,30,0,0,Free +1,unique_prod3,nb_failing,31,31,0,0,Free +1,unique_prod3,nb_failing,32,32,0,0,Free +1,unique_prod3,nb_failing,33,33,0,0,Free +1,unique_prod3,nb_failing,34,34,0,0,Free +1,unique_prod3,nb_failing,35,35,0,0,Free +1,unique_prod3,nb_failing,36,36,0,0,Free +1,unique_prod3,nb_failing,37,37,0,0,Free +1,unique_prod3,nb_failing,38,38,0,0,Free +1,unique_prod3,nb_failing,39,39,0,0,Free +1,unique_prod3,nb_failing,40,40,0,0,Free +1,unique_prod3,nb_failing,41,41,0,0,Free +1,unique_prod3,nb_failing,42,42,0,0,Free +1,unique_prod3,nb_failing,43,43,0,0,Free +1,unique_prod3,nb_failing,44,44,0,0,Free +1,unique_prod3,nb_failing,45,45,0,0,Free +1,unique_prod3,nb_failing,46,46,0,0,Free +1,unique_prod3,nb_failing,47,47,0,0,Free +1,unique_prod3,nb_failing,48,48,0,0,Free +1,unique_prod3,nb_failing,49,49,0,0,Free +1,unique_prod3,nb_failing,50,50,0,0,Free +1,unique_prod3,nb_failing,51,51,0,0,Free +1,unique_prod3,nb_failing,52,52,0,0,Free +1,unique_prod3,nb_failing,53,53,0,0,Free +1,unique_prod3,nb_failing,54,54,0,0,Free +1,unique_prod3,nb_failing,55,55,0,0,Free +1,unique_prod3,nb_failing,56,56,0,0,Free +1,unique_prod3,nb_failing,57,57,0,0,Free +1,unique_prod3,nb_failing,58,58,0,0,Free +1,unique_prod3,nb_failing,59,59,0,0,Free +1,unique_prod3,nb_failing,60,60,0,0,Free +1,unique_prod3,nb_failing,61,61,0,0,Free +1,unique_prod3,nb_failing,62,62,0,0,Free +1,unique_prod3,nb_failing,63,63,0,0,Free +1,unique_prod3,nb_failing,64,64,0,0,Free +1,unique_prod3,nb_failing,65,65,0,0,Free +1,unique_prod3,nb_failing,66,66,0,0,Free +1,unique_prod3,nb_failing,67,67,0,0,Free +1,unique_prod3,nb_failing,68,68,0,0,Free +1,unique_prod3,nb_failing,69,69,0,0,Free +1,unique_prod3,nb_failing,70,70,0,0,Free +1,unique_prod3,nb_failing,71,71,0,0,Free +1,unique_prod3,nb_failing,72,72,0,0,Free +1,unique_prod3,nb_failing,73,73,0,0,Free +1,unique_prod3,nb_failing,74,74,0,0,Free +1,unique_prod3,nb_failing,75,75,0,0,Free +1,unique_prod3,nb_failing,76,76,0,0,Free +1,unique_prod3,nb_failing,77,77,0,0,Free +1,unique_prod3,nb_failing,78,78,0,0,Free +1,unique_prod3,nb_failing,79,79,0,0,Free +1,unique_prod3,nb_failing,80,80,0,0,Free +1,unique_prod3,nb_failing,81,81,0,0,Free +1,unique_prod3,nb_failing,82,82,0,0,Free +1,unique_prod3,nb_failing,83,83,0,0,Free +1,unique_prod3,nb_failing,84,84,0,0,Free +1,unique_prod3,nb_failing,85,85,0,0,Free +1,unique_prod3,nb_failing,86,86,0,0,Free +1,unique_prod3,nb_failing,87,87,0,0,Free +1,unique_prod3,nb_failing,88,88,0,0,Free +1,unique_prod3,nb_failing,89,89,0,0,Free +1,unique_prod3,nb_failing,90,90,0,0,Free +1,unique_prod3,nb_failing,91,91,0,0,Free +1,unique_prod3,nb_failing,92,92,0,0,Free +1,unique_prod3,nb_failing,93,93,0,0,Free +1,unique_prod3,nb_failing,94,94,0,0,Free +1,unique_prod3,nb_failing,95,95,0,0,Free +1,unique_prod3,nb_failing,96,96,0,0,Free +1,unique_prod3,nb_failing,97,97,0,0,Free +1,unique_prod3,nb_failing,98,98,0,0,Free +1,unique_prod3,nb_failing,99,99,0,0,Free +1,unique_prod3,nb_failing,100,100,0,0,Free +1,unique_prod3,nb_failing,101,101,0,0,Free +1,unique_prod3,nb_failing,102,102,0,0,Free +1,unique_prod3,nb_failing,103,103,0,0,Free +1,unique_prod3,nb_failing,104,104,0,0,Free +1,unique_prod3,nb_failing,105,105,0,0,Free +1,unique_prod3,nb_failing,106,106,0,0,Free +1,unique_prod3,nb_failing,107,107,0,0,Free +1,unique_prod3,nb_failing,108,108,0,0,Free +1,unique_prod3,nb_failing,109,109,0,0,Free +1,unique_prod3,nb_failing,110,110,0,0,Free +1,unique_prod3,nb_failing,111,111,0,0,Free +1,unique_prod3,nb_failing,112,112,0,0,Free +1,unique_prod3,nb_failing,113,113,0,0,Free +1,unique_prod3,nb_failing,114,114,0,0,Free +1,unique_prod3,nb_failing,115,115,0,0,Free +1,unique_prod3,nb_failing,116,116,0,0,Free +1,unique_prod3,nb_failing,117,117,0,0,Free +1,unique_prod3,nb_failing,118,118,0,0,Free +1,unique_prod3,nb_failing,119,119,0,0,Free +1,unique_prod3,nb_failing,120,120,0,0,Free +1,unique_prod3,nb_failing,121,121,0,0,Free +1,unique_prod3,nb_failing,122,122,0,0,Free +1,unique_prod3,nb_failing,123,123,0,0,Free +1,unique_prod3,nb_failing,124,124,0,0,Free +1,unique_prod3,nb_failing,125,125,0,0,Free +1,unique_prod3,nb_failing,126,126,0,0,Free +1,unique_prod3,nb_failing,127,127,0,0,Free +1,unique_prod3,nb_failing,128,128,0,0,Free +1,unique_prod3,nb_failing,129,129,0,0,Free +1,unique_prod3,nb_failing,130,130,0,0,Free +1,unique_prod3,nb_failing,131,131,0,0,Free +1,unique_prod3,nb_failing,132,132,0,0,Free +1,unique_prod3,nb_failing,133,133,0,0,Free +1,unique_prod3,nb_failing,134,134,0,0,Free +1,unique_prod3,nb_failing,135,135,0,0,Free +1,unique_prod3,nb_failing,136,136,0,0,Free +1,unique_prod3,nb_failing,137,137,0,0,Free +1,unique_prod3,nb_failing,138,138,0,0,Free +1,unique_prod3,nb_failing,139,139,0,0,Free +1,unique_prod3,nb_failing,140,140,0,0,Free +1,unique_prod3,nb_failing,141,141,0,0,Free +1,unique_prod3,nb_failing,142,142,0,0,Free +1,unique_prod3,nb_failing,143,143,0,0,Free +1,unique_prod3,nb_failing,144,144,0,0,Free +1,unique_prod3,nb_failing,145,145,0,0,Free +1,unique_prod3,nb_failing,146,146,0,0,Free +1,unique_prod3,nb_failing,147,147,0,0,Free +1,unique_prod3,nb_failing,148,148,0,0,Free +1,unique_prod3,nb_failing,149,149,0,0,Free +1,unique_prod3,nb_failing,150,150,0,0,Free +1,unique_prod3,nb_failing,151,151,0,0,Free +1,unique_prod3,nb_failing,152,152,0,0,Free +1,unique_prod3,nb_failing,153,153,0,0,Free +1,unique_prod3,nb_failing,154,154,0,0,Free +1,unique_prod3,nb_failing,155,155,0,0,Free +1,unique_prod3,nb_failing,156,156,0,0,Free +1,unique_prod3,nb_failing,157,157,0,0,Free +1,unique_prod3,nb_failing,158,158,0,0,Free +1,unique_prod3,nb_failing,159,159,0,0,Free +1,unique_prod3,nb_failing,160,160,0,0,Free +1,unique_prod3,nb_failing,161,161,0,0,Free +1,unique_prod3,nb_failing,162,162,0,0,Free +1,unique_prod3,nb_failing,163,163,0,0,Free +1,unique_prod3,nb_failing,164,164,0,0,Free +1,unique_prod3,nb_failing,165,165,0,0,Free +1,unique_prod3,nb_failing,166,166,0,0,Free +1,unique_prod3,nb_failing,167,167,0,0,Free +1,unique_prod3,nb_failing,168,168,0,0,Free +1,unique_prod3,max_generation,1,1,0,None,Free +1,unique_prod3,max_generation,2,2,0,None,Free +1,unique_prod3,max_generation,3,3,0,None,Free +1,unique_prod3,max_generation,4,4,0,None,Free +1,unique_prod3,max_generation,5,5,0,None,Free +1,unique_prod3,max_generation,6,6,0,None,Free +1,unique_prod3,max_generation,7,7,0,None,Free +1,unique_prod3,max_generation,8,8,0,None,Free +1,unique_prod3,max_generation,9,9,0,None,Free +1,unique_prod3,max_generation,10,10,0,None,Free +1,unique_prod3,max_generation,11,11,0,None,Free +1,unique_prod3,max_generation,12,12,0,None,Free +1,unique_prod3,max_generation,13,13,0,None,Free +1,unique_prod3,max_generation,14,14,0,None,Free +1,unique_prod3,max_generation,15,15,0,None,Free +1,unique_prod3,max_generation,16,16,0,None,Free +1,unique_prod3,max_generation,17,17,0,None,Free +1,unique_prod3,max_generation,18,18,0,None,Free +1,unique_prod3,max_generation,19,19,0,None,Free +1,unique_prod3,max_generation,20,20,0,None,Free +1,unique_prod3,max_generation,21,21,0,None,Free +1,unique_prod3,max_generation,22,22,0,None,Free +1,unique_prod3,max_generation,23,23,0,None,Free +1,unique_prod3,max_generation,24,24,0,None,Free +1,unique_prod3,max_generation,25,25,0,None,Free +1,unique_prod3,max_generation,26,26,0,None,Free +1,unique_prod3,max_generation,27,27,0,None,Free +1,unique_prod3,max_generation,28,28,0,None,Free +1,unique_prod3,max_generation,29,29,0,None,Free +1,unique_prod3,max_generation,30,30,0,None,Free +1,unique_prod3,max_generation,31,31,0,None,Free +1,unique_prod3,max_generation,32,32,0,None,Free +1,unique_prod3,max_generation,33,33,0,None,Free +1,unique_prod3,max_generation,34,34,0,None,Free +1,unique_prod3,max_generation,35,35,0,None,Free +1,unique_prod3,max_generation,36,36,0,None,Free +1,unique_prod3,max_generation,37,37,0,None,Free +1,unique_prod3,max_generation,38,38,0,None,Free +1,unique_prod3,max_generation,39,39,0,None,Free +1,unique_prod3,max_generation,40,40,0,None,Free +1,unique_prod3,max_generation,41,41,0,None,Free +1,unique_prod3,max_generation,42,42,0,None,Free +1,unique_prod3,max_generation,43,43,0,None,Free +1,unique_prod3,max_generation,44,44,0,None,Free +1,unique_prod3,max_generation,45,45,0,None,Free +1,unique_prod3,max_generation,46,46,0,None,Free +1,unique_prod3,max_generation,47,47,0,None,Free +1,unique_prod3,max_generation,48,48,0,None,Free +1,unique_prod3,max_generation,49,49,0,None,Free +1,unique_prod3,max_generation,50,50,0,None,Free +1,unique_prod3,max_generation,51,51,0,None,Free +1,unique_prod3,max_generation,52,52,0,None,Free +1,unique_prod3,max_generation,53,53,0,None,Free +1,unique_prod3,max_generation,54,54,0,None,Free +1,unique_prod3,max_generation,55,55,0,None,Free +1,unique_prod3,max_generation,56,56,0,None,Free +1,unique_prod3,max_generation,57,57,0,None,Free +1,unique_prod3,max_generation,58,58,0,None,Free +1,unique_prod3,max_generation,59,59,0,None,Free +1,unique_prod3,max_generation,60,60,0,None,Free +1,unique_prod3,max_generation,61,61,0,None,Free +1,unique_prod3,max_generation,62,62,0,None,Free +1,unique_prod3,max_generation,63,63,0,None,Free +1,unique_prod3,max_generation,64,64,0,None,Free +1,unique_prod3,max_generation,65,65,0,None,Free +1,unique_prod3,max_generation,66,66,0,None,Free +1,unique_prod3,max_generation,67,67,0,None,Free +1,unique_prod3,max_generation,68,68,0,None,Free +1,unique_prod3,max_generation,69,69,0,None,Free +1,unique_prod3,max_generation,70,70,0,None,Free +1,unique_prod3,max_generation,71,71,0,None,Free +1,unique_prod3,max_generation,72,72,0,None,Free +1,unique_prod3,max_generation,73,73,0,None,Free +1,unique_prod3,max_generation,74,74,0,None,Free +1,unique_prod3,max_generation,75,75,0,None,Free +1,unique_prod3,max_generation,76,76,0,None,Free +1,unique_prod3,max_generation,77,77,0,None,Free +1,unique_prod3,max_generation,78,78,0,None,Free +1,unique_prod3,max_generation,79,79,0,None,Free +1,unique_prod3,max_generation,80,80,0,None,Free +1,unique_prod3,max_generation,81,81,0,None,Free +1,unique_prod3,max_generation,82,82,0,None,Free +1,unique_prod3,max_generation,83,83,0,None,Free +1,unique_prod3,max_generation,84,84,0,None,Free +1,unique_prod3,max_generation,85,85,0,None,Free +1,unique_prod3,max_generation,86,86,0,None,Free +1,unique_prod3,max_generation,87,87,0,None,Free +1,unique_prod3,max_generation,88,88,0,None,Free +1,unique_prod3,max_generation,89,89,0,None,Free +1,unique_prod3,max_generation,90,90,0,None,Free +1,unique_prod3,max_generation,91,91,0,None,Free +1,unique_prod3,max_generation,92,92,0,None,Free +1,unique_prod3,max_generation,93,93,0,None,Free +1,unique_prod3,max_generation,94,94,0,None,Free +1,unique_prod3,max_generation,95,95,0,None,Free +1,unique_prod3,max_generation,96,96,0,None,Free +1,unique_prod3,max_generation,97,97,0,None,Free +1,unique_prod3,max_generation,98,98,0,None,Free +1,unique_prod3,max_generation,99,99,0,None,Free +1,unique_prod3,max_generation,100,100,0,None,Free +1,unique_prod3,max_generation,101,101,0,None,Free +1,unique_prod3,max_generation,102,102,0,None,Free +1,unique_prod3,max_generation,103,103,0,None,Free +1,unique_prod3,max_generation,104,104,0,None,Free +1,unique_prod3,max_generation,105,105,0,None,Free +1,unique_prod3,max_generation,106,106,0,None,Free +1,unique_prod3,max_generation,107,107,0,None,Free +1,unique_prod3,max_generation,108,108,0,None,Free +1,unique_prod3,max_generation,109,109,0,None,Free +1,unique_prod3,max_generation,110,110,0,None,Free +1,unique_prod3,max_generation,111,111,0,None,Free +1,unique_prod3,max_generation,112,112,0,None,Free +1,unique_prod3,max_generation,113,113,0,None,Free +1,unique_prod3,max_generation,114,114,0,None,Free +1,unique_prod3,max_generation,115,115,0,None,Free +1,unique_prod3,max_generation,116,116,0,None,Free +1,unique_prod3,max_generation,117,117,0,None,Free +1,unique_prod3,max_generation,118,118,0,None,Free +1,unique_prod3,max_generation,119,119,0,None,Free +1,unique_prod3,max_generation,120,120,0,None,Free +1,unique_prod3,max_generation,121,121,0,None,Free +1,unique_prod3,max_generation,122,122,0,None,Free +1,unique_prod3,max_generation,123,123,0,None,Free +1,unique_prod3,max_generation,124,124,0,None,Free +1,unique_prod3,max_generation,125,125,0,None,Free +1,unique_prod3,max_generation,126,126,0,None,Free +1,unique_prod3,max_generation,127,127,0,None,Free +1,unique_prod3,max_generation,128,128,0,None,Free +1,unique_prod3,max_generation,129,129,0,None,Free +1,unique_prod3,max_generation,130,130,0,None,Free +1,unique_prod3,max_generation,131,131,0,None,Free +1,unique_prod3,max_generation,132,132,0,None,Free +1,unique_prod3,max_generation,133,133,0,None,Free +1,unique_prod3,max_generation,134,134,0,None,Free +1,unique_prod3,max_generation,135,135,0,None,Free +1,unique_prod3,max_generation,136,136,0,None,Free +1,unique_prod3,max_generation,137,137,0,None,Free +1,unique_prod3,max_generation,138,138,0,None,Free +1,unique_prod3,max_generation,139,139,0,None,Free +1,unique_prod3,max_generation,140,140,0,None,Free +1,unique_prod3,max_generation,141,141,0,None,Free +1,unique_prod3,max_generation,142,142,0,None,Free +1,unique_prod3,max_generation,143,143,0,None,Free +1,unique_prod3,max_generation,144,144,0,None,Free +1,unique_prod3,max_generation,145,145,0,None,Free +1,unique_prod3,max_generation,146,146,0,None,Free +1,unique_prod3,max_generation,147,147,0,None,Free +1,unique_prod3,max_generation,148,148,0,None,Free +1,unique_prod3,max_generation,149,149,0,None,Free +1,unique_prod3,max_generation,150,150,0,None,Free +1,unique_prod3,max_generation,151,151,0,None,Free +1,unique_prod3,max_generation,152,152,0,None,Free +1,unique_prod3,max_generation,153,153,0,None,Free +1,unique_prod3,max_generation,154,154,0,None,Free +1,unique_prod3,max_generation,155,155,0,None,Free +1,unique_prod3,max_generation,156,156,0,None,Free +1,unique_prod3,max_generation,157,157,0,None,Free +1,unique_prod3,max_generation,158,158,0,None,Free +1,unique_prod3,max_generation,159,159,0,None,Free +1,unique_prod3,max_generation,160,160,0,None,Free +1,unique_prod3,max_generation,161,161,0,None,Free +1,unique_prod3,max_generation,162,162,0,None,Free +1,unique_prod3,max_generation,163,163,0,None,Free +1,unique_prod3,max_generation,164,164,0,None,Free +1,unique_prod3,max_generation,165,165,0,None,Free +1,unique_prod3,max_generation,166,166,0,None,Free +1,unique_prod3,max_generation,167,167,0,None,Free +1,unique_prod3,max_generation,168,168,0,None,Free +1,unique_prod3,min_generation,1,1,0,None,Free +1,unique_prod3,min_generation,2,2,0,None,Free +1,unique_prod3,min_generation,3,3,0,None,Free +1,unique_prod3,min_generation,4,4,0,None,Free +1,unique_prod3,min_generation,5,5,0,None,Free +1,unique_prod3,min_generation,6,6,0,None,Free +1,unique_prod3,min_generation,7,7,0,None,Free +1,unique_prod3,min_generation,8,8,0,None,Free +1,unique_prod3,min_generation,9,9,0,None,Free +1,unique_prod3,min_generation,10,10,0,None,Free +1,unique_prod3,min_generation,11,11,0,None,Free +1,unique_prod3,min_generation,12,12,0,None,Free +1,unique_prod3,min_generation,13,13,0,None,Free +1,unique_prod3,min_generation,14,14,0,None,Free +1,unique_prod3,min_generation,15,15,0,None,Free +1,unique_prod3,min_generation,16,16,0,None,Free +1,unique_prod3,min_generation,17,17,0,None,Free +1,unique_prod3,min_generation,18,18,0,None,Free +1,unique_prod3,min_generation,19,19,0,None,Free +1,unique_prod3,min_generation,20,20,0,None,Free +1,unique_prod3,min_generation,21,21,0,None,Free +1,unique_prod3,min_generation,22,22,0,None,Free +1,unique_prod3,min_generation,23,23,0,None,Free +1,unique_prod3,min_generation,24,24,0,None,Free +1,unique_prod3,min_generation,25,25,0,None,Free +1,unique_prod3,min_generation,26,26,0,None,Free +1,unique_prod3,min_generation,27,27,0,None,Free +1,unique_prod3,min_generation,28,28,0,None,Free +1,unique_prod3,min_generation,29,29,0,None,Free +1,unique_prod3,min_generation,30,30,0,None,Free +1,unique_prod3,min_generation,31,31,0,None,Free +1,unique_prod3,min_generation,32,32,0,None,Free +1,unique_prod3,min_generation,33,33,0,None,Free +1,unique_prod3,min_generation,34,34,0,None,Free +1,unique_prod3,min_generation,35,35,0,None,Free +1,unique_prod3,min_generation,36,36,0,None,Free +1,unique_prod3,min_generation,37,37,0,None,Free +1,unique_prod3,min_generation,38,38,0,None,Free +1,unique_prod3,min_generation,39,39,0,None,Free +1,unique_prod3,min_generation,40,40,0,None,Free +1,unique_prod3,min_generation,41,41,0,None,Free +1,unique_prod3,min_generation,42,42,0,None,Free +1,unique_prod3,min_generation,43,43,0,None,Free +1,unique_prod3,min_generation,44,44,0,None,Free +1,unique_prod3,min_generation,45,45,0,None,Free +1,unique_prod3,min_generation,46,46,0,None,Free +1,unique_prod3,min_generation,47,47,0,None,Free +1,unique_prod3,min_generation,48,48,0,None,Free +1,unique_prod3,min_generation,49,49,0,None,Free +1,unique_prod3,min_generation,50,50,0,None,Free +1,unique_prod3,min_generation,51,51,0,None,Free +1,unique_prod3,min_generation,52,52,0,None,Free +1,unique_prod3,min_generation,53,53,0,None,Free +1,unique_prod3,min_generation,54,54,0,None,Free +1,unique_prod3,min_generation,55,55,0,None,Free +1,unique_prod3,min_generation,56,56,0,None,Free +1,unique_prod3,min_generation,57,57,0,None,Free +1,unique_prod3,min_generation,58,58,0,None,Free +1,unique_prod3,min_generation,59,59,0,None,Free +1,unique_prod3,min_generation,60,60,0,None,Free +1,unique_prod3,min_generation,61,61,0,None,Free +1,unique_prod3,min_generation,62,62,0,None,Free +1,unique_prod3,min_generation,63,63,0,None,Free +1,unique_prod3,min_generation,64,64,0,None,Free +1,unique_prod3,min_generation,65,65,0,None,Free +1,unique_prod3,min_generation,66,66,0,None,Free +1,unique_prod3,min_generation,67,67,0,None,Free +1,unique_prod3,min_generation,68,68,0,None,Free +1,unique_prod3,min_generation,69,69,0,None,Free +1,unique_prod3,min_generation,70,70,0,None,Free +1,unique_prod3,min_generation,71,71,0,None,Free +1,unique_prod3,min_generation,72,72,0,None,Free +1,unique_prod3,min_generation,73,73,0,None,Free +1,unique_prod3,min_generation,74,74,0,None,Free +1,unique_prod3,min_generation,75,75,0,None,Free +1,unique_prod3,min_generation,76,76,0,None,Free +1,unique_prod3,min_generation,77,77,0,None,Free +1,unique_prod3,min_generation,78,78,0,None,Free +1,unique_prod3,min_generation,79,79,0,None,Free +1,unique_prod3,min_generation,80,80,0,None,Free +1,unique_prod3,min_generation,81,81,0,None,Free +1,unique_prod3,min_generation,82,82,0,None,Free +1,unique_prod3,min_generation,83,83,0,None,Free +1,unique_prod3,min_generation,84,84,0,None,Free +1,unique_prod3,min_generation,85,85,0,None,Free +1,unique_prod3,min_generation,86,86,0,None,Free +1,unique_prod3,min_generation,87,87,0,None,Free +1,unique_prod3,min_generation,88,88,0,None,Free +1,unique_prod3,min_generation,89,89,0,None,Free +1,unique_prod3,min_generation,90,90,0,None,Free +1,unique_prod3,min_generation,91,91,0,None,Free +1,unique_prod3,min_generation,92,92,0,None,Free +1,unique_prod3,min_generation,93,93,0,None,Free +1,unique_prod3,min_generation,94,94,0,None,Free +1,unique_prod3,min_generation,95,95,0,None,Free +1,unique_prod3,min_generation,96,96,0,None,Free +1,unique_prod3,min_generation,97,97,0,None,Free +1,unique_prod3,min_generation,98,98,0,None,Free +1,unique_prod3,min_generation,99,99,0,None,Free +1,unique_prod3,min_generation,100,100,0,None,Free +1,unique_prod3,min_generation,101,101,0,None,Free +1,unique_prod3,min_generation,102,102,0,None,Free +1,unique_prod3,min_generation,103,103,0,None,Free +1,unique_prod3,min_generation,104,104,0,None,Free +1,unique_prod3,min_generation,105,105,0,None,Free +1,unique_prod3,min_generation,106,106,0,None,Free +1,unique_prod3,min_generation,107,107,0,None,Free +1,unique_prod3,min_generation,108,108,0,None,Free +1,unique_prod3,min_generation,109,109,0,None,Free +1,unique_prod3,min_generation,110,110,0,None,Free +1,unique_prod3,min_generation,111,111,0,None,Free +1,unique_prod3,min_generation,112,112,0,None,Free +1,unique_prod3,min_generation,113,113,0,None,Free +1,unique_prod3,min_generation,114,114,0,None,Free +1,unique_prod3,min_generation,115,115,0,None,Free +1,unique_prod3,min_generation,116,116,0,None,Free +1,unique_prod3,min_generation,117,117,0,None,Free +1,unique_prod3,min_generation,118,118,0,None,Free +1,unique_prod3,min_generation,119,119,0,None,Free +1,unique_prod3,min_generation,120,120,0,None,Free +1,unique_prod3,min_generation,121,121,0,None,Free +1,unique_prod3,min_generation,122,122,0,None,Free +1,unique_prod3,min_generation,123,123,0,None,Free +1,unique_prod3,min_generation,124,124,0,None,Free +1,unique_prod3,min_generation,125,125,0,None,Free +1,unique_prod3,min_generation,126,126,0,None,Free +1,unique_prod3,min_generation,127,127,0,None,Free +1,unique_prod3,min_generation,128,128,0,None,Free +1,unique_prod3,min_generation,129,129,0,None,Free +1,unique_prod3,min_generation,130,130,0,None,Free +1,unique_prod3,min_generation,131,131,0,None,Free +1,unique_prod3,min_generation,132,132,0,None,Free +1,unique_prod3,min_generation,133,133,0,None,Free +1,unique_prod3,min_generation,134,134,0,None,Free +1,unique_prod3,min_generation,135,135,0,None,Free +1,unique_prod3,min_generation,136,136,0,None,Free +1,unique_prod3,min_generation,137,137,0,None,Free +1,unique_prod3,min_generation,138,138,0,None,Free +1,unique_prod3,min_generation,139,139,0,None,Free +1,unique_prod3,min_generation,140,140,0,None,Free +1,unique_prod3,min_generation,141,141,0,None,Free +1,unique_prod3,min_generation,142,142,0,None,Free +1,unique_prod3,min_generation,143,143,0,None,Free +1,unique_prod3,min_generation,144,144,0,None,Free +1,unique_prod3,min_generation,145,145,0,None,Free +1,unique_prod3,min_generation,146,146,0,None,Free +1,unique_prod3,min_generation,147,147,0,None,Free +1,unique_prod3,min_generation,148,148,0,None,Free +1,unique_prod3,min_generation,149,149,0,None,Free +1,unique_prod3,min_generation,150,150,0,None,Free +1,unique_prod3,min_generation,151,151,0,None,Free +1,unique_prod3,min_generation,152,152,0,None,Free +1,unique_prod3,min_generation,153,153,0,None,Free +1,unique_prod3,min_generation,154,154,0,None,Free +1,unique_prod3,min_generation,155,155,0,None,Free +1,unique_prod3,min_generation,156,156,0,None,Free +1,unique_prod3,min_generation,157,157,0,None,Free +1,unique_prod3,min_generation,158,158,0,None,Free +1,unique_prod3,min_generation,159,159,0,None,Free +1,unique_prod3,min_generation,160,160,0,None,Free +1,unique_prod3,min_generation,161,161,0,None,Free +1,unique_prod3,min_generation,162,162,0,None,Free +1,unique_prod3,min_generation,163,163,0,None,Free +1,unique_prod3,min_generation,164,164,0,None,Free +1,unique_prod3,min_generation,165,165,0,None,Free +1,unique_prod3,min_generation,166,166,0,None,Free +1,unique_prod3,min_generation,167,167,0,None,Free +1,unique_prod3,min_generation,168,168,0,None,Free +1,unique_prod3,on_units_dynamics,1,1,0,None,Free +1,unique_prod3,on_units_dynamics,2,2,0,None,Free +1,unique_prod3,on_units_dynamics,3,3,0,None,Free +1,unique_prod3,on_units_dynamics,4,4,0,None,Free +1,unique_prod3,on_units_dynamics,5,5,0,None,Free +1,unique_prod3,on_units_dynamics,6,6,0,None,Free +1,unique_prod3,on_units_dynamics,7,7,0,None,Free +1,unique_prod3,on_units_dynamics,8,8,0,None,Free +1,unique_prod3,on_units_dynamics,9,9,0,None,Free +1,unique_prod3,on_units_dynamics,10,10,0,None,Free +1,unique_prod3,on_units_dynamics,11,11,0,None,Free +1,unique_prod3,on_units_dynamics,12,12,0,None,Free +1,unique_prod3,on_units_dynamics,13,13,0,None,Free +1,unique_prod3,on_units_dynamics,14,14,0,None,Free +1,unique_prod3,on_units_dynamics,15,15,0,None,Free +1,unique_prod3,on_units_dynamics,16,16,0,None,Free +1,unique_prod3,on_units_dynamics,17,17,0,None,Free +1,unique_prod3,on_units_dynamics,18,18,0,None,Free +1,unique_prod3,on_units_dynamics,19,19,0,None,Free +1,unique_prod3,on_units_dynamics,20,20,0,None,Free +1,unique_prod3,on_units_dynamics,21,21,0,None,Free +1,unique_prod3,on_units_dynamics,22,22,0,None,Free +1,unique_prod3,on_units_dynamics,23,23,0,None,Free +1,unique_prod3,on_units_dynamics,24,24,0,None,Free +1,unique_prod3,on_units_dynamics,25,25,0,None,Free +1,unique_prod3,on_units_dynamics,26,26,0,None,Free +1,unique_prod3,on_units_dynamics,27,27,0,None,Free +1,unique_prod3,on_units_dynamics,28,28,0,None,Free +1,unique_prod3,on_units_dynamics,29,29,0,None,Free +1,unique_prod3,on_units_dynamics,30,30,0,None,Free +1,unique_prod3,on_units_dynamics,31,31,0,None,Free +1,unique_prod3,on_units_dynamics,32,32,0,None,Free +1,unique_prod3,on_units_dynamics,33,33,0,None,Free +1,unique_prod3,on_units_dynamics,34,34,0,None,Free +1,unique_prod3,on_units_dynamics,35,35,0,None,Free +1,unique_prod3,on_units_dynamics,36,36,0,None,Free +1,unique_prod3,on_units_dynamics,37,37,0,None,Free +1,unique_prod3,on_units_dynamics,38,38,0,None,Free +1,unique_prod3,on_units_dynamics,39,39,0,None,Free +1,unique_prod3,on_units_dynamics,40,40,0,None,Free +1,unique_prod3,on_units_dynamics,41,41,0,None,Free +1,unique_prod3,on_units_dynamics,42,42,0,None,Free +1,unique_prod3,on_units_dynamics,43,43,0,None,Free +1,unique_prod3,on_units_dynamics,44,44,0,None,Free +1,unique_prod3,on_units_dynamics,45,45,0,None,Free +1,unique_prod3,on_units_dynamics,46,46,0,None,Free +1,unique_prod3,on_units_dynamics,47,47,0,None,Free +1,unique_prod3,on_units_dynamics,48,48,0,None,Free +1,unique_prod3,on_units_dynamics,49,49,0,None,Free +1,unique_prod3,on_units_dynamics,50,50,0,None,Free +1,unique_prod3,on_units_dynamics,51,51,0,None,Free +1,unique_prod3,on_units_dynamics,52,52,0,None,Free +1,unique_prod3,on_units_dynamics,53,53,0,None,Free +1,unique_prod3,on_units_dynamics,54,54,0,None,Free +1,unique_prod3,on_units_dynamics,55,55,0,None,Free +1,unique_prod3,on_units_dynamics,56,56,0,None,Free +1,unique_prod3,on_units_dynamics,57,57,0,None,Free +1,unique_prod3,on_units_dynamics,58,58,0,None,Free +1,unique_prod3,on_units_dynamics,59,59,0,None,Free +1,unique_prod3,on_units_dynamics,60,60,0,None,Free +1,unique_prod3,on_units_dynamics,61,61,0,None,Free +1,unique_prod3,on_units_dynamics,62,62,0,None,Free +1,unique_prod3,on_units_dynamics,63,63,0,None,Free +1,unique_prod3,on_units_dynamics,64,64,0,None,Free +1,unique_prod3,on_units_dynamics,65,65,0,None,Free +1,unique_prod3,on_units_dynamics,66,66,0,None,Free +1,unique_prod3,on_units_dynamics,67,67,0,None,Free +1,unique_prod3,on_units_dynamics,68,68,0,None,Free +1,unique_prod3,on_units_dynamics,69,69,0,None,Free +1,unique_prod3,on_units_dynamics,70,70,0,None,Free +1,unique_prod3,on_units_dynamics,71,71,0,None,Free +1,unique_prod3,on_units_dynamics,72,72,0,None,Free +1,unique_prod3,on_units_dynamics,73,73,0,None,Free +1,unique_prod3,on_units_dynamics,74,74,0,None,Free +1,unique_prod3,on_units_dynamics,75,75,0,None,Free +1,unique_prod3,on_units_dynamics,76,76,0,None,Free +1,unique_prod3,on_units_dynamics,77,77,0,None,Free +1,unique_prod3,on_units_dynamics,78,78,0,None,Free +1,unique_prod3,on_units_dynamics,79,79,0,None,Free +1,unique_prod3,on_units_dynamics,80,80,0,None,Free +1,unique_prod3,on_units_dynamics,81,81,0,None,Free +1,unique_prod3,on_units_dynamics,82,82,0,None,Free +1,unique_prod3,on_units_dynamics,83,83,0,None,Free +1,unique_prod3,on_units_dynamics,84,84,0,None,Free +1,unique_prod3,on_units_dynamics,85,85,0,None,Free +1,unique_prod3,on_units_dynamics,86,86,0,None,Free +1,unique_prod3,on_units_dynamics,87,87,0,None,Free +1,unique_prod3,on_units_dynamics,88,88,0,None,Free +1,unique_prod3,on_units_dynamics,89,89,0,None,Free +1,unique_prod3,on_units_dynamics,90,90,0,None,Free +1,unique_prod3,on_units_dynamics,91,91,0,None,Free +1,unique_prod3,on_units_dynamics,92,92,0,None,Free +1,unique_prod3,on_units_dynamics,93,93,0,None,Free +1,unique_prod3,on_units_dynamics,94,94,0,None,Free +1,unique_prod3,on_units_dynamics,95,95,0,None,Free +1,unique_prod3,on_units_dynamics,96,96,0,None,Free +1,unique_prod3,on_units_dynamics,97,97,0,None,Free +1,unique_prod3,on_units_dynamics,98,98,0,None,Free +1,unique_prod3,on_units_dynamics,99,99,0,None,Free +1,unique_prod3,on_units_dynamics,100,100,0,None,Free +1,unique_prod3,on_units_dynamics,101,101,0,None,Free +1,unique_prod3,on_units_dynamics,102,102,0,None,Free +1,unique_prod3,on_units_dynamics,103,103,0,None,Free +1,unique_prod3,on_units_dynamics,104,104,0,None,Free +1,unique_prod3,on_units_dynamics,105,105,0,None,Free +1,unique_prod3,on_units_dynamics,106,106,0,None,Free +1,unique_prod3,on_units_dynamics,107,107,0,None,Free +1,unique_prod3,on_units_dynamics,108,108,0,None,Free +1,unique_prod3,on_units_dynamics,109,109,0,None,Free +1,unique_prod3,on_units_dynamics,110,110,0,None,Free +1,unique_prod3,on_units_dynamics,111,111,0,None,Free +1,unique_prod3,on_units_dynamics,112,112,0,None,Free +1,unique_prod3,on_units_dynamics,113,113,0,None,Free +1,unique_prod3,on_units_dynamics,114,114,0,None,Free +1,unique_prod3,on_units_dynamics,115,115,0,None,Free +1,unique_prod3,on_units_dynamics,116,116,0,None,Free +1,unique_prod3,on_units_dynamics,117,117,0,None,Free +1,unique_prod3,on_units_dynamics,118,118,0,None,Free +1,unique_prod3,on_units_dynamics,119,119,0,None,Free +1,unique_prod3,on_units_dynamics,120,120,0,None,Free +1,unique_prod3,on_units_dynamics,121,121,0,None,Free +1,unique_prod3,on_units_dynamics,122,122,0,None,Free +1,unique_prod3,on_units_dynamics,123,123,0,None,Free +1,unique_prod3,on_units_dynamics,124,124,0,None,Free +1,unique_prod3,on_units_dynamics,125,125,0,None,Free +1,unique_prod3,on_units_dynamics,126,126,0,None,Free +1,unique_prod3,on_units_dynamics,127,127,0,None,Free +1,unique_prod3,on_units_dynamics,128,128,0,None,Free +1,unique_prod3,on_units_dynamics,129,129,0,None,Free +1,unique_prod3,on_units_dynamics,130,130,0,None,Free +1,unique_prod3,on_units_dynamics,131,131,0,None,Free +1,unique_prod3,on_units_dynamics,132,132,0,None,Free +1,unique_prod3,on_units_dynamics,133,133,0,None,Free +1,unique_prod3,on_units_dynamics,134,134,0,None,Free +1,unique_prod3,on_units_dynamics,135,135,0,None,Free +1,unique_prod3,on_units_dynamics,136,136,0,None,Free +1,unique_prod3,on_units_dynamics,137,137,0,None,Free +1,unique_prod3,on_units_dynamics,138,138,0,None,Free +1,unique_prod3,on_units_dynamics,139,139,0,None,Free +1,unique_prod3,on_units_dynamics,140,140,0,None,Free +1,unique_prod3,on_units_dynamics,141,141,0,None,Free +1,unique_prod3,on_units_dynamics,142,142,0,None,Free +1,unique_prod3,on_units_dynamics,143,143,0,None,Free +1,unique_prod3,on_units_dynamics,144,144,0,None,Free +1,unique_prod3,on_units_dynamics,145,145,0,None,Free +1,unique_prod3,on_units_dynamics,146,146,0,None,Free +1,unique_prod3,on_units_dynamics,147,147,0,None,Free +1,unique_prod3,on_units_dynamics,148,148,0,None,Free +1,unique_prod3,on_units_dynamics,149,149,0,None,Free +1,unique_prod3,on_units_dynamics,150,150,0,None,Free +1,unique_prod3,on_units_dynamics,151,151,0,None,Free +1,unique_prod3,on_units_dynamics,152,152,0,None,Free +1,unique_prod3,on_units_dynamics,153,153,0,None,Free +1,unique_prod3,on_units_dynamics,154,154,0,None,Free +1,unique_prod3,on_units_dynamics,155,155,0,None,Free +1,unique_prod3,on_units_dynamics,156,156,0,None,Free +1,unique_prod3,on_units_dynamics,157,157,0,None,Free +1,unique_prod3,on_units_dynamics,158,158,0,None,Free +1,unique_prod3,on_units_dynamics,159,159,0,None,Free +1,unique_prod3,on_units_dynamics,160,160,0,None,Free +1,unique_prod3,on_units_dynamics,161,161,0,None,Free +1,unique_prod3,on_units_dynamics,162,162,0,None,Free +1,unique_prod3,on_units_dynamics,163,163,0,None,Free +1,unique_prod3,on_units_dynamics,164,164,0,None,Free +1,unique_prod3,on_units_dynamics,165,165,0,None,Free +1,unique_prod3,on_units_dynamics,166,166,0,None,Free +1,unique_prod3,on_units_dynamics,167,167,0,None,Free +1,unique_prod3,on_units_dynamics,168,168,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,1,1,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,2,2,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,3,3,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,4,4,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,5,5,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,6,6,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,7,7,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,8,8,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,9,9,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,10,10,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,11,11,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,12,12,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,13,13,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,14,14,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,15,15,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,16,16,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,17,17,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,18,18,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,19,19,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,20,20,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,21,21,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,22,22,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,23,23,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,24,24,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,25,25,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,26,26,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,27,27,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,28,28,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,29,29,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,30,30,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,31,31,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,32,32,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,33,33,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,34,34,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,35,35,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,36,36,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,37,37,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,38,38,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,39,39,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,40,40,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,41,41,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,42,42,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,43,43,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,44,44,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,45,45,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,46,46,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,47,47,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,48,48,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,49,49,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,50,50,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,51,51,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,52,52,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,53,53,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,54,54,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,55,55,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,56,56,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,57,57,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,58,58,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,59,59,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,60,60,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,61,61,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,62,62,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,63,63,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,64,64,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,65,65,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,66,66,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,67,67,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,68,68,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,69,69,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,70,70,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,71,71,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,72,72,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,73,73,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,74,74,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,75,75,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,76,76,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,77,77,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,78,78,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,79,79,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,80,80,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,81,81,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,82,82,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,83,83,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,84,84,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,85,85,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,86,86,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,87,87,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,88,88,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,89,89,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,90,90,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,91,91,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,92,92,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,93,93,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,94,94,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,95,95,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,96,96,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,97,97,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,98,98,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,99,99,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,100,100,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,101,101,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,102,102,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,103,103,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,104,104,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,105,105,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,106,106,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,107,107,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,108,108,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,109,109,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,110,110,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,111,111,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,112,112,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,113,113,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,114,114,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,115,115,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,116,116,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,117,117,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,118,118,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,119,119,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,120,120,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,121,121,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,122,122,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,123,123,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,124,124,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,125,125,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,126,126,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,127,127,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,128,128,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,129,129,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,130,130,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,131,131,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,132,132,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,133,133,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,134,134,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,135,135,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,136,136,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,137,137,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,138,138,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,139,139,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,140,140,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,141,141,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,142,142,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,143,143,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,144,144,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,145,145,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,146,146,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,147,147,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,148,148,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,149,149,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,150,150,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,151,151,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,152,152,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,153,153,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,154,154,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,155,155,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,156,156,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,157,157,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,158,158,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,159,159,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,160,160,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,161,161,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,162,162,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,163,163,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,164,164,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,165,165,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,166,166,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,167,167,0,None,Free +1,unique_prod3,nb_failing_lower_than_stopping,168,168,0,None,Free +1,unique_prod3,min_up_duration,1,1,0,None,Free +1,unique_prod3,min_up_duration,2,2,0,None,Free +1,unique_prod3,min_up_duration,3,3,0,None,Free +1,unique_prod3,min_up_duration,4,4,0,None,Free +1,unique_prod3,min_up_duration,5,5,0,None,Free +1,unique_prod3,min_up_duration,6,6,0,None,Free +1,unique_prod3,min_up_duration,7,7,0,None,Free +1,unique_prod3,min_up_duration,8,8,0,None,Free +1,unique_prod3,min_up_duration,9,9,0,None,Free +1,unique_prod3,min_up_duration,10,10,0,None,Free +1,unique_prod3,min_up_duration,11,11,0,None,Free +1,unique_prod3,min_up_duration,12,12,0,None,Free +1,unique_prod3,min_up_duration,13,13,0,None,Free +1,unique_prod3,min_up_duration,14,14,0,None,Free +1,unique_prod3,min_up_duration,15,15,0,None,Free +1,unique_prod3,min_up_duration,16,16,0,None,Free +1,unique_prod3,min_up_duration,17,17,0,None,Free +1,unique_prod3,min_up_duration,18,18,0,None,Free +1,unique_prod3,min_up_duration,19,19,0,None,Free +1,unique_prod3,min_up_duration,20,20,0,None,Free +1,unique_prod3,min_up_duration,21,21,0,None,Free +1,unique_prod3,min_up_duration,22,22,0,None,Free +1,unique_prod3,min_up_duration,23,23,0,None,Free +1,unique_prod3,min_up_duration,24,24,0,None,Free +1,unique_prod3,min_up_duration,25,25,0,None,Free +1,unique_prod3,min_up_duration,26,26,0,None,Free +1,unique_prod3,min_up_duration,27,27,0,None,Free +1,unique_prod3,min_up_duration,28,28,0,None,Free +1,unique_prod3,min_up_duration,29,29,0,None,Free +1,unique_prod3,min_up_duration,30,30,0,None,Free +1,unique_prod3,min_up_duration,31,31,0,None,Free +1,unique_prod3,min_up_duration,32,32,0,None,Free +1,unique_prod3,min_up_duration,33,33,0,None,Free +1,unique_prod3,min_up_duration,34,34,0,None,Free +1,unique_prod3,min_up_duration,35,35,0,None,Free +1,unique_prod3,min_up_duration,36,36,0,None,Free +1,unique_prod3,min_up_duration,37,37,0,None,Free +1,unique_prod3,min_up_duration,38,38,0,None,Free +1,unique_prod3,min_up_duration,39,39,0,None,Free +1,unique_prod3,min_up_duration,40,40,0,None,Free +1,unique_prod3,min_up_duration,41,41,0,None,Free +1,unique_prod3,min_up_duration,42,42,0,None,Free +1,unique_prod3,min_up_duration,43,43,0,None,Free +1,unique_prod3,min_up_duration,44,44,0,None,Free +1,unique_prod3,min_up_duration,45,45,0,None,Free +1,unique_prod3,min_up_duration,46,46,0,None,Free +1,unique_prod3,min_up_duration,47,47,0,None,Free +1,unique_prod3,min_up_duration,48,48,0,None,Free +1,unique_prod3,min_up_duration,49,49,0,None,Free +1,unique_prod3,min_up_duration,50,50,0,None,Free +1,unique_prod3,min_up_duration,51,51,0,None,Free +1,unique_prod3,min_up_duration,52,52,0,None,Free +1,unique_prod3,min_up_duration,53,53,0,None,Free +1,unique_prod3,min_up_duration,54,54,0,None,Free +1,unique_prod3,min_up_duration,55,55,0,None,Free +1,unique_prod3,min_up_duration,56,56,0,None,Free +1,unique_prod3,min_up_duration,57,57,0,None,Free +1,unique_prod3,min_up_duration,58,58,0,None,Free +1,unique_prod3,min_up_duration,59,59,0,None,Free +1,unique_prod3,min_up_duration,60,60,0,None,Free +1,unique_prod3,min_up_duration,61,61,0,None,Free +1,unique_prod3,min_up_duration,62,62,0,None,Free +1,unique_prod3,min_up_duration,63,63,0,None,Free +1,unique_prod3,min_up_duration,64,64,0,None,Free +1,unique_prod3,min_up_duration,65,65,0,None,Free +1,unique_prod3,min_up_duration,66,66,0,None,Free +1,unique_prod3,min_up_duration,67,67,0,None,Free +1,unique_prod3,min_up_duration,68,68,0,None,Free +1,unique_prod3,min_up_duration,69,69,0,None,Free +1,unique_prod3,min_up_duration,70,70,0,None,Free +1,unique_prod3,min_up_duration,71,71,0,None,Free +1,unique_prod3,min_up_duration,72,72,0,None,Free +1,unique_prod3,min_up_duration,73,73,0,None,Free +1,unique_prod3,min_up_duration,74,74,0,None,Free +1,unique_prod3,min_up_duration,75,75,0,None,Free +1,unique_prod3,min_up_duration,76,76,0,None,Free +1,unique_prod3,min_up_duration,77,77,0,None,Free +1,unique_prod3,min_up_duration,78,78,0,None,Free +1,unique_prod3,min_up_duration,79,79,0,None,Free +1,unique_prod3,min_up_duration,80,80,0,None,Free +1,unique_prod3,min_up_duration,81,81,0,None,Free +1,unique_prod3,min_up_duration,82,82,0,None,Free +1,unique_prod3,min_up_duration,83,83,0,None,Free +1,unique_prod3,min_up_duration,84,84,0,None,Free +1,unique_prod3,min_up_duration,85,85,0,None,Free +1,unique_prod3,min_up_duration,86,86,0,None,Free +1,unique_prod3,min_up_duration,87,87,0,None,Free +1,unique_prod3,min_up_duration,88,88,0,None,Free +1,unique_prod3,min_up_duration,89,89,0,None,Free +1,unique_prod3,min_up_duration,90,90,0,None,Free +1,unique_prod3,min_up_duration,91,91,0,None,Free +1,unique_prod3,min_up_duration,92,92,0,None,Free +1,unique_prod3,min_up_duration,93,93,0,None,Free +1,unique_prod3,min_up_duration,94,94,0,None,Free +1,unique_prod3,min_up_duration,95,95,0,None,Free +1,unique_prod3,min_up_duration,96,96,0,None,Free +1,unique_prod3,min_up_duration,97,97,0,None,Free +1,unique_prod3,min_up_duration,98,98,0,None,Free +1,unique_prod3,min_up_duration,99,99,0,None,Free +1,unique_prod3,min_up_duration,100,100,0,None,Free +1,unique_prod3,min_up_duration,101,101,0,None,Free +1,unique_prod3,min_up_duration,102,102,0,None,Free +1,unique_prod3,min_up_duration,103,103,0,None,Free +1,unique_prod3,min_up_duration,104,104,0,None,Free +1,unique_prod3,min_up_duration,105,105,0,None,Free +1,unique_prod3,min_up_duration,106,106,0,None,Free +1,unique_prod3,min_up_duration,107,107,0,None,Free +1,unique_prod3,min_up_duration,108,108,0,None,Free +1,unique_prod3,min_up_duration,109,109,0,None,Free +1,unique_prod3,min_up_duration,110,110,0,None,Free +1,unique_prod3,min_up_duration,111,111,0,None,Free +1,unique_prod3,min_up_duration,112,112,0,None,Free +1,unique_prod3,min_up_duration,113,113,0,None,Free +1,unique_prod3,min_up_duration,114,114,0,None,Free +1,unique_prod3,min_up_duration,115,115,0,None,Free +1,unique_prod3,min_up_duration,116,116,0,None,Free +1,unique_prod3,min_up_duration,117,117,0,None,Free +1,unique_prod3,min_up_duration,118,118,0,None,Free +1,unique_prod3,min_up_duration,119,119,0,None,Free +1,unique_prod3,min_up_duration,120,120,0,None,Free +1,unique_prod3,min_up_duration,121,121,0,None,Free +1,unique_prod3,min_up_duration,122,122,0,None,Free +1,unique_prod3,min_up_duration,123,123,0,None,Free +1,unique_prod3,min_up_duration,124,124,0,None,Free +1,unique_prod3,min_up_duration,125,125,0,None,Free +1,unique_prod3,min_up_duration,126,126,0,None,Free +1,unique_prod3,min_up_duration,127,127,0,None,Free +1,unique_prod3,min_up_duration,128,128,0,None,Free +1,unique_prod3,min_up_duration,129,129,0,None,Free +1,unique_prod3,min_up_duration,130,130,0,None,Free +1,unique_prod3,min_up_duration,131,131,0,None,Free +1,unique_prod3,min_up_duration,132,132,0,None,Free +1,unique_prod3,min_up_duration,133,133,0,None,Free +1,unique_prod3,min_up_duration,134,134,0,None,Free +1,unique_prod3,min_up_duration,135,135,0,None,Free +1,unique_prod3,min_up_duration,136,136,0,None,Free +1,unique_prod3,min_up_duration,137,137,0,None,Free +1,unique_prod3,min_up_duration,138,138,0,None,Free +1,unique_prod3,min_up_duration,139,139,0,None,Free +1,unique_prod3,min_up_duration,140,140,0,None,Free +1,unique_prod3,min_up_duration,141,141,0,None,Free +1,unique_prod3,min_up_duration,142,142,0,None,Free +1,unique_prod3,min_up_duration,143,143,0,None,Free +1,unique_prod3,min_up_duration,144,144,0,None,Free +1,unique_prod3,min_up_duration,145,145,0,None,Free +1,unique_prod3,min_up_duration,146,146,0,None,Free +1,unique_prod3,min_up_duration,147,147,0,None,Free +1,unique_prod3,min_up_duration,148,148,0,None,Free +1,unique_prod3,min_up_duration,149,149,0,None,Free +1,unique_prod3,min_up_duration,150,150,0,None,Free +1,unique_prod3,min_up_duration,151,151,0,None,Free +1,unique_prod3,min_up_duration,152,152,0,None,Free +1,unique_prod3,min_up_duration,153,153,0,None,Free +1,unique_prod3,min_up_duration,154,154,0,None,Free +1,unique_prod3,min_up_duration,155,155,0,None,Free +1,unique_prod3,min_up_duration,156,156,0,None,Free +1,unique_prod3,min_up_duration,157,157,0,None,Free +1,unique_prod3,min_up_duration,158,158,0,None,Free +1,unique_prod3,min_up_duration,159,159,0,None,Free +1,unique_prod3,min_up_duration,160,160,0,None,Free +1,unique_prod3,min_up_duration,161,161,0,None,Free +1,unique_prod3,min_up_duration,162,162,0,None,Free +1,unique_prod3,min_up_duration,163,163,0,None,Free +1,unique_prod3,min_up_duration,164,164,0,None,Free +1,unique_prod3,min_up_duration,165,165,0,None,Free +1,unique_prod3,min_up_duration,166,166,0,None,Free +1,unique_prod3,min_up_duration,167,167,0,None,Free +1,unique_prod3,min_up_duration,168,168,0,None,Free +1,unique_prod3,min_down_duration,1,1,0,None,Free +1,unique_prod3,min_down_duration,2,2,0,None,Free +1,unique_prod3,min_down_duration,3,3,0,None,Free +1,unique_prod3,min_down_duration,4,4,0,None,Free +1,unique_prod3,min_down_duration,5,5,0,None,Free +1,unique_prod3,min_down_duration,6,6,0,None,Free +1,unique_prod3,min_down_duration,7,7,0,None,Free +1,unique_prod3,min_down_duration,8,8,0,None,Free +1,unique_prod3,min_down_duration,9,9,0,None,Free +1,unique_prod3,min_down_duration,10,10,0,None,Free +1,unique_prod3,min_down_duration,11,11,0,None,Free +1,unique_prod3,min_down_duration,12,12,0,None,Free +1,unique_prod3,min_down_duration,13,13,0,None,Free +1,unique_prod3,min_down_duration,14,14,0,None,Free +1,unique_prod3,min_down_duration,15,15,0,None,Free +1,unique_prod3,min_down_duration,16,16,0,None,Free +1,unique_prod3,min_down_duration,17,17,0,None,Free +1,unique_prod3,min_down_duration,18,18,0,None,Free +1,unique_prod3,min_down_duration,19,19,0,None,Free +1,unique_prod3,min_down_duration,20,20,0,None,Free +1,unique_prod3,min_down_duration,21,21,0,None,Free +1,unique_prod3,min_down_duration,22,22,0,None,Free +1,unique_prod3,min_down_duration,23,23,0,None,Free +1,unique_prod3,min_down_duration,24,24,0,None,Free +1,unique_prod3,min_down_duration,25,25,0,None,Free +1,unique_prod3,min_down_duration,26,26,0,None,Free +1,unique_prod3,min_down_duration,27,27,0,None,Free +1,unique_prod3,min_down_duration,28,28,0,None,Free +1,unique_prod3,min_down_duration,29,29,0,None,Free +1,unique_prod3,min_down_duration,30,30,0,None,Free +1,unique_prod3,min_down_duration,31,31,0,None,Free +1,unique_prod3,min_down_duration,32,32,0,None,Free +1,unique_prod3,min_down_duration,33,33,0,None,Free +1,unique_prod3,min_down_duration,34,34,0,None,Free +1,unique_prod3,min_down_duration,35,35,0,None,Free +1,unique_prod3,min_down_duration,36,36,0,None,Free +1,unique_prod3,min_down_duration,37,37,0,None,Free +1,unique_prod3,min_down_duration,38,38,0,None,Free +1,unique_prod3,min_down_duration,39,39,0,None,Free +1,unique_prod3,min_down_duration,40,40,0,None,Free +1,unique_prod3,min_down_duration,41,41,0,None,Free +1,unique_prod3,min_down_duration,42,42,0,None,Free +1,unique_prod3,min_down_duration,43,43,0,None,Free +1,unique_prod3,min_down_duration,44,44,0,None,Free +1,unique_prod3,min_down_duration,45,45,0,None,Free +1,unique_prod3,min_down_duration,46,46,0,None,Free +1,unique_prod3,min_down_duration,47,47,0,None,Free +1,unique_prod3,min_down_duration,48,48,0,None,Free +1,unique_prod3,min_down_duration,49,49,0,None,Free +1,unique_prod3,min_down_duration,50,50,0,None,Free +1,unique_prod3,min_down_duration,51,51,0,None,Free +1,unique_prod3,min_down_duration,52,52,0,None,Free +1,unique_prod3,min_down_duration,53,53,0,None,Free +1,unique_prod3,min_down_duration,54,54,0,None,Free +1,unique_prod3,min_down_duration,55,55,0,None,Free +1,unique_prod3,min_down_duration,56,56,0,None,Free +1,unique_prod3,min_down_duration,57,57,0,None,Free +1,unique_prod3,min_down_duration,58,58,0,None,Free +1,unique_prod3,min_down_duration,59,59,0,None,Free +1,unique_prod3,min_down_duration,60,60,0,None,Free +1,unique_prod3,min_down_duration,61,61,0,None,Free +1,unique_prod3,min_down_duration,62,62,0,None,Free +1,unique_prod3,min_down_duration,63,63,0,None,Free +1,unique_prod3,min_down_duration,64,64,0,None,Free +1,unique_prod3,min_down_duration,65,65,0,None,Free +1,unique_prod3,min_down_duration,66,66,0,None,Free +1,unique_prod3,min_down_duration,67,67,0,None,Free +1,unique_prod3,min_down_duration,68,68,0,None,Free +1,unique_prod3,min_down_duration,69,69,0,None,Free +1,unique_prod3,min_down_duration,70,70,0,None,Free +1,unique_prod3,min_down_duration,71,71,0,None,Free +1,unique_prod3,min_down_duration,72,72,0,None,Free +1,unique_prod3,min_down_duration,73,73,0,None,Free +1,unique_prod3,min_down_duration,74,74,0,None,Free +1,unique_prod3,min_down_duration,75,75,0,None,Free +1,unique_prod3,min_down_duration,76,76,0,None,Free +1,unique_prod3,min_down_duration,77,77,0,None,Free +1,unique_prod3,min_down_duration,78,78,0,None,Free +1,unique_prod3,min_down_duration,79,79,0,None,Free +1,unique_prod3,min_down_duration,80,80,0,None,Free +1,unique_prod3,min_down_duration,81,81,0,None,Free +1,unique_prod3,min_down_duration,82,82,0,None,Free +1,unique_prod3,min_down_duration,83,83,0,None,Free +1,unique_prod3,min_down_duration,84,84,0,None,Free +1,unique_prod3,min_down_duration,85,85,0,None,Free +1,unique_prod3,min_down_duration,86,86,0,None,Free +1,unique_prod3,min_down_duration,87,87,0,None,Free +1,unique_prod3,min_down_duration,88,88,0,None,Free +1,unique_prod3,min_down_duration,89,89,0,None,Free +1,unique_prod3,min_down_duration,90,90,0,None,Free +1,unique_prod3,min_down_duration,91,91,0,None,Free +1,unique_prod3,min_down_duration,92,92,0,None,Free +1,unique_prod3,min_down_duration,93,93,0,None,Free +1,unique_prod3,min_down_duration,94,94,0,None,Free +1,unique_prod3,min_down_duration,95,95,0,None,Free +1,unique_prod3,min_down_duration,96,96,0,None,Free +1,unique_prod3,min_down_duration,97,97,0,None,Free +1,unique_prod3,min_down_duration,98,98,0,None,Free +1,unique_prod3,min_down_duration,99,99,0,None,Free +1,unique_prod3,min_down_duration,100,100,0,None,Free +1,unique_prod3,min_down_duration,101,101,0,None,Free +1,unique_prod3,min_down_duration,102,102,0,None,Free +1,unique_prod3,min_down_duration,103,103,0,None,Free +1,unique_prod3,min_down_duration,104,104,0,None,Free +1,unique_prod3,min_down_duration,105,105,0,None,Free +1,unique_prod3,min_down_duration,106,106,0,None,Free +1,unique_prod3,min_down_duration,107,107,0,None,Free +1,unique_prod3,min_down_duration,108,108,0,None,Free +1,unique_prod3,min_down_duration,109,109,0,None,Free +1,unique_prod3,min_down_duration,110,110,0,None,Free +1,unique_prod3,min_down_duration,111,111,0,None,Free +1,unique_prod3,min_down_duration,112,112,0,None,Free +1,unique_prod3,min_down_duration,113,113,0,None,Free +1,unique_prod3,min_down_duration,114,114,0,None,Free +1,unique_prod3,min_down_duration,115,115,0,None,Free +1,unique_prod3,min_down_duration,116,116,0,None,Free +1,unique_prod3,min_down_duration,117,117,0,None,Free +1,unique_prod3,min_down_duration,118,118,0,None,Free +1,unique_prod3,min_down_duration,119,119,0,None,Free +1,unique_prod3,min_down_duration,120,120,0,None,Free +1,unique_prod3,min_down_duration,121,121,0,None,Free +1,unique_prod3,min_down_duration,122,122,0,None,Free +1,unique_prod3,min_down_duration,123,123,0,None,Free +1,unique_prod3,min_down_duration,124,124,0,None,Free +1,unique_prod3,min_down_duration,125,125,0,None,Free +1,unique_prod3,min_down_duration,126,126,0,None,Free +1,unique_prod3,min_down_duration,127,127,0,None,Free +1,unique_prod3,min_down_duration,128,128,0,None,Free +1,unique_prod3,min_down_duration,129,129,0,None,Free +1,unique_prod3,min_down_duration,130,130,0,None,Free +1,unique_prod3,min_down_duration,131,131,0,None,Free +1,unique_prod3,min_down_duration,132,132,0,None,Free +1,unique_prod3,min_down_duration,133,133,0,None,Free +1,unique_prod3,min_down_duration,134,134,0,None,Free +1,unique_prod3,min_down_duration,135,135,0,None,Free +1,unique_prod3,min_down_duration,136,136,0,None,Free +1,unique_prod3,min_down_duration,137,137,0,None,Free +1,unique_prod3,min_down_duration,138,138,0,None,Free +1,unique_prod3,min_down_duration,139,139,0,None,Free +1,unique_prod3,min_down_duration,140,140,0,None,Free +1,unique_prod3,min_down_duration,141,141,0,None,Free +1,unique_prod3,min_down_duration,142,142,0,None,Free +1,unique_prod3,min_down_duration,143,143,0,None,Free +1,unique_prod3,min_down_duration,144,144,0,None,Free +1,unique_prod3,min_down_duration,145,145,0,None,Free +1,unique_prod3,min_down_duration,146,146,0,None,Free +1,unique_prod3,min_down_duration,147,147,0,None,Free +1,unique_prod3,min_down_duration,148,148,0,None,Free +1,unique_prod3,min_down_duration,149,149,0,None,Free +1,unique_prod3,min_down_duration,150,150,0,None,Free +1,unique_prod3,min_down_duration,151,151,0,None,Free +1,unique_prod3,min_down_duration,152,152,0,None,Free +1,unique_prod3,min_down_duration,153,153,0,None,Free +1,unique_prod3,min_down_duration,154,154,0,None,Free +1,unique_prod3,min_down_duration,155,155,0,None,Free +1,unique_prod3,min_down_duration,156,156,0,None,Free +1,unique_prod3,min_down_duration,157,157,0,None,Free +1,unique_prod3,min_down_duration,158,158,0,None,Free +1,unique_prod3,min_down_duration,159,159,0,None,Free +1,unique_prod3,min_down_duration,160,160,0,None,Free +1,unique_prod3,min_down_duration,161,161,0,None,Free +1,unique_prod3,min_down_duration,162,162,0,None,Free +1,unique_prod3,min_down_duration,163,163,0,None,Free +1,unique_prod3,min_down_duration,164,164,0,None,Free +1,unique_prod3,min_down_duration,165,165,0,None,Free +1,unique_prod3,min_down_duration,166,166,0,None,Free +1,unique_prod3,min_down_duration,167,167,0,None,Free +1,unique_prod3,min_down_duration,168,168,0,None,Free +1,unique_prod3,balance_port.flow,1,1,0,86.7446002046025,None +1,unique_prod3,balance_port.flow,2,2,0,35.0406665399429,None +1,unique_prod3,balance_port.flow,3,3,0,115.227154977506,None +1,unique_prod3,balance_port.flow,4,4,0,70.2903745370878,None +1,unique_prod3,balance_port.flow,5,5,0,107.757555377475,None +1,unique_prod3,balance_port.flow,6,6,0,44.3839373685811,None +1,unique_prod3,balance_port.flow,7,7,0,27.9081239778326,None +1,unique_prod3,balance_port.flow,8,8,0,62.1306682891878,None +1,unique_prod3,balance_port.flow,9,9,0,46.3806909454445,None +1,unique_prod3,balance_port.flow,10,10,0,104.807109528294,None +1,unique_prod3,balance_port.flow,11,11,0,43.8799050611277,None +1,unique_prod3,balance_port.flow,12,12,0,95.2770752239112,None +1,unique_prod3,balance_port.flow,13,13,0,61.6467962696488,None +1,unique_prod3,balance_port.flow,14,14,0,41.4966259154289,None +1,unique_prod3,balance_port.flow,15,15,0,95.3797838008454,None +1,unique_prod3,balance_port.flow,16,16,0,30.6798797731156,None +1,unique_prod3,balance_port.flow,17,17,0,108.992371620552,None +1,unique_prod3,balance_port.flow,18,18,0,115.453862325268,None +1,unique_prod3,balance_port.flow,19,19,0,113.389769729041,None +1,unique_prod3,balance_port.flow,20,20,0,63.8813714923112,None +1,unique_prod3,balance_port.flow,21,21,0,26.7822393011396,None +1,unique_prod3,balance_port.flow,22,22,0,118.274638521427,None +1,unique_prod3,balance_port.flow,23,23,0,56.6052176289877,None +1,unique_prod3,balance_port.flow,24,24,0,91.8419706133252,None +1,unique_prod3,balance_port.flow,25,25,0,58.7401984903259,None +1,unique_prod3,balance_port.flow,26,26,0,27.3701662033307,None +1,unique_prod3,balance_port.flow,27,27,0,106.085592311079,None +1,unique_prod3,balance_port.flow,28,28,0,87.0963367814873,None +1,unique_prod3,balance_port.flow,29,29,0,97.5055674379429,None +1,unique_prod3,balance_port.flow,30,30,0,77.1923748484218,None +1,unique_prod3,balance_port.flow,31,31,0,108.968921914618,None +1,unique_prod3,balance_port.flow,32,32,0,110.802971078147,None +1,unique_prod3,balance_port.flow,33,33,0,25.0004832536596,None +1,unique_prod3,balance_port.flow,34,34,0,31.1574466696731,None +1,unique_prod3,balance_port.flow,35,35,0,47.4844041369394,None +1,unique_prod3,balance_port.flow,36,36,0,36.7972562353426,None +1,unique_prod3,balance_port.flow,37,37,0,0,None +1,unique_prod3,balance_port.flow,38,38,0,36,None +1,unique_prod3,balance_port.flow,39,39,0,0,None +1,unique_prod3,balance_port.flow,40,40,0,36,None +1,unique_prod3,balance_port.flow,41,41,0,0,None +1,unique_prod3,balance_port.flow,42,42,0,25.7417474644486,None +1,unique_prod3,balance_port.flow,43,43,0,0,None +1,unique_prod3,balance_port.flow,44,44,0,36,None +1,unique_prod3,balance_port.flow,45,45,0,0,None +1,unique_prod3,balance_port.flow,46,46,0,109.568356923788,None +1,unique_prod3,balance_port.flow,47,47,0,112.147547949227,None +1,unique_prod3,balance_port.flow,48,48,0,31.0196153931618,None +1,unique_prod3,balance_port.flow,49,49,0,28.3630025500843,None +1,unique_prod3,balance_port.flow,50,50,0,66.1029979911987,None +1,unique_prod3,balance_port.flow,51,51,0,81.7652092704441,None +1,unique_prod3,balance_port.flow,52,52,0,53.7817952606976,None +1,unique_prod3,balance_port.flow,53,53,0,89.4631912305743,None +1,unique_prod3,balance_port.flow,54,54,0,44.0652624807855,None +1,unique_prod3,balance_port.flow,55,55,0,73.8820125334593,None +1,unique_prod3,balance_port.flow,56,56,0,78.3349277346308,None +1,unique_prod3,balance_port.flow,57,57,0,66.3520696884074,None +1,unique_prod3,balance_port.flow,58,58,0,37.2053910920897,None +1,unique_prod3,balance_port.flow,59,59,0,44.5001463491964,None +1,unique_prod3,balance_port.flow,60,60,0,36.8037016421897,None +1,unique_prod3,balance_port.flow,61,61,0,54.9404833438378,None +1,unique_prod3,balance_port.flow,62,62,0,46.4531630567644,None +1,unique_prod3,balance_port.flow,63,63,0,74.6398229779028,None +1,unique_prod3,balance_port.flow,64,64,0,36,None +1,unique_prod3,balance_port.flow,65,65,0,0,None +1,unique_prod3,balance_port.flow,66,66,0,54.8997719116905,None +1,unique_prod3,balance_port.flow,67,67,0,0,None +1,unique_prod3,balance_port.flow,68,68,0,107.073175793713,None +1,unique_prod3,balance_port.flow,69,69,0,105.339156228021,None +1,unique_prod3,balance_port.flow,70,70,0,34.3504479345707,None +1,unique_prod3,balance_port.flow,71,71,0,54.3666704615316,None +1,unique_prod3,balance_port.flow,72,72,0,63.3099512449237,None +1,unique_prod3,balance_port.flow,73,73,0,89.9605453365551,None +1,unique_prod3,balance_port.flow,74,74,0,91.4145875974346,None +1,unique_prod3,balance_port.flow,75,75,0,66.8350026386077,None +1,unique_prod3,balance_port.flow,76,76,0,64.3509194975245,None +1,unique_prod3,balance_port.flow,77,77,0,101.052536742714,None +1,unique_prod3,balance_port.flow,78,78,0,75.7072862705928,None +1,unique_prod3,balance_port.flow,79,79,0,112.656046983748,None +1,unique_prod3,balance_port.flow,80,80,0,29.9431235735704,None +1,unique_prod3,balance_port.flow,81,81,0,59.2880115205255,None +1,unique_prod3,balance_port.flow,82,82,0,117.15828215524,None +1,unique_prod3,balance_port.flow,83,83,0,91.1139790426458,None +1,unique_prod3,balance_port.flow,84,84,0,109.533655673015,None +1,unique_prod3,balance_port.flow,85,85,0,48.3410609659152,None +1,unique_prod3,balance_port.flow,86,86,0,36,None +1,unique_prod3,balance_port.flow,87,87,0,0,None +1,unique_prod3,balance_port.flow,88,88,0,36,None +1,unique_prod3,balance_port.flow,89,89,0,46.7465283577889,None +1,unique_prod3,balance_port.flow,90,90,0,65.9032473378589,None +1,unique_prod3,balance_port.flow,91,91,0,100.610353298105,None +1,unique_prod3,balance_port.flow,92,92,0,53.6072798246664,None +1,unique_prod3,balance_port.flow,93,93,0,83.6655963362103,None +1,unique_prod3,balance_port.flow,94,94,0,66.2062192939545,None +1,unique_prod3,balance_port.flow,95,95,0,111.67907572244,None +1,unique_prod3,balance_port.flow,96,96,0,87.2338467025124,None +1,unique_prod3,balance_port.flow,97,97,0,86.7486190190613,None +1,unique_prod3,balance_port.flow,98,98,0,58.4119472044511,None +1,unique_prod3,balance_port.flow,99,99,0,74.202390451498,None +1,unique_prod3,balance_port.flow,100,100,0,28.3283819831479,None +1,unique_prod3,balance_port.flow,101,101,0,33.0537275427542,None +1,unique_prod3,balance_port.flow,102,102,0,112.474770533572,None +1,unique_prod3,balance_port.flow,103,103,0,31.0122721342572,None +1,unique_prod3,balance_port.flow,104,104,0,113.224066035942,None +1,unique_prod3,balance_port.flow,105,105,0,92.8334531637361,None +1,unique_prod3,balance_port.flow,106,106,0,83.7418123909771,None +1,unique_prod3,balance_port.flow,107,107,0,96.4790125551072,None +1,unique_prod3,balance_port.flow,108,108,0,37.6003112609882,None +1,unique_prod3,balance_port.flow,109,109,0,100.206930978955,None +1,unique_prod3,balance_port.flow,110,110,0,93.6387023892842,None +1,unique_prod3,balance_port.flow,111,111,0,75.9781709392689,None +1,unique_prod3,balance_port.flow,112,112,0,70.3281113436892,None +1,unique_prod3,balance_port.flow,113,113,0,69.7418505957587,None +1,unique_prod3,balance_port.flow,114,114,0,66.819151722241,None +1,unique_prod3,balance_port.flow,115,115,0,102.342725991086,None +1,unique_prod3,balance_port.flow,116,116,0,36,None +1,unique_prod3,balance_port.flow,117,117,0,0,None +1,unique_prod3,balance_port.flow,118,118,0,36,None +1,unique_prod3,balance_port.flow,119,119,0,0,None +1,unique_prod3,balance_port.flow,120,120,0,36,None +1,unique_prod3,balance_port.flow,121,121,0,0,None +1,unique_prod3,balance_port.flow,122,122,0,36,None +1,unique_prod3,balance_port.flow,123,123,0,0,None +1,unique_prod3,balance_port.flow,124,124,0,36,None +1,unique_prod3,balance_port.flow,125,125,0,66.6101369349591,None +1,unique_prod3,balance_port.flow,126,126,0,77.6399910403183,None +1,unique_prod3,balance_port.flow,127,127,0,102.13097804562,None +1,unique_prod3,balance_port.flow,128,128,0,54.5115202814303,None +1,unique_prod3,balance_port.flow,129,129,0,62.1163289447044,None +1,unique_prod3,balance_port.flow,130,130,0,109.456151836167,None +1,unique_prod3,balance_port.flow,131,131,0,103.239955948888,None +1,unique_prod3,balance_port.flow,132,132,0,34.2419599708293,None +1,unique_prod3,balance_port.flow,133,133,0,39.3384840545482,None +1,unique_prod3,balance_port.flow,134,134,0,86.5170536292929,None +1,unique_prod3,balance_port.flow,135,135,0,78.630164495394,None +1,unique_prod3,balance_port.flow,136,136,0,118.47203262961,None +1,unique_prod3,balance_port.flow,137,137,0,95.9192652152842,None +1,unique_prod3,balance_port.flow,138,138,0,101.030364562023,None +1,unique_prod3,balance_port.flow,139,139,0,45.917748480726,None +1,unique_prod3,balance_port.flow,140,140,0,73.7964908115012,None +1,unique_prod3,balance_port.flow,141,141,0,84.4904066982728,None +1,unique_prod3,balance_port.flow,142,142,0,24.2097946987361,None +1,unique_prod3,balance_port.flow,143,143,0,73.2686175039337,None +1,unique_prod3,balance_port.flow,144,144,0,67.1222562088379,None +1,unique_prod3,balance_port.flow,145,145,0,57.5382096376846,None +1,unique_prod3,balance_port.flow,146,146,0,36,None +1,unique_prod3,balance_port.flow,147,147,0,0,None +1,unique_prod3,balance_port.flow,148,148,0,26.9735481599307,None +1,unique_prod3,balance_port.flow,149,149,0,0,None +1,unique_prod3,balance_port.flow,150,150,0,36,None +1,unique_prod3,balance_port.flow,151,151,0,0,None +1,unique_prod3,balance_port.flow,152,152,0,25.5871743423072,None +1,unique_prod3,balance_port.flow,153,153,0,0,None +1,unique_prod3,balance_port.flow,154,154,0,36,None +1,unique_prod3,balance_port.flow,155,155,0,90.8604860185884,None +1,unique_prod3,balance_port.flow,156,156,0,90.5039771947081,None +1,unique_prod3,balance_port.flow,157,157,0,89.5340226483247,None +1,unique_prod3,balance_port.flow,158,158,0,109.644523969925,None +1,unique_prod3,balance_port.flow,159,159,0,114.775488277451,None +1,unique_prod3,balance_port.flow,160,160,0,31.8535224357824,None +1,unique_prod3,balance_port.flow,161,161,0,35.2213651729531,None +1,unique_prod3,balance_port.flow,162,162,0,27.5394208534916,None +1,unique_prod3,balance_port.flow,163,163,0,112.683859789937,None +1,unique_prod3,balance_port.flow,164,164,0,118.262476071676,None +1,unique_prod3,balance_port.flow,165,165,0,0,None +1,unique_prod3,balance_port.flow,166,166,0,30.0936966993773,None +1,unique_prod3,balance_port.flow,167,167,0,36.2742204680014,None +1,unique_prod3,balance_port.flow,168,168,0,108.469331993674,None +1,load_unique,balance_port.flow,1,1,0,-736.73,None +1,load_unique,balance_port.flow,2,2,0,-547.99,None +1,load_unique,balance_port.flow,3,3,0,-844.24,None +1,load_unique,balance_port.flow,4,4,0,-547.27,None +1,load_unique,balance_port.flow,5,5,0,-509.54,None +1,load_unique,balance_port.flow,6,6,0,-803.45,None +1,load_unique,balance_port.flow,7,7,0,-480.97,None +1,load_unique,balance_port.flow,8,8,0,-521.64,None +1,load_unique,balance_port.flow,9,9,0,-604.89,None +1,load_unique,balance_port.flow,10,10,0,-599.11,None +1,load_unique,balance_port.flow,11,11,0,-722.59,None +1,load_unique,balance_port.flow,12,12,0,-636.72,None +1,load_unique,balance_port.flow,13,13,0,-544.49,None +1,load_unique,balance_port.flow,14,14,0,-528.57,None +1,load_unique,balance_port.flow,15,15,0,-480.98,None +1,load_unique,balance_port.flow,16,16,0,-498.35,None +1,load_unique,balance_port.flow,17,17,0,-681.24,None +1,load_unique,balance_port.flow,18,18,0,-841.3,None +1,load_unique,balance_port.flow,19,19,0,-874.61,None +1,load_unique,balance_port.flow,20,20,0,-506.36,None +1,load_unique,balance_port.flow,21,21,0,-784.14,None +1,load_unique,balance_port.flow,22,22,0,-446.29,None +1,load_unique,balance_port.flow,23,23,0,-594.61,None +1,load_unique,balance_port.flow,24,24,0,-638.76,None +1,load_unique,balance_port.flow,25,25,0,-766.24,None +1,load_unique,balance_port.flow,26,26,0,-845.49,None +1,load_unique,balance_port.flow,27,27,0,-537.89,None +1,load_unique,balance_port.flow,28,28,0,-579.86,None +1,load_unique,balance_port.flow,29,29,0,-796.61,None +1,load_unique,balance_port.flow,30,30,0,-649.69,None +1,load_unique,balance_port.flow,31,31,0,-654.61,None +1,load_unique,balance_port.flow,32,32,0,-604.63,None +1,load_unique,balance_port.flow,33,33,0,-862.36,None +1,load_unique,balance_port.flow,34,34,0,-656.75,None +1,load_unique,balance_port.flow,35,35,0,-799.12,None +1,load_unique,balance_port.flow,36,36,0,-561.84,None +1,load_unique,balance_port.flow,37,37,0,-0,None +1,load_unique,balance_port.flow,38,38,0,-0,None +1,load_unique,balance_port.flow,39,39,0,-0,None +1,load_unique,balance_port.flow,40,40,0,-0,None +1,load_unique,balance_port.flow,41,41,0,-0,None +1,load_unique,balance_port.flow,42,42,0,-0,None +1,load_unique,balance_port.flow,43,43,0,-0,None +1,load_unique,balance_port.flow,44,44,0,-0,None +1,load_unique,balance_port.flow,45,45,0,-0,None +1,load_unique,balance_port.flow,46,46,0,-654.39,None +1,load_unique,balance_port.flow,47,47,0,-687.5,None +1,load_unique,balance_port.flow,48,48,0,-479.1,None +1,load_unique,balance_port.flow,49,49,0,-590.43,None +1,load_unique,balance_port.flow,50,50,0,-755.26,None +1,load_unique,balance_port.flow,51,51,0,-574.25,None +1,load_unique,balance_port.flow,52,52,0,-739.42,None +1,load_unique,balance_port.flow,53,53,0,-635.71,None +1,load_unique,balance_port.flow,54,54,0,-907,None +1,load_unique,balance_port.flow,55,55,0,-858.84,None +1,load_unique,balance_port.flow,56,56,0,-556.55,None +1,load_unique,balance_port.flow,57,57,0,-690.38,None +1,load_unique,balance_port.flow,58,58,0,-577.92,None +1,load_unique,balance_port.flow,59,59,0,-734.97,None +1,load_unique,balance_port.flow,60,60,0,-827.65,None +1,load_unique,balance_port.flow,61,61,0,-589.21,None +1,load_unique,balance_port.flow,62,62,0,-706.6,None +1,load_unique,balance_port.flow,63,63,0,-692.94,None +1,load_unique,balance_port.flow,64,64,0,-0,None +1,load_unique,balance_port.flow,65,65,0,-0,None +1,load_unique,balance_port.flow,66,66,0,-708.89,None +1,load_unique,balance_port.flow,67,67,0,-0,None +1,load_unique,balance_port.flow,68,68,0,-683.05,None +1,load_unique,balance_port.flow,69,69,0,-712.32,None +1,load_unique,balance_port.flow,70,70,0,-658.41,None +1,load_unique,balance_port.flow,71,71,0,-641.16,None +1,load_unique,balance_port.flow,72,72,0,-885.88,None +1,load_unique,balance_port.flow,73,73,0,-737.91,None +1,load_unique,balance_port.flow,74,74,0,-577.85,None +1,load_unique,balance_port.flow,75,75,0,-772.26,None +1,load_unique,balance_port.flow,76,76,0,-637.61,None +1,load_unique,balance_port.flow,77,77,0,-614.17,None +1,load_unique,balance_port.flow,78,78,0,-751.17,None +1,load_unique,balance_port.flow,79,79,0,-807.55,None +1,load_unique,balance_port.flow,80,80,0,-780.66,None +1,load_unique,balance_port.flow,81,81,0,-768.38,None +1,load_unique,balance_port.flow,82,82,0,-707.41,None +1,load_unique,balance_port.flow,83,83,0,-736.61,None +1,load_unique,balance_port.flow,84,84,0,-767.37,None +1,load_unique,balance_port.flow,85,85,0,-843.95,None +1,load_unique,balance_port.flow,86,86,0,-0,None +1,load_unique,balance_port.flow,87,87,0,-0,None +1,load_unique,balance_port.flow,88,88,0,-0,None +1,load_unique,balance_port.flow,89,89,0,-656.74,None +1,load_unique,balance_port.flow,90,90,0,-796.69,None +1,load_unique,balance_port.flow,91,91,0,-712.19,None +1,load_unique,balance_port.flow,92,92,0,-796.34,None +1,load_unique,balance_port.flow,93,93,0,-605.81,None +1,load_unique,balance_port.flow,94,94,0,-697.36,None +1,load_unique,balance_port.flow,95,95,0,-603.45,None +1,load_unique,balance_port.flow,96,96,0,-833.54,None +1,load_unique,balance_port.flow,97,97,0,-606.89,None +1,load_unique,balance_port.flow,98,98,0,-673.68,None +1,load_unique,balance_port.flow,99,99,0,-774.9,None +1,load_unique,balance_port.flow,100,100,0,-0,None +1,load_unique,balance_port.flow,101,101,0,-695.18,None +1,load_unique,balance_port.flow,102,102,0,-881.96,None +1,load_unique,balance_port.flow,103,103,0,-621.29,None +1,load_unique,balance_port.flow,104,104,0,-539.14,None +1,load_unique,balance_port.flow,105,105,0,-713.11,None +1,load_unique,balance_port.flow,106,106,0,-897.81,None +1,load_unique,balance_port.flow,107,107,0,-651.17,None +1,load_unique,balance_port.flow,108,108,0,-702.63,None +1,load_unique,balance_port.flow,109,109,0,-846.88,None +1,load_unique,balance_port.flow,110,110,0,-655.82,None +1,load_unique,balance_port.flow,111,111,0,-868.32,None +1,load_unique,balance_port.flow,112,112,0,-911.14,None +1,load_unique,balance_port.flow,113,113,0,-655.08,None +1,load_unique,balance_port.flow,114,114,0,-844.09,None +1,load_unique,balance_port.flow,115,115,0,-850.72,None +1,load_unique,balance_port.flow,116,116,0,-0,None +1,load_unique,balance_port.flow,117,117,0,-0,None +1,load_unique,balance_port.flow,118,118,0,-0,None +1,load_unique,balance_port.flow,119,119,0,-0,None +1,load_unique,balance_port.flow,120,120,0,-0,None +1,load_unique,balance_port.flow,121,121,0,-0,None +1,load_unique,balance_port.flow,122,122,0,-0,None +1,load_unique,balance_port.flow,123,123,0,-0,None +1,load_unique,balance_port.flow,124,124,0,-0,None +1,load_unique,balance_port.flow,125,125,0,-999.64,None +1,load_unique,balance_port.flow,126,126,0,-869.94,None +1,load_unique,balance_port.flow,127,127,0,-797.79,None +1,load_unique,balance_port.flow,128,128,0,-748.35,None +1,load_unique,balance_port.flow,129,129,0,-809.36,None +1,load_unique,balance_port.flow,130,130,0,-815.73,None +1,load_unique,balance_port.flow,131,131,0,-963.16,None +1,load_unique,balance_port.flow,132,132,0,-752.9,None +1,load_unique,balance_port.flow,133,133,0,-756.33,None +1,load_unique,balance_port.flow,134,134,0,-763.16,None +1,load_unique,balance_port.flow,135,135,0,-931.61,None +1,load_unique,balance_port.flow,136,136,0,-786.89,None +1,load_unique,balance_port.flow,137,137,0,-758.99,None +1,load_unique,balance_port.flow,138,138,0,-711.5,None +1,load_unique,balance_port.flow,139,139,0,-719.95,None +1,load_unique,balance_port.flow,140,140,0,-841.85,None +1,load_unique,balance_port.flow,141,141,0,-814.44,None +1,load_unique,balance_port.flow,142,142,0,-932.7,None +1,load_unique,balance_port.flow,143,143,0,-723.29,None +1,load_unique,balance_port.flow,144,144,0,-917.69,None +1,load_unique,balance_port.flow,145,145,0,-920.01,None +1,load_unique,balance_port.flow,146,146,0,-0,None +1,load_unique,balance_port.flow,147,147,0,-0,None +1,load_unique,balance_port.flow,148,148,0,-0,None +1,load_unique,balance_port.flow,149,149,0,-0,None +1,load_unique,balance_port.flow,150,150,0,-0,None +1,load_unique,balance_port.flow,151,151,0,-0,None +1,load_unique,balance_port.flow,152,152,0,-0,None +1,load_unique,balance_port.flow,153,153,0,-0,None +1,load_unique,balance_port.flow,154,154,0,-0,None +1,load_unique,balance_port.flow,155,155,0,-866,None +1,load_unique,balance_port.flow,156,156,0,-822.11,None +1,load_unique,balance_port.flow,157,157,0,-726.63,None +1,load_unique,balance_port.flow,158,158,0,-932.05,None +1,load_unique,balance_port.flow,159,159,0,-766.61,None +1,load_unique,balance_port.flow,160,160,0,-764.17,None +1,load_unique,balance_port.flow,161,161,0,-750.01,None +1,load_unique,balance_port.flow,162,162,0,-785.51,None +1,load_unique,balance_port.flow,163,163,0,-752.31,None +1,load_unique,balance_port.flow,164,164,0,-925.49,None +1,load_unique,balance_port.flow,165,165,0,-0,None +1,load_unique,balance_port.flow,166,166,0,-836.95,None +1,load_unique,balance_port.flow,167,167,0,-847.57,None +1,load_unique,balance_port.flow,168,168,0,-868.01,None +1,unique,spillage,1,1,0,0,Free +1,unique,spillage,2,2,0,0,Free +1,unique,spillage,3,3,0,0,Free +1,unique,spillage,4,4,0,0,Free +1,unique,spillage,5,5,0,0,Free +1,unique,spillage,6,6,0,0,Free +1,unique,spillage,7,7,0,0,Free +1,unique,spillage,8,8,0,0,Free +1,unique,spillage,9,9,0,0,Free +1,unique,spillage,10,10,0,0,Free +1,unique,spillage,11,11,0,0,Free +1,unique,spillage,12,12,0,0,Free +1,unique,spillage,13,13,0,0,Free +1,unique,spillage,14,14,0,0,Free +1,unique,spillage,15,15,0,0,Free +1,unique,spillage,16,16,0,0,Free +1,unique,spillage,17,17,0,0,Free +1,unique,spillage,18,18,0,0,Free +1,unique,spillage,19,19,0,0,Free +1,unique,spillage,20,20,0,0,Free +1,unique,spillage,21,21,0,0,Free +1,unique,spillage,22,22,0,0,Free +1,unique,spillage,23,23,0,0,Free +1,unique,spillage,24,24,0,0,Free +1,unique,spillage,25,25,0,0,Free +1,unique,spillage,26,26,0,0,Free +1,unique,spillage,27,27,0,0,Free +1,unique,spillage,28,28,0,0,Free +1,unique,spillage,29,29,0,0,Free +1,unique,spillage,30,30,0,0,Free +1,unique,spillage,31,31,0,0,Free +1,unique,spillage,32,32,0,0,Free +1,unique,spillage,33,33,0,0,Free +1,unique,spillage,34,34,0,0,Free +1,unique,spillage,35,35,0,0,Free +1,unique,spillage,36,36,0,0,Free +1,unique,spillage,37,37,0,-0,Free +1,unique,spillage,38,38,0,186,Free +1,unique,spillage,39,39,0,-0,Free +1,unique,spillage,40,40,0,186,Free +1,unique,spillage,41,41,0,-0,Free +1,unique,spillage,42,42,0,175.741747464449,Free +1,unique,spillage,43,43,0,-0,Free +1,unique,spillage,44,44,0,186,Free +1,unique,spillage,45,45,0,-0,Free +1,unique,spillage,46,46,0,0,Free +1,unique,spillage,47,47,0,0,Free +1,unique,spillage,48,48,0,0,Free +1,unique,spillage,49,49,0,0,Free +1,unique,spillage,50,50,0,0,Free +1,unique,spillage,51,51,0,0,Free +1,unique,spillage,52,52,0,0,Free +1,unique,spillage,53,53,0,0,Free +1,unique,spillage,54,54,0,0,Free +1,unique,spillage,55,55,0,0,Free +1,unique,spillage,56,56,0,0,Free +1,unique,spillage,57,57,0,0,Free +1,unique,spillage,58,58,0,0,Free +1,unique,spillage,59,59,0,0,Free +1,unique,spillage,60,60,0,0,Free +1,unique,spillage,61,61,0,0,Free +1,unique,spillage,62,62,0,0,Free +1,unique,spillage,63,63,0,0,Free +1,unique,spillage,64,64,0,186,Free +1,unique,spillage,65,65,0,-0,Free +1,unique,spillage,66,66,0,0,Free +1,unique,spillage,67,67,0,-0,Free +1,unique,spillage,68,68,0,0,Free +1,unique,spillage,69,69,0,0,Free +1,unique,spillage,70,70,0,0,Free +1,unique,spillage,71,71,0,0,Free +1,unique,spillage,72,72,0,0,Free +1,unique,spillage,73,73,0,0,Free +1,unique,spillage,74,74,0,0,Free +1,unique,spillage,75,75,0,0,Free +1,unique,spillage,76,76,0,0,Free +1,unique,spillage,77,77,0,0,Free +1,unique,spillage,78,78,0,0,Free +1,unique,spillage,79,79,0,0,Free +1,unique,spillage,80,80,0,0,Free +1,unique,spillage,81,81,0,0,Free +1,unique,spillage,82,82,0,0,Free +1,unique,spillage,83,83,0,0,Free +1,unique,spillage,84,84,0,0,Free +1,unique,spillage,85,85,0,0,Free +1,unique,spillage,86,86,0,186,Free +1,unique,spillage,87,87,0,-0,Free +1,unique,spillage,88,88,0,186,Free +1,unique,spillage,89,89,0,0,Free +1,unique,spillage,90,90,0,0,Free +1,unique,spillage,91,91,0,0,Free +1,unique,spillage,92,92,0,0,Free +1,unique,spillage,93,93,0,0,Free +1,unique,spillage,94,94,0,0,Free +1,unique,spillage,95,95,0,0,Free +1,unique,spillage,96,96,0,0,Free +1,unique,spillage,97,97,0,0,Free +1,unique,spillage,98,98,0,0,Free +1,unique,spillage,99,99,0,0,Free +1,unique,spillage,100,100,0,178.328381983148,Free +1,unique,spillage,101,101,0,0,Free +1,unique,spillage,102,102,0,0,Free +1,unique,spillage,103,103,0,0,Free +1,unique,spillage,104,104,0,0,Free +1,unique,spillage,105,105,0,0,Free +1,unique,spillage,106,106,0,0,Free +1,unique,spillage,107,107,0,0,Free +1,unique,spillage,108,108,0,0,Free +1,unique,spillage,109,109,0,0,Free +1,unique,spillage,110,110,0,0,Free +1,unique,spillage,111,111,0,0,Free +1,unique,spillage,112,112,0,0,Free +1,unique,spillage,113,113,0,0,Free +1,unique,spillage,114,114,0,0,Free +1,unique,spillage,115,115,0,0,Free +1,unique,spillage,116,116,0,186,Free +1,unique,spillage,117,117,0,-0,Free +1,unique,spillage,118,118,0,186,Free +1,unique,spillage,119,119,0,-0,Free +1,unique,spillage,120,120,0,186,Free +1,unique,spillage,121,121,0,-0,Free +1,unique,spillage,122,122,0,186,Free +1,unique,spillage,123,123,0,-0,Free +1,unique,spillage,124,124,0,186,Free +1,unique,spillage,125,125,0,0,Free +1,unique,spillage,126,126,0,0,Free +1,unique,spillage,127,127,0,0,Free +1,unique,spillage,128,128,0,0,Free +1,unique,spillage,129,129,0,0,Free +1,unique,spillage,130,130,0,0,Free +1,unique,spillage,131,131,0,0,Free +1,unique,spillage,132,132,0,0,Free +1,unique,spillage,133,133,0,0,Free +1,unique,spillage,134,134,0,0,Free +1,unique,spillage,135,135,0,0,Free +1,unique,spillage,136,136,0,0,Free +1,unique,spillage,137,137,0,0,Free +1,unique,spillage,138,138,0,0,Free +1,unique,spillage,139,139,0,0,Free +1,unique,spillage,140,140,0,0,Free +1,unique,spillage,141,141,0,0,Free +1,unique,spillage,142,142,0,0,Free +1,unique,spillage,143,143,0,0,Free +1,unique,spillage,144,144,0,0,Free +1,unique,spillage,145,145,0,0,Free +1,unique,spillage,146,146,0,186,Free +1,unique,spillage,147,147,0,-0,Free +1,unique,spillage,148,148,0,176.973548159931,Free +1,unique,spillage,149,149,0,-0,Free +1,unique,spillage,150,150,0,186,Free +1,unique,spillage,151,151,0,-0,Free +1,unique,spillage,152,152,0,175.587174342307,Free +1,unique,spillage,153,153,0,-0,Free +1,unique,spillage,154,154,0,186,Free +1,unique,spillage,155,155,0,0,Free +1,unique,spillage,156,156,0,0,Free +1,unique,spillage,157,157,0,0,Free +1,unique,spillage,158,158,0,0,Free +1,unique,spillage,159,159,0,0,Free +1,unique,spillage,160,160,0,0,Free +1,unique,spillage,161,161,0,0,Free +1,unique,spillage,162,162,0,0,Free +1,unique,spillage,163,163,0,0,Free +1,unique,spillage,164,164,0,0,Free +1,unique,spillage,165,165,0,-0,Free +1,unique,spillage,166,166,0,0,Free +1,unique,spillage,167,167,0,0,Free +1,unique,spillage,168,168,0,0,Free +1,unique,unsupplied_energy,1,1,0,299.985399795397,Free +1,unique,unsupplied_energy,2,2,0,162.949333460057,Free +1,unique,unsupplied_energy,3,3,0,379.012845022494,Free +1,unique,unsupplied_energy,4,4,0,126.979625462912,Free +1,unique,unsupplied_energy,5,5,0,51.7824446225246,Free +1,unique,unsupplied_energy,6,6,0,409.066062631419,Free +1,unique,unsupplied_energy,7,7,0,103.061876022167,Free +1,unique,unsupplied_energy,8,8,0,109.509331710812,Free +1,unique,unsupplied_energy,9,9,0,208.509309054556,Free +1,unique,unsupplied_energy,10,10,0,144.302890471706,Free +1,unique,unsupplied_energy,11,11,0,328.710094938872,Free +1,unique,unsupplied_energy,12,12,0,191.442924776089,Free +1,unique,unsupplied_energy,13,13,0,132.843203730351,Free +1,unique,unsupplied_energy,14,14,0,137.073374084571,Free +1,unique,unsupplied_energy,15,15,0,35.6002161991546,Free +1,unique,unsupplied_energy,16,16,0,117.670120226884,Free +1,unique,unsupplied_energy,17,17,0,222.247628379448,Free +1,unique,unsupplied_energy,18,18,0,375.846137674732,Free +1,unique,unsupplied_energy,19,19,0,411.220230270959,Free +1,unique,unsupplied_energy,20,20,0,92.4786285076889,Free +1,unique,unsupplied_energy,21,21,0,407.35776069886,Free +1,unique,unsupplied_energy,22,22,0,0,Free +1,unique,unsupplied_energy,23,23,0,188.004782371012,Free +1,unique,unsupplied_energy,24,24,0,196.918029386675,Free +1,unique,unsupplied_energy,25,25,0,357.499801509674,Free +1,unique,unsupplied_energy,26,26,0,468.119833796669,Free +1,unique,unsupplied_energy,27,27,0,81.8044076889208,Free +1,unique,unsupplied_energy,28,28,0,142.763663218513,Free +1,unique,unsupplied_energy,29,29,0,349.104432562057,Free +1,unique,unsupplied_energy,30,30,0,222.497625151578,Free +1,unique,unsupplied_energy,31,31,0,195.641078085382,Free +1,unique,unsupplied_energy,32,32,0,143.827028921853,Free +1,unique,unsupplied_energy,33,33,0,487.35951674634,Free +1,unique,unsupplied_energy,34,34,0,275.592553330327,Free +1,unique,unsupplied_energy,35,35,0,401.635595863061,Free +1,unique,unsupplied_energy,36,36,0,175.042743764657,Free +1,unique,unsupplied_energy,37,37,0,0,Free +1,unique,unsupplied_energy,38,38,0,0,Free +1,unique,unsupplied_energy,39,39,0,0,Free +1,unique,unsupplied_energy,40,40,0,0,Free +1,unique,unsupplied_energy,41,41,0,0,Free +1,unique,unsupplied_energy,42,42,0,0,Free +1,unique,unsupplied_energy,43,43,0,0,Free +1,unique,unsupplied_energy,44,44,0,0,Free +1,unique,unsupplied_energy,45,45,0,0,Free +1,unique,unsupplied_energy,46,46,0,194.821643076212,Free +1,unique,unsupplied_energy,47,47,0,225.352452050773,Free +1,unique,unsupplied_energy,48,48,0,98.0803846068383,Free +1,unique,unsupplied_energy,49,49,0,212.066997449916,Free +1,unique,unsupplied_energy,50,50,0,339.157002008801,Free +1,unique,unsupplied_energy,51,51,0,142.484790729556,Free +1,unique,unsupplied_energy,52,52,0,335.638204739302,Free +1,unique,unsupplied_energy,53,53,0,196.246808769426,Free +1,unique,unsupplied_energy,54,54,0,512.934737519215,Free +1,unique,unsupplied_energy,55,55,0,434.957987466541,Free +1,unique,unsupplied_energy,56,56,0,128.215072265369,Free +1,unique,unsupplied_energy,57,57,0,274.027930311593,Free +1,unique,unsupplied_energy,58,58,0,190.71460890791,Free +1,unique,unsupplied_energy,59,59,0,340.469853650804,Free +1,unique,unsupplied_energy,60,60,0,440.84629835781,Free +1,unique,unsupplied_energy,61,61,0,184.269516656162,Free +1,unique,unsupplied_energy,62,62,0,310.146836943236,Free +1,unique,unsupplied_energy,63,63,0,268.300177022097,Free +1,unique,unsupplied_energy,64,64,0,0,Free +1,unique,unsupplied_energy,65,65,0,0,Free +1,unique,unsupplied_energy,66,66,0,303.99022808831,Free +1,unique,unsupplied_energy,67,67,0,0,Free +1,unique,unsupplied_energy,68,68,0,225.976824206287,Free +1,unique,unsupplied_energy,69,69,0,256.980843771979,Free +1,unique,unsupplied_energy,70,70,0,274.059552065429,Free +1,unique,unsupplied_energy,71,71,0,236.793329538468,Free +1,unique,unsupplied_energy,72,72,0,472.570048755076,Free +1,unique,unsupplied_energy,73,73,0,297.949454663445,Free +1,unique,unsupplied_energy,74,74,0,136.435412402565,Free +1,unique,unsupplied_energy,75,75,0,355.424997361392,Free +1,unique,unsupplied_energy,76,76,0,223.259080502476,Free +1,unique,unsupplied_energy,77,77,0,163.117463257286,Free +1,unique,unsupplied_energy,78,78,0,325.462713729407,Free +1,unique,unsupplied_energy,79,79,0,344.893953016252,Free +1,unique,unsupplied_energy,80,80,0,400.71687642643,Free +1,unique,unsupplied_energy,81,81,0,359.091988479474,Free +1,unique,unsupplied_energy,82,82,0,240.25171784476,Free +1,unique,unsupplied_energy,83,83,0,295.496020957354,Free +1,unique,unsupplied_energy,84,84,0,307.836344326985,Free +1,unique,unsupplied_energy,85,85,0,445.608939034085,Free +1,unique,unsupplied_energy,86,86,0,0,Free +1,unique,unsupplied_energy,87,87,0,0,Free +1,unique,unsupplied_energy,88,88,0,0,Free +1,unique,unsupplied_energy,89,89,0,259.993471642211,Free +1,unique,unsupplied_energy,90,90,0,380.786752662141,Free +1,unique,unsupplied_energy,91,91,0,261.579646701895,Free +1,unique,unsupplied_energy,92,92,0,392.732720175334,Free +1,unique,unsupplied_energy,93,93,0,172.14440366379,Free +1,unique,unsupplied_energy,94,94,0,281.153780706045,Free +1,unique,unsupplied_energy,95,95,0,141.77092427756,Free +1,unique,unsupplied_energy,96,96,0,396.306153297488,Free +1,unique,unsupplied_energy,97,97,0,170.141380980939,Free +1,unique,unsupplied_energy,98,98,0,265.268052795549,Free +1,unique,unsupplied_energy,99,99,0,350.697609548502,Free +1,unique,unsupplied_energy,100,100,0,0,Free +1,unique,unsupplied_energy,101,101,0,312.126272457246,Free +1,unique,unsupplied_energy,102,102,0,419.485229466429,Free +1,unique,unsupplied_energy,103,103,0,240.277727865743,Free +1,unique,unsupplied_energy,104,104,0,75.9159339640584,Free +1,unique,unsupplied_energy,105,105,0,270.276546836264,Free +1,unique,unsupplied_energy,106,106,0,464.068187609023,Free +1,unique,unsupplied_energy,107,107,0,204.690987444893,Free +1,unique,unsupplied_energy,108,108,0,315.029688739012,Free +1,unique,unsupplied_energy,109,109,0,396.673069021045,Free +1,unique,unsupplied_energy,110,110,0,212.181297610716,Free +1,unique,unsupplied_energy,111,111,0,442.341829060731,Free +1,unique,unsupplied_energy,112,112,0,490.811888656311,Free +1,unique,unsupplied_energy,113,113,0,235.338149404241,Free +1,unique,unsupplied_energy,114,114,0,427.270848277759,Free +1,unique,unsupplied_energy,115,115,0,398.377274008914,Free +1,unique,unsupplied_energy,116,116,0,0,Free +1,unique,unsupplied_energy,117,117,0,0,Free +1,unique,unsupplied_energy,118,118,0,0,Free +1,unique,unsupplied_energy,119,119,0,0,Free +1,unique,unsupplied_energy,120,120,0,0,Free +1,unique,unsupplied_energy,121,121,0,0,Free +1,unique,unsupplied_energy,122,122,0,0,Free +1,unique,unsupplied_energy,123,123,0,0,Free +1,unique,unsupplied_energy,124,124,0,0,Free +1,unique,unsupplied_energy,125,125,0,583.029863065041,Free +1,unique,unsupplied_energy,126,126,0,442.300008959682,Free +1,unique,unsupplied_energy,127,127,0,345.65902195438,Free +1,unique,unsupplied_energy,128,128,0,343.83847971857,Free +1,unique,unsupplied_energy,129,129,0,397.243671055296,Free +1,unique,unsupplied_energy,130,130,0,356.273848163833,Free +1,unique,unsupplied_energy,131,131,0,509.920044051112,Free +1,unique,unsupplied_energy,132,132,0,368.658040029171,Free +1,unique,unsupplied_energy,133,133,0,366.991515945452,Free +1,unique,unsupplied_energy,134,134,0,326.642946370707,Free +1,unique,unsupplied_energy,135,135,0,502.979835504606,Free +1,unique,unsupplied_energy,136,136,0,318.41796737039,Free +1,unique,unsupplied_energy,137,137,0,313.070734784716,Free +1,unique,unsupplied_energy,138,138,0,260.469635437977,Free +1,unique,unsupplied_energy,139,139,0,324.032251519274,Free +1,unique,unsupplied_energy,140,140,0,418.053509188499,Free +1,unique,unsupplied_energy,141,141,0,379.949593301727,Free +1,unique,unsupplied_energy,142,142,0,558.490205301264,Free +1,unique,unsupplied_energy,143,143,0,300.021382496066,Free +1,unique,unsupplied_energy,144,144,0,500.567743791162,Free +1,unique,unsupplied_energy,145,145,0,512.471790362315,Free +1,unique,unsupplied_energy,146,146,0,0,Free +1,unique,unsupplied_energy,147,147,0,0,Free +1,unique,unsupplied_energy,148,148,0,0,Free +1,unique,unsupplied_energy,149,149,0,0,Free +1,unique,unsupplied_energy,150,150,0,0,Free +1,unique,unsupplied_energy,151,151,0,0,Free +1,unique,unsupplied_energy,152,152,0,0,Free +1,unique,unsupplied_energy,153,153,0,0,Free +1,unique,unsupplied_energy,154,154,0,0,Free +1,unique,unsupplied_energy,155,155,0,425.139513981412,Free +1,unique,unsupplied_energy,156,156,0,381.606022805292,Free +1,unique,unsupplied_energy,157,157,0,287.095977351675,Free +1,unique,unsupplied_energy,158,158,0,472.405476030074,Free +1,unique,unsupplied_energy,159,159,0,301.834511722549,Free +1,unique,unsupplied_energy,160,160,0,382.316477564218,Free +1,unique,unsupplied_energy,161,161,0,364.788634827047,Free +1,unique,unsupplied_energy,162,162,0,407.970579146508,Free +1,unique,unsupplied_energy,163,163,0,289.626140210063,Free +1,unique,unsupplied_energy,164,164,0,457.227523928324,Free +1,unique,unsupplied_energy,165,165,0,0,Free +1,unique,unsupplied_energy,166,166,0,456.856303300623,Free +1,unique,unsupplied_energy,167,167,0,461.295779531999,Free +1,unique,unsupplied_energy,168,168,0,409.540668006326,Free +1,unique,balance,1,1,0,None,Free +1,unique,balance,2,2,0,None,Free +1,unique,balance,3,3,0,None,Free +1,unique,balance,4,4,0,None,Free +1,unique,balance,5,5,0,None,Free +1,unique,balance,6,6,0,None,Free +1,unique,balance,7,7,0,None,Free +1,unique,balance,8,8,0,None,Free +1,unique,balance,9,9,0,None,Free +1,unique,balance,10,10,0,None,Free +1,unique,balance,11,11,0,None,Free +1,unique,balance,12,12,0,None,Free +1,unique,balance,13,13,0,None,Free +1,unique,balance,14,14,0,None,Free +1,unique,balance,15,15,0,None,Free +1,unique,balance,16,16,0,None,Free +1,unique,balance,17,17,0,None,Free +1,unique,balance,18,18,0,None,Free +1,unique,balance,19,19,0,None,Free +1,unique,balance,20,20,0,None,Free +1,unique,balance,21,21,0,None,Free +1,unique,balance,22,22,0,None,Free +1,unique,balance,23,23,0,None,Free +1,unique,balance,24,24,0,None,Free +1,unique,balance,25,25,0,None,Free +1,unique,balance,26,26,0,None,Free +1,unique,balance,27,27,0,None,Free +1,unique,balance,28,28,0,None,Free +1,unique,balance,29,29,0,None,Free +1,unique,balance,30,30,0,None,Free +1,unique,balance,31,31,0,None,Free +1,unique,balance,32,32,0,None,Free +1,unique,balance,33,33,0,None,Free +1,unique,balance,34,34,0,None,Free +1,unique,balance,35,35,0,None,Free +1,unique,balance,36,36,0,None,Free +1,unique,balance,37,37,0,None,Free +1,unique,balance,38,38,0,None,Free +1,unique,balance,39,39,0,None,Free +1,unique,balance,40,40,0,None,Free +1,unique,balance,41,41,0,None,Free +1,unique,balance,42,42,0,None,Free +1,unique,balance,43,43,0,None,Free +1,unique,balance,44,44,0,None,Free +1,unique,balance,45,45,0,None,Free +1,unique,balance,46,46,0,None,Free +1,unique,balance,47,47,0,None,Free +1,unique,balance,48,48,0,None,Free +1,unique,balance,49,49,0,None,Free +1,unique,balance,50,50,0,None,Free +1,unique,balance,51,51,0,None,Free +1,unique,balance,52,52,0,None,Free +1,unique,balance,53,53,0,None,Free +1,unique,balance,54,54,0,None,Free +1,unique,balance,55,55,0,None,Free +1,unique,balance,56,56,0,None,Free +1,unique,balance,57,57,0,None,Free +1,unique,balance,58,58,0,None,Free +1,unique,balance,59,59,0,None,Free +1,unique,balance,60,60,0,None,Free +1,unique,balance,61,61,0,None,Free +1,unique,balance,62,62,0,None,Free +1,unique,balance,63,63,0,None,Free +1,unique,balance,64,64,0,None,Free +1,unique,balance,65,65,0,None,Free +1,unique,balance,66,66,0,None,Free +1,unique,balance,67,67,0,None,Free +1,unique,balance,68,68,0,None,Free +1,unique,balance,69,69,0,None,Free +1,unique,balance,70,70,0,None,Free +1,unique,balance,71,71,0,None,Free +1,unique,balance,72,72,0,None,Free +1,unique,balance,73,73,0,None,Free +1,unique,balance,74,74,0,None,Free +1,unique,balance,75,75,0,None,Free +1,unique,balance,76,76,0,None,Free +1,unique,balance,77,77,0,None,Free +1,unique,balance,78,78,0,None,Free +1,unique,balance,79,79,0,None,Free +1,unique,balance,80,80,0,None,Free +1,unique,balance,81,81,0,None,Free +1,unique,balance,82,82,0,None,Free +1,unique,balance,83,83,0,None,Free +1,unique,balance,84,84,0,None,Free +1,unique,balance,85,85,0,None,Free +1,unique,balance,86,86,0,None,Free +1,unique,balance,87,87,0,None,Free +1,unique,balance,88,88,0,None,Free +1,unique,balance,89,89,0,None,Free +1,unique,balance,90,90,0,None,Free +1,unique,balance,91,91,0,None,Free +1,unique,balance,92,92,0,None,Free +1,unique,balance,93,93,0,None,Free +1,unique,balance,94,94,0,None,Free +1,unique,balance,95,95,0,None,Free +1,unique,balance,96,96,0,None,Free +1,unique,balance,97,97,0,None,Free +1,unique,balance,98,98,0,None,Free +1,unique,balance,99,99,0,None,Free +1,unique,balance,100,100,0,None,Free +1,unique,balance,101,101,0,None,Free +1,unique,balance,102,102,0,None,Free +1,unique,balance,103,103,0,None,Free +1,unique,balance,104,104,0,None,Free +1,unique,balance,105,105,0,None,Free +1,unique,balance,106,106,0,None,Free +1,unique,balance,107,107,0,None,Free +1,unique,balance,108,108,0,None,Free +1,unique,balance,109,109,0,None,Free +1,unique,balance,110,110,0,None,Free +1,unique,balance,111,111,0,None,Free +1,unique,balance,112,112,0,None,Free +1,unique,balance,113,113,0,None,Free +1,unique,balance,114,114,0,None,Free +1,unique,balance,115,115,0,None,Free +1,unique,balance,116,116,0,None,Free +1,unique,balance,117,117,0,None,Free +1,unique,balance,118,118,0,None,Free +1,unique,balance,119,119,0,None,Free +1,unique,balance,120,120,0,None,Free +1,unique,balance,121,121,0,None,Free +1,unique,balance,122,122,0,None,Free +1,unique,balance,123,123,0,None,Free +1,unique,balance,124,124,0,None,Free +1,unique,balance,125,125,0,None,Free +1,unique,balance,126,126,0,None,Free +1,unique,balance,127,127,0,None,Free +1,unique,balance,128,128,0,None,Free +1,unique,balance,129,129,0,None,Free +1,unique,balance,130,130,0,None,Free +1,unique,balance,131,131,0,None,Free +1,unique,balance,132,132,0,None,Free +1,unique,balance,133,133,0,None,Free +1,unique,balance,134,134,0,None,Free +1,unique,balance,135,135,0,None,Free +1,unique,balance,136,136,0,None,Free +1,unique,balance,137,137,0,None,Free +1,unique,balance,138,138,0,None,Free +1,unique,balance,139,139,0,None,Free +1,unique,balance,140,140,0,None,Free +1,unique,balance,141,141,0,None,Free +1,unique,balance,142,142,0,None,Free +1,unique,balance,143,143,0,None,Free +1,unique,balance,144,144,0,None,Free +1,unique,balance,145,145,0,None,Free +1,unique,balance,146,146,0,None,Free +1,unique,balance,147,147,0,None,Free +1,unique,balance,148,148,0,None,Free +1,unique,balance,149,149,0,None,Free +1,unique,balance,150,150,0,None,Free +1,unique,balance,151,151,0,None,Free +1,unique,balance,152,152,0,None,Free +1,unique,balance,153,153,0,None,Free +1,unique,balance,154,154,0,None,Free +1,unique,balance,155,155,0,None,Free +1,unique,balance,156,156,0,None,Free +1,unique,balance,157,157,0,None,Free +1,unique,balance,158,158,0,None,Free +1,unique,balance,159,159,0,None,Free +1,unique,balance,160,160,0,None,Free +1,unique,balance,161,161,0,None,Free +1,unique,balance,162,162,0,None,Free +1,unique,balance,163,163,0,None,Free +1,unique,balance,164,164,0,None,Free +1,unique,balance,165,165,0,None,Free +1,unique,balance,166,166,0,None,Free +1,unique,balance,167,167,0,None,Free +1,unique,balance,168,168,0,None,Free +1,None,OBJECTIVE_VALUE,None,None,0,20919941.049852,None diff --git a/tests/e2e/models/operators/test_operators_v1.py b/tests/e2e/models/operators/test_operators_v1.py index 0bd85824..6e32e3ed 100644 --- a/tests/e2e/models/operators/test_operators_v1.py +++ b/tests/e2e/models/operators/test_operators_v1.py @@ -17,16 +17,18 @@ import pandas as pd import pytest -from gems.model.parsing import InputLibrary, parse_yaml_library +from gems.model.parsing import LibrarySchema, parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.simulation import OutputValues, build_problem +from gems.simulation import build_problem +from gems.simulation.simulation_table import SimulationTableBuilder from gems.simulation.time_block import TimeBlock +from gems.study import Study from gems.study.parsing import parse_yaml_components -from gems.study.resolve_components import build_data_base, build_network, resolve_system +from gems.study.resolve_components import build_data_base, resolve_system @pytest.fixture -def data_dir(request) -> Path: +def data_dir(request: pytest.FixtureRequest) -> Path: return request.param @@ -66,7 +68,7 @@ def relative_accuracy() -> float: @pytest.fixture -def input_libraries(input_dir: Path) -> List[InputLibrary]: +def input_libraries(input_dir: Path) -> List[LibrarySchema]: libs_dir = input_dir / "model-libraries" with open(libs_dir / "test_lib.yml") as lib_file: lib_new = parse_yaml_library(lib_file) @@ -79,6 +81,7 @@ def input_libraries(input_dir: Path) -> List[InputLibrary]: Path(__file__).parent / "optest1", Path(__file__).parent / "optest2", Path(__file__).parent / "optest3", + Path(__file__).parent / "optest4", ], indirect=True, ) @@ -87,7 +90,7 @@ def test_model_behaviour( optim_result_file: str, batch: int, relative_accuracy: float, - input_libraries: List[InputLibrary], + input_libraries: List[LibrarySchema], results_dir: Path, series_dir: Path, ) -> None: @@ -102,9 +105,8 @@ def test_model_behaviour( input_component = parse_yaml_components(compo_file) result_lib = resolve_library(input_libraries) - components_input = resolve_system(input_component, result_lib) + system_input = resolve_system(input_component, result_lib) database = build_data_base(input_component, Path(series_dir)) - network = build_network(components_input) df_ref = pd.read_csv(results_dir / optim_result_file) expected_objective = df_ref[df_ref["output"] == "OBJECTIVE_VALUE"]["value"].iloc[0] ref_gen3 = ( @@ -117,26 +119,25 @@ def test_model_behaviour( for _ in range(0, batch): problem = build_problem( - network, - database, + Study(system_input, database), TimeBlock(1, timesteps), - scenarios, - ) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert math.isclose( - problem.solver.Objective().Value(), - problem.solver.Objective().BestBound(), - rel_tol=relative_accuracy, + list(range(scenarios)), ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" assert math.isclose( expected_objective, - problem.solver.Objective().Value(), + problem.objective_value, rel_tol=relative_accuracy, ) - output = OutputValues(problem) - gen3_values = output.component("unique_prod3").var("generation").value[0] + df = SimulationTableBuilder().build(problem) + gen3_values = ( + df.component("unique_prod3") + .output("generation") + .value(scenario_index=0) + .tolist() + ) for t, (ref_val, sol_val) in enumerate(zip(ref_gen3, gen3_values)): assert math.isclose( diff --git a/tests/e2e/models/poc-various-models/libs/standard.py b/tests/e2e/models/poc-various-models/libs/standard.py index a8f6cd7f..ca6a93a1 100644 --- a/tests/e2e/models/poc-various-models/libs/standard.py +++ b/tests/e2e/models/poc-various-models/libs/standard.py @@ -17,7 +17,6 @@ from gems.expression import literal, param, var from gems.expression.expression import port_field from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext from gems.model.constraint import Constraint from gems.model.model import ModelPort, model from gems.model.parameter import float_parameter, int_parameter @@ -58,12 +57,14 @@ == var("spillage") - var("unsupplied_energy"), ) ], - objective_operational_contribution=( - param("spillage_cost") * var("spillage") - + param("ens_cost") * var("unsupplied_energy") - ) - .time_sum() - .expec(), + objective_contributions={ + "operational": ( + param("spillage_cost") * var("spillage") + + param("ens_cost") * var("unsupplied_energy") + ) + .time_sum() + .expec() + }, ) """ @@ -130,13 +131,13 @@ name="Max generation", expression=var("generation") <= param("p_max") ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) GENERATOR_MODEL_WITH_PMIN = model( - id="GEN", + id="GEN_WITH_PMIN", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("p_min", CONSTANT), @@ -160,9 +161,9 @@ lower_bound=literal(0), ), # To test both ways of setting constraints ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) """ @@ -170,7 +171,7 @@ and total generation in whole period. It considers a full storage with no replenishing """ GENERATOR_MODEL_WITH_STORAGE = model( - id="GEN", + id="GEN_WITH_STORAGE", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", CONSTANT), @@ -193,14 +194,14 @@ expression=var("generation").time_sum() <= param("full_storage"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # For now, no starting cost THERMAL_CLUSTER_MODEL_HD = model( - id="GEN", + id="THERMAL_CLUSTER_HD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -265,14 +266,14 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # Same model as previous one, except that starting/stopping variables are now non anticipative THERMAL_CLUSTER_MODEL_DHD = model( - id="GEN", + id="THERMAL_CLUSTER_DHD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -337,9 +338,9 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) SPILLAGE_MODEL = model( @@ -353,9 +354,9 @@ definition=-var("spillage"), ) ], - objective_operational_contribution=(param("cost") * var("spillage")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("spillage")).time_sum().expec() + }, ) UNSUPPLIED_ENERGY_MODEL = model( @@ -369,9 +370,9 @@ definition=var("unsupplied_energy"), ) ], - objective_operational_contribution=(param("cost") * var("unsupplied_energy")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("unsupplied_energy")).time_sum().expec() + }, ) # Simplified model @@ -418,12 +419,12 @@ == param("inflows"), ), ], - objective_operational_contribution=literal(0), # Implcitement nul ? + objective_contributions={"operational": literal(0)}, # Implcitement nul ? ) """ Simple thermal unit that can be invested on""" THERMAL_CANDIDATE = model( - id="GEN", + id="THERMAL_CANDIDATE", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -436,7 +437,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -449,15 +449,15 @@ constraints=[ Constraint(name="Max generation", expression=var("generation") <= var("p_max")) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("p_max"), + }, ) """ Simple thermal unit that can be invested on and with already installed capacity""" THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA = model( - id="GEN", + id="THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -471,7 +471,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -488,8 +487,8 @@ <= param("already_installed_capa") + var("invested_capa"), ) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("invested_capa"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("invested_capa"), + }, ) diff --git a/tests/e2e/models/poc-various-models/libs/standard_sc.py b/tests/e2e/models/poc-various-models/libs/standard_sc.py index 0c91e5c0..64665071 100644 --- a/tests/e2e/models/poc-various-models/libs/standard_sc.py +++ b/tests/e2e/models/poc-various-models/libs/standard_sc.py @@ -171,7 +171,9 @@ definition=var("p") * param("emission_rate"), ), ], - objective_operational_contribution=(param("cost") * var("p")).time_sum().expec(), + objective_contributions={ + "operational": (param("cost") * var("p")).time_sum().expec() + }, ) """ @@ -279,14 +281,16 @@ var("Pgrad-s") >= var("withdrawal").shift(-1) - var("withdrawal"), ), ], - objective_operational_contribution=( - param("level_penality") * var("level") - + param("withdrawal_penality") * var("withdrawal") - + param("Pgrad+i_penality") * var("Pgrad+i") - + param("Pgrad-i_penality") * var("Pgrad-i") - + param("Pgrad+s_penality") * var("Pgrad+s") - + param("Pgrad-s_penality") * var("Pgrad-s") - ) - .time_sum() - .expec(), + objective_contributions={ + "operational": ( + param("level_penality") * var("level") + + param("withdrawal_penality") * var("withdrawal") + + param("Pgrad+i_penality") * var("Pgrad+i") + + param("Pgrad-i_penality") * var("Pgrad-i") + + param("Pgrad+s_penality") * var("Pgrad+s") + + param("Pgrad-s_penality") * var("Pgrad-s") + ) + .time_sum() + .expec() + }, ) diff --git a/tests/e2e/models/poc-various-models/test_ac_link.py b/tests/e2e/models/poc-various-models/test_ac_link.py index a4583532..15adc9e7 100644 --- a/tests/e2e/models/poc-various-models/test_ac_link.py +++ b/tests/e2e/models/poc-various-models/test_ac_link.py @@ -17,8 +17,17 @@ from gems.model.library import Library, library from gems.model.parsing import parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) @pytest.fixture @@ -38,7 +47,7 @@ def ac_lib(libs_dir: Path, std_lib: Library) -> dict[str, Library]: def test_ac_network_no_links(ac_lib: dict[str, Library]) -> None: """ - The network only has one AC node where a generator and a demand are connected. + The system only has one AC node where a generator and a demand are connected. There is actually no AC link connected to it, we just check that generation matches demand on this node: @@ -46,7 +55,7 @@ def test_ac_network_no_links(ac_lib: dict[str, Library]) -> None: - cost = 30 --> objective = 30 * 100 = 3000 """ - ac_node_model = ac_lib["ac"].models["ac-node"] + ac_node_model = ac_lib["ac"].models["ac.ac-node"] database = DataBase() database.add_data("D", "demand", ConstantData(100)) @@ -54,7 +63,7 @@ def test_ac_network_no_links(ac_lib: dict[str, Library]) -> None: database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", ConstantData(30)) - node = Node(model=ac_node_model, id="N") + node = Component(model=ac_node_model, id="N") demand = create_component( model=DEMAND_MODEL, id="D", @@ -65,24 +74,25 @@ def test_ac_network_no_links(ac_lib: dict[str, Library]) -> None: id="G", ) - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "injections")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "injections")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "injections")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "injections")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(3000, abs=0.01) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(3000, abs=0.01) def test_ac_network(ac_lib: dict[str, Library]) -> None: """ - The network only has 2 AC nodes connected by 1 AC link. + The system only has 2 AC nodes connected by 1 AC link. Node 1 carries the demand of 100 MW, node 2 carries the generator with a cost of 35 per MWh. @@ -90,8 +100,8 @@ def test_ac_network(ac_lib: dict[str, Library]) -> None: We check that final cost matches the demand: 100 * 35 = 3500, and that flow on the line is -100 MW. """ - ac_node_model = ac_lib["ac"].models["ac-node"] - ac_link_model = ac_lib["ac"].models["ac-link"] + ac_node_model = ac_lib["ac"].models["ac.ac-node"] + ac_link_model = ac_lib["ac"].models["ac.ac-link"] database = DataBase() database.add_data("D", "demand", ConstantData(100)) @@ -101,8 +111,8 @@ def test_ac_network(ac_lib: dict[str, Library]) -> None: database.add_data("L", "reactance", ConstantData(1)) - node1 = Node(model=ac_node_model, id="1") - node2 = Node(model=ac_node_model, id="2") + node1 = Component(model=ac_node_model, id="1") + node2 = Component(model=ac_node_model, id="2") demand = create_component( model=DEMAND_MODEL, id="D", @@ -118,32 +128,34 @@ def test_ac_network(ac_lib: dict[str, Library]) -> None: id="L", ) - network = Network("test") - network.add_node(node1) - network.add_node(node2) - network.add_component(demand) - network.add_component(gen) - network.add_component(link) - network.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) - network.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) - network.connect(PortRef(link, "port1"), PortRef(node1, "links")) - network.connect(PortRef(link, "port2"), PortRef(node2, "links")) + system = System("test") + system.add_component(node1) + system.add_component(node2) + system.add_component(demand) + system.add_component(gen) + system.add_component(link) + system.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) + system.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) + system.connect(PortRef(link, "port1"), PortRef(node1, "links")) + system.connect(PortRef(link, "port2"), PortRef(node2, "links")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(3500, abs=0.01) - - assert OutputValues(problem).component("L").var("flow").value == pytest.approx( - -100, abs=0.01 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(3500, abs=0.01) + + df = SimulationTableBuilder().build(problem) + assert df.component("L").output("flow").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-100, abs=0.01) def test_parallel_ac_links(ac_lib: dict[str, Library]) -> None: """ - The network has 2 AC nodes connected by 2 parallel links, + The system has 2 AC nodes connected by 2 parallel links, where reactance is 1 for line L1, and 2 for line L2. We expect flow to be te twice bigger on L1 than on L2. @@ -153,8 +165,8 @@ def test_parallel_ac_links(ac_lib: dict[str, Library]) -> None: We check that final cost matches the demand: 100 * 35 = 3500, and that flow on L1 is -66. MW while flow on L2 is only -33.3 MW. """ - ac_node_model = ac_lib["ac"].models["ac-node"] - ac_link_model = ac_lib["ac"].models["ac-link"] + ac_node_model = ac_lib["ac"].models["ac.ac-node"] + ac_link_model = ac_lib["ac"].models["ac.ac-link"] database = DataBase() database.add_data("D", "demand", ConstantData(100)) @@ -165,8 +177,8 @@ def test_parallel_ac_links(ac_lib: dict[str, Library]) -> None: database.add_data("L1", "reactance", ConstantData(1)) database.add_data("L2", "reactance", ConstantData(2)) - node1 = Node(model=ac_node_model, id="1") - node2 = Node(model=ac_node_model, id="2") + node1 = Component(model=ac_node_model, id="1") + node2 = Component(model=ac_node_model, id="2") demand = create_component( model=DEMAND_MODEL, id="D", @@ -184,33 +196,35 @@ def test_parallel_ac_links(ac_lib: dict[str, Library]) -> None: id="L2", ) - network = Network("test") - network.add_node(node1) - network.add_node(node2) - network.add_component(demand) - network.add_component(gen) - network.add_component(link1) - network.add_component(link2) - network.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) - network.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) - network.connect(PortRef(link1, "port1"), PortRef(node1, "links")) - network.connect(PortRef(link1, "port2"), PortRef(node2, "links")) - network.connect(PortRef(link2, "port1"), PortRef(node1, "links")) - network.connect(PortRef(link2, "port2"), PortRef(node2, "links")) + system = System("test") + system.add_component(node1) + system.add_component(node2) + system.add_component(demand) + system.add_component(gen) + system.add_component(link1) + system.add_component(link2) + system.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) + system.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) + system.connect(PortRef(link1, "port1"), PortRef(node1, "links")) + system.connect(PortRef(link1, "port2"), PortRef(node2, "links")) + system.connect(PortRef(link2, "port1"), PortRef(node1, "links")) + system.connect(PortRef(link2, "port2"), PortRef(node2, "links")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(3500, abs=0.01) - - assert OutputValues(problem).component("L1").var("flow").value == pytest.approx( - -66.67, abs=0.01 - ) - assert OutputValues(problem).component("L2").var("flow").value == pytest.approx( - -33.33, abs=0.01 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(3500, abs=0.01) + + df = SimulationTableBuilder().build(problem) + assert df.component("L1").output("flow").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-66.67, abs=0.01) + assert df.component("L2").output("flow").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-33.33, abs=0.01) def test_parallel_ac_links_with_pst(ac_lib: dict[str, Library]) -> None: @@ -226,9 +240,9 @@ def test_parallel_ac_links_with_pst(ac_lib: dict[str, Library]) -> None: Objective value is 3500 (for generation) + 50 (for phase shift). """ - ac_node_model = ac_lib["ac"].models["ac-node"] - ac_link_model = ac_lib["ac"].models["ac-link-with-limit"] - pst_model = ac_lib["ac"].models["ac-link-with-pst"] + ac_node_model = ac_lib["ac"].models["ac.ac-node"] + ac_link_model = ac_lib["ac"].models["ac.ac-link-with-limit"] + pst_model = ac_lib["ac"].models["ac.ac-link-with-pst"] database = DataBase() database.add_data("D", "demand", ConstantData(100)) @@ -242,8 +256,8 @@ def test_parallel_ac_links_with_pst(ac_lib: dict[str, Library]) -> None: database.add_data("T", "flow_limit", ConstantData(50)) database.add_data("T", "phase_shift_cost", ConstantData(1)) - node1 = Node(model=ac_node_model, id="1") - node2 = Node(model=ac_node_model, id="2") + node1 = Component(model=ac_node_model, id="1") + node2 = Component(model=ac_node_model, id="2") demand = create_component( model=DEMAND_MODEL, id="D", @@ -261,33 +275,35 @@ def test_parallel_ac_links_with_pst(ac_lib: dict[str, Library]) -> None: id="T", ) - network = Network("test") - network.add_node(node1) - network.add_node(node2) - network.add_component(demand) - network.add_component(gen) - network.add_component(link1) - network.add_component(link2) - network.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) - network.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) - network.connect(PortRef(link1, "port1"), PortRef(node1, "links")) - network.connect(PortRef(link1, "port2"), PortRef(node2, "links")) - network.connect(PortRef(link2, "port1"), PortRef(node1, "links")) - network.connect(PortRef(link2, "port2"), PortRef(node2, "links")) + system = System("test") + system.add_component(node1) + system.add_component(node2) + system.add_component(demand) + system.add_component(gen) + system.add_component(link1) + system.add_component(link2) + system.connect(PortRef(demand, "balance_port"), PortRef(node1, "injections")) + system.connect(PortRef(gen, "balance_port"), PortRef(node2, "injections")) + system.connect(PortRef(link1, "port1"), PortRef(node1, "links")) + system.connect(PortRef(link1, "port2"), PortRef(node2, "links")) + system.connect(PortRef(link2, "port1"), PortRef(node1, "links")) + system.connect(PortRef(link2, "port2"), PortRef(node2, "links")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == pytest.approx(3550, abs=0.01) - - assert OutputValues(problem).component("L").var("flow").value == pytest.approx( - -50, abs=0.01 - ) - assert OutputValues(problem).component("T").var("flow").value == pytest.approx( - -50, abs=0.01 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) ) - assert OutputValues(problem).component("T").var( - "phase_shift" - ).value == pytest.approx(-50, abs=0.01) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == pytest.approx(3550, abs=0.01) + + df = SimulationTableBuilder().build(problem) + assert df.component("L").output("flow").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-50, abs=0.01) + assert df.component("T").output("flow").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-50, abs=0.01) + assert df.component("T").output("phase_shift").value( + time_index=0, scenario_index=0 + ) == pytest.approx(-50, abs=0.01) diff --git a/tests/e2e/models/poc-various-models/test_electrolyzer.py b/tests/e2e/models/poc-various-models/test_electrolyzer.py index 71cf634c..eb1c2179 100644 --- a/tests/e2e/models/poc-various-models/test_electrolyzer.py +++ b/tests/e2e/models/poc-various-models/test_electrolyzer.py @@ -25,7 +25,15 @@ ) from gems.model.port import PortFieldDefinition, PortFieldId from gems.simulation import TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) ELECTRICAL_PORT = PortType(id="electrical_port", fields=[PortField("flow")]) @@ -42,7 +50,7 @@ ) ELECTRICAL_GENERATOR_MODEL = model( - id="GEN", + id="ELECTRICAL_GEN", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", CONSTANT), @@ -57,9 +65,9 @@ definition=var("generation"), ) ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) H2_PORT = PortType(id="h2_port", fields=[PortField("flow")]) @@ -123,8 +131,8 @@ def test_electrolyzer() -> None: - elec_node = Node(model=ELECTRICAL_NODE_MODEL, id="1") - h2_node = Node(model=H2_NODE_MODEL, id="2") + elec_node = Component(model=ELECTRICAL_NODE_MODEL, id="1") + h2_node = Component(model=H2_NODE_MODEL, id="2") electric_gen = create_component( model=ELECTRICAL_GENERATOR_MODEL, @@ -148,24 +156,26 @@ def test_electrolyzer() -> None: database.add_data("G", "cost", ConstantData(30)) database.add_data("E", "efficiency", ConstantData(0.7)) - network = Network("test") - network.add_node(elec_node) - network.add_node(h2_node) - network.add_component(demand_h2) - network.add_component(electric_gen) - network.add_component(electrolyzer) - network.connect(PortRef(demand_h2, "h2_port"), PortRef(h2_node, "h2_port")) - network.connect(PortRef(h2_node, "h2_port"), PortRef(electrolyzer, "h2_port")) - network.connect( + system = System("test") + system.add_component(elec_node) + system.add_component(h2_node) + system.add_component(demand_h2) + system.add_component(electric_gen) + system.add_component(electrolyzer) + system.connect(PortRef(demand_h2, "h2_port"), PortRef(h2_node, "h2_port")) + system.connect(PortRef(h2_node, "h2_port"), PortRef(electrolyzer, "h2_port")) + system.connect( PortRef(elec_node, "electrical_port"), PortRef(electrolyzer, "electrical_port") ) - network.connect( + system.connect( PortRef(elec_node, "electrical_port"), PortRef(electric_gen, "electrical_port"), ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - assert status == problem.solver.OPTIMAL - assert problem.solver.Objective().Value() == 3000 + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert problem.objective_value == 3000 diff --git a/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs.py b/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs.py index 1663ff40..cf5eacfc 100644 --- a/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs.py +++ b/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs.py @@ -21,8 +21,17 @@ TWO_INPUTS_CONVERTOR_MODEL, ) -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) """ This file tests various modellings for an electrolyser with multiple inputs. The models are created in Python directly. @@ -59,15 +68,15 @@ def test_electrolyzer_n_inputs_1() -> None: total gaz production = flow_ep1 * alpha_ez1 + flow_ep2 * alpha_ez2 + flow_gp """ - elec_node_1 = Node(model=NODE_BALANCE_MODEL, id="e1") + elec_node_1 = Component(model=NODE_BALANCE_MODEL, id="e1") electric_prod_1 = create_component(model=GENERATOR_MODEL, id="ep1") electrolyzer1 = create_component(model=CONVERTOR_MODEL, id="ez1") - elec_node_2 = Node(model=NODE_BALANCE_MODEL, id="e2") + elec_node_2 = Component(model=NODE_BALANCE_MODEL, id="e2") electric_prod_2 = create_component(model=GENERATOR_MODEL, id="ep2") electrolyzer2 = create_component(model=CONVERTOR_MODEL, id="ez2") - gaz_node = Node(model=NODE_BALANCE_MODEL, id="g") + gaz_node = Component(model=NODE_BALANCE_MODEL, id="g") gaz_prod = create_component(model=GENERATOR_MODEL, id="gp") gaz_demand = create_component(model=DEMAND_MODEL, id="gd") @@ -85,56 +94,58 @@ def test_electrolyzer_n_inputs_1() -> None: database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_component(electric_prod_1) - network.add_component(electrolyzer1) - network.add_node(elec_node_2) - network.add_component(electric_prod_2) - network.add_component(electrolyzer2) - network.add_node(gaz_node) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(electric_prod_1) + system.add_component(electrolyzer1) + system.add_component(elec_node_2) + system.add_component(electric_prod_2) + system.add_component(electrolyzer2) + system.add_component(gaz_node) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + + system.connect( PortRef(electric_prod_1, "balance_port"), PortRef(elec_node_1, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_1, "balance_port"), PortRef(electrolyzer1, "FlowDI") ) - network.connect(PortRef(electrolyzer1, "FlowDO"), PortRef(gaz_node, "balance_port")) - network.connect( + system.connect(PortRef(electrolyzer1, "FlowDO"), PortRef(gaz_node, "balance_port")) + system.connect( PortRef(electric_prod_2, "balance_port"), PortRef(elec_node_2, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_2, "balance_port"), PortRef(electrolyzer2, "FlowDI") ) - network.connect(PortRef(electrolyzer2, "FlowDO"), PortRef(gaz_node, "balance_port")) - network.connect( + system.connect(PortRef(electrolyzer2, "FlowDO"), PortRef(gaz_node, "balance_port")) + system.connect( PortRef(gaz_node, "balance_port"), PortRef(gaz_demand, "balance_port") ) - network.connect( - PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port") - ) + system.connect(PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value - print(ep1_gen) - print(ep2_gen) - print(gp_gen) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 42) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1990) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1990) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 42, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_2() -> None: @@ -149,9 +160,9 @@ def test_electrolyzer_n_inputs_2() -> None: total gaz production = flow_ep1 * alpha1_ez + flow_ep2 * alpha2_ez + flow_gp """ - elec_node_1 = Node(model=NODE_BALANCE_MODEL, id="e1") - elec_node_2 = Node(model=NODE_BALANCE_MODEL, id="e2") - gaz_node = Node(model=NODE_BALANCE_MODEL, id="g") + elec_node_1 = Component(model=NODE_BALANCE_MODEL, id="e1") + elec_node_2 = Component(model=NODE_BALANCE_MODEL, id="e2") + gaz_node = Component(model=NODE_BALANCE_MODEL, id="g") electric_prod_1 = create_component(model=GENERATOR_MODEL, id="ep1") electric_prod_2 = create_component(model=GENERATOR_MODEL, id="ep2") @@ -176,54 +187,56 @@ def test_electrolyzer_n_inputs_2() -> None: database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + + system.connect( PortRef(electric_prod_1, "balance_port"), PortRef(elec_node_1, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_1, "balance_port"), PortRef(electrolyzer, "FlowDI1") ) - network.connect( + system.connect( PortRef(electric_prod_2, "balance_port"), PortRef(elec_node_2, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_2, "balance_port"), PortRef(electrolyzer, "FlowDI2") ) - network.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) - network.connect( + system.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) + system.connect( PortRef(gaz_node, "balance_port"), PortRef(gaz_demand, "balance_port") ) - network.connect( - PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port") - ) + system.connect(PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value - print(ep1_gen) - print(ep2_gen) - print(gp_gen) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 42) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1990) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1990) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 42, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_3() -> None: @@ -239,9 +252,9 @@ def test_electrolyzer_n_inputs_3() -> None: The result is different since we only have one alpha at 0.7 """ - elec_node_1 = Node(model=NODE_BALANCE_MODEL, id="e1") - elec_node_2 = Node(model=NODE_BALANCE_MODEL, id="e2") - gaz_node = Node(model=NODE_BALANCE_MODEL, id="g") + elec_node_1 = Component(model=NODE_BALANCE_MODEL, id="e1") + elec_node_2 = Component(model=NODE_BALANCE_MODEL, id="e2") + gaz_node = Component(model=NODE_BALANCE_MODEL, id="g") electric_prod_1 = create_component(model=GENERATOR_MODEL, id="ep1") electric_prod_2 = create_component(model=GENERATOR_MODEL, id="ep2") @@ -268,57 +281,62 @@ def test_electrolyzer_n_inputs_3() -> None: database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - network.add_component(consumption_electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + system.add_component(consumption_electrolyzer) + + system.connect( PortRef(electric_prod_1, "balance_port"), PortRef(elec_node_1, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_1, "balance_port"), PortRef(consumption_electrolyzer, "FlowDI1"), ) - network.connect( + system.connect( PortRef(electric_prod_2, "balance_port"), PortRef(elec_node_2, "balance_port") ) - network.connect( + system.connect( PortRef(elec_node_2, "balance_port"), PortRef(consumption_electrolyzer, "FlowDI2"), ) - network.connect( + system.connect( PortRef(consumption_electrolyzer, "FlowDO"), PortRef(electrolyzer, "FlowDI") ) - network.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) - network.connect( + system.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) + system.connect( PortRef(gaz_node, "balance_port"), PortRef(gaz_demand, "balance_port") ) - network.connect( - PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port") - ) + system.connect(PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 30) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1750) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1750) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 30, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_4() -> None: @@ -334,9 +352,9 @@ def test_electrolyzer_n_inputs_4() -> None: same as test 3, the result is different than the first two since we only have one alpha at 0.7 """ - elec_node_1 = Node(model=NODE_BALANCE_MODEL_MOD, id="e1") - elec_node_2 = Node(model=NODE_BALANCE_MODEL_MOD, id="e2") - gaz_node = Node(model=NODE_BALANCE_MODEL, id="g") + elec_node_1 = Component(model=NODE_BALANCE_MODEL_MOD, id="e1") + elec_node_2 = Component(model=NODE_BALANCE_MODEL_MOD, id="e2") + gaz_node = Component(model=NODE_BALANCE_MODEL, id="g") electric_prod_1 = create_component(model=GENERATOR_MODEL, id="ep1") electric_prod_2 = create_component(model=GENERATOR_MODEL, id="ep2") @@ -360,50 +378,54 @@ def test_electrolyzer_n_inputs_4() -> None: database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + + system.connect( PortRef(electric_prod_1, "balance_port"), PortRef(elec_node_1, "balance_port_n") ) - network.connect( + system.connect( PortRef(elec_node_1, "balance_port_e"), PortRef(electrolyzer, "FlowDI") ) - network.connect( + system.connect( PortRef(electric_prod_2, "balance_port"), PortRef(elec_node_2, "balance_port_n") ) - network.connect( + system.connect( PortRef(elec_node_2, "balance_port_e"), PortRef(electrolyzer, "FlowDI") ) - network.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) - network.connect( + system.connect(PortRef(electrolyzer, "FlowDO"), PortRef(gaz_node, "balance_port")) + system.connect( PortRef(gaz_node, "balance_port"), PortRef(gaz_demand, "balance_port") ) - network.connect( - PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port") - ) + system.connect(PortRef(gaz_prod, "balance_port"), PortRef(gaz_node, "balance_port")) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 30) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1750) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1750) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 30, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) diff --git a/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs_yaml.py b/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs_yaml.py index 78bfc5a0..f838ef49 100644 --- a/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs_yaml.py +++ b/tests/e2e/models/poc-various-models/test_electrolyzer_n_inputs_yaml.py @@ -13,8 +13,17 @@ import math from gems.model.library import Library -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) """ This file tests various modellings for an electrolyser with multiple inputs. The models are read from a YAML model file. @@ -54,20 +63,20 @@ def test_electrolyzer_n_inputs_1( """ - gen_model = lib_dict["basic"].models["generator"] - node_model = lib_dict["basic"].models["node"] - convertor_model = lib_dict_sc["basic"].models["convertor"] - demand_model = lib_dict["basic"].models["demand"] + gen_model = lib_dict["basic"].models["basic.generator"] + node_model = lib_dict["basic"].models["basic.node"] + convertor_model = lib_dict_sc["basic"].models["basic.convertor"] + demand_model = lib_dict["basic"].models["basic.demand"] - elec_node_1 = Node(model=node_model, id="e1") + elec_node_1 = Component(model=node_model, id="e1") electric_prod_1 = create_component(model=gen_model, id="ep1") electrolyzer1 = create_component(model=convertor_model, id="ez1") - elec_node_2 = Node(model=node_model, id="e2") + elec_node_2 = Component(model=node_model, id="e2") electric_prod_2 = create_component(model=gen_model, id="ep2") electrolyzer2 = create_component(model=convertor_model, id="ez2") - gaz_node = Node(model=node_model, id="g") + gaz_node = Component(model=node_model, id="g") gaz_prod = create_component(model=gen_model, id="gp") gaz_demand = create_component(model=demand_model, id="gd") @@ -85,62 +94,66 @@ def test_electrolyzer_n_inputs_1( database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_component(electric_prod_1) - network.add_component(electrolyzer1) - network.add_node(elec_node_2) - network.add_component(electric_prod_2) - network.add_component(electrolyzer2) - network.add_node(gaz_node) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(electric_prod_1) + system.add_component(electrolyzer1) + system.add_component(elec_node_2) + system.add_component(electric_prod_2) + system.add_component(electrolyzer2) + system.add_component(gaz_node) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + + system.connect( PortRef(electric_prod_1, "injection_port"), PortRef(elec_node_1, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_1, "injection_port"), PortRef(electrolyzer1, "input_port") ) - network.connect( + system.connect( PortRef(electrolyzer1, "output_port"), PortRef(gaz_node, "injection_port") ) - network.connect( + system.connect( PortRef(electric_prod_2, "injection_port"), PortRef(elec_node_2, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_2, "injection_port"), PortRef(electrolyzer2, "input_port") ) - network.connect( + system.connect( PortRef(electrolyzer2, "output_port"), PortRef(gaz_node, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_node, "injection_port"), PortRef(gaz_demand, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_prod, "injection_port"), PortRef(gaz_node, "injection_port") ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value - print(ep1_gen) - print(ep2_gen) - print(gp_gen) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 42) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1990) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1990) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 42, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_2( @@ -157,14 +170,14 @@ def test_electrolyzer_n_inputs_2( total gaz production = flow_ep1 * alpha1_ez + flow_ep2 * alpha2_ez + flow_gp """ - gen_model = lib_dict["basic"].models["generator"] - node_model = lib_dict["basic"].models["node"] - convertor_model = lib_dict_sc["basic"].models["two_input_convertor"] - demand_model = lib_dict["basic"].models["demand"] + gen_model = lib_dict["basic"].models["basic.generator"] + node_model = lib_dict["basic"].models["basic.node"] + convertor_model = lib_dict_sc["basic"].models["basic.two_input_convertor"] + demand_model = lib_dict["basic"].models["basic.demand"] - elec_node_1 = Node(model=node_model, id="e1") - elec_node_2 = Node(model=node_model, id="e2") - gaz_node = Node(model=node_model, id="g") + elec_node_1 = Component(model=node_model, id="e1") + elec_node_2 = Component(model=node_model, id="e2") + gaz_node = Component(model=node_model, id="g") electric_prod_1 = create_component(model=gen_model, id="ep1") electric_prod_2 = create_component(model=gen_model, id="ep2") @@ -189,58 +202,62 @@ def test_electrolyzer_n_inputs_2( database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + + system.connect( PortRef(electric_prod_1, "injection_port"), PortRef(elec_node_1, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_1, "injection_port"), PortRef(electrolyzer, "input_port1") ) - network.connect( + system.connect( PortRef(electric_prod_2, "injection_port"), PortRef(elec_node_2, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_2, "injection_port"), PortRef(electrolyzer, "input_port2") ) - network.connect( + system.connect( PortRef(electrolyzer, "output_port"), PortRef(gaz_node, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_node, "injection_port"), PortRef(gaz_demand, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_prod, "injection_port"), PortRef(gaz_node, "injection_port") ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value - print(ep1_gen) - print(ep2_gen) - print(gp_gen) + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 42) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1990) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1990) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 42, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_3( @@ -259,15 +276,17 @@ def test_electrolyzer_n_inputs_3( The result is different since we only have one alpha at 0.7 """ - gen_model = lib_dict["basic"].models["generator"] - node_model = lib_dict["basic"].models["node"] - convertor_model = lib_dict_sc["basic"].models["convertor"] - demand_model = lib_dict["basic"].models["demand"] - decompose_flow_model = lib_dict_sc["basic"].models["decompose_1_flow_into_2_flow"] + gen_model = lib_dict["basic"].models["basic.generator"] + node_model = lib_dict["basic"].models["basic.node"] + convertor_model = lib_dict_sc["basic"].models["basic.convertor"] + demand_model = lib_dict["basic"].models["basic.demand"] + decompose_flow_model = lib_dict_sc["basic"].models[ + "basic.decompose_1_flow_into_2_flow" + ] - elec_node_1 = Node(model=node_model, id="e1") - elec_node_2 = Node(model=node_model, id="e2") - gaz_node = Node(model=node_model, id="g") + elec_node_1 = Component(model=node_model, id="e1") + elec_node_2 = Component(model=node_model, id="e2") + gaz_node = Component(model=node_model, id="g") electric_prod_1 = create_component(model=gen_model, id="ep1") electric_prod_2 = create_component(model=gen_model, id="ep2") @@ -292,62 +311,69 @@ def test_electrolyzer_n_inputs_3( database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - network.add_component(consumption_electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + system.add_component(consumption_electrolyzer) + + system.connect( PortRef(electric_prod_1, "injection_port"), PortRef(elec_node_1, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_1, "injection_port"), PortRef(consumption_electrolyzer, "input_port1"), ) - network.connect( + system.connect( PortRef(electric_prod_2, "injection_port"), PortRef(elec_node_2, "injection_port"), ) - network.connect( + system.connect( PortRef(elec_node_2, "injection_port"), PortRef(consumption_electrolyzer, "input_port2"), ) - network.connect( + system.connect( PortRef(consumption_electrolyzer, "output_port"), PortRef(electrolyzer, "input_port"), ) - network.connect( + system.connect( PortRef(electrolyzer, "output_port"), PortRef(gaz_node, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_node, "injection_port"), PortRef(gaz_demand, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_prod, "injection_port"), PortRef(gaz_node, "injection_port") ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 30) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1750) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1750) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 30, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) def test_electrolyzer_n_inputs_4( @@ -366,15 +392,15 @@ def test_electrolyzer_n_inputs_4( same as test 3, the result is different than the first two since we only have one alpha at 0.7 """ - gen_model = lib_dict["basic"].models["generator"] - node_model = lib_dict["basic"].models["node"] - node_mod_model = lib_dict_sc["basic"].models["node_mod"] - convertor_model = lib_dict_sc["basic"].models["convertor_receive_in"] - demand_model = lib_dict["basic"].models["demand"] + gen_model = lib_dict["basic"].models["basic.generator"] + node_model = lib_dict["basic"].models["basic.node"] + node_mod_model = lib_dict_sc["basic"].models["basic.node_mod"] + convertor_model = lib_dict_sc["basic"].models["basic.convertor_receive_in"] + demand_model = lib_dict["basic"].models["basic.demand"] - elec_node_1 = Node(model=node_mod_model, id="e1") - elec_node_2 = Node(model=node_mod_model, id="e2") - gaz_node = Node(model=node_model, id="g") + elec_node_1 = Component(model=node_mod_model, id="e1") + elec_node_2 = Component(model=node_mod_model, id="e2") + gaz_node = Component(model=node_model, id="g") electric_prod_1 = create_component(model=gen_model, id="ep1") electric_prod_2 = create_component(model=gen_model, id="ep2") @@ -398,54 +424,60 @@ def test_electrolyzer_n_inputs_4( database.add_data("gp", "p_max", ConstantData(30)) database.add_data("gp", "cost", ConstantData(15)) - network = Network("test") - network.add_node(elec_node_1) - network.add_node(elec_node_2) - network.add_node(gaz_node) - network.add_component(electric_prod_1) - network.add_component(electric_prod_2) - network.add_component(gaz_prod) - network.add_component(gaz_demand) - network.add_component(electrolyzer) - - network.connect( + system = System("test") + system.add_component(elec_node_1) + system.add_component(elec_node_2) + system.add_component(gaz_node) + system.add_component(electric_prod_1) + system.add_component(electric_prod_2) + system.add_component(gaz_prod) + system.add_component(gaz_demand) + system.add_component(electrolyzer) + + system.connect( PortRef(electric_prod_1, "injection_port"), PortRef(elec_node_1, "injection_port_n"), ) - network.connect( + system.connect( PortRef(elec_node_1, "injection_port_e"), PortRef(electrolyzer, "input_port") ) - network.connect( + system.connect( PortRef(electric_prod_2, "injection_port"), PortRef(elec_node_2, "injection_port_n"), ) - network.connect( + system.connect( PortRef(elec_node_2, "injection_port_e"), PortRef(electrolyzer, "input_port") ) - network.connect( + system.connect( PortRef(electrolyzer, "output_port"), PortRef(gaz_node, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_node, "injection_port"), PortRef(gaz_demand, "injection_port") ) - network.connect( + system.connect( PortRef(gaz_prod, "injection_port"), PortRef(gaz_node, "injection_port") ) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - - output = OutputValues(problem) - ep1_gen = output.component("ep1").var("generation").value - ep2_gen = output.component("ep2").var("generation").value - gp_gen = output.component("gp").var("generation").value + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" - assert math.isclose(ep1_gen, 70) # type: ignore - assert math.isclose(ep2_gen, 30) # type: ignore - assert math.isclose(gp_gen, 30) # type: ignore + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 1750) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 1750) + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("ep1").output("generation").value(time_index=0, scenario_index=0), + 70, + ) + assert math.isclose( + df.component("ep2").output("generation").value(time_index=0, scenario_index=0), + 30, + ) + assert math.isclose( + df.component("gp").output("generation").value(time_index=0, scenario_index=0), + 30, + ) diff --git a/tests/e2e/models/poc-various-models/test_quota_co2.py b/tests/e2e/models/poc-various-models/test_quota_co2.py index ee3bc445..3f721988 100644 --- a/tests/e2e/models/poc-various-models/test_quota_co2.py +++ b/tests/e2e/models/poc-various-models/test_quota_co2.py @@ -19,8 +19,17 @@ from libs.standard import DEMAND_MODEL, LINK_MODEL, NODE_BALANCE_MODEL from libs.standard_sc import C02_POWER_MODEL, QUOTA_CO2_MODEL -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) def test_quota_co2() -> None: @@ -36,30 +45,30 @@ def test_quota_co2() -> None: QuotaCO2 Test of a generation of energy and co2 with a quota to limit the emission""" - n1 = Node(model=NODE_BALANCE_MODEL, id="N1") - n2 = Node(model=NODE_BALANCE_MODEL, id="N2") + n1 = Component(model=NODE_BALANCE_MODEL, id="N1") + n2 = Component(model=NODE_BALANCE_MODEL, id="N2") oil1 = create_component(model=C02_POWER_MODEL, id="Oil1") coal1 = create_component(model=C02_POWER_MODEL, id="Coal1") l12 = create_component(model=LINK_MODEL, id="L12") demand = create_component(model=DEMAND_MODEL, id="Demand") monQuotaCO2 = create_component(model=QUOTA_CO2_MODEL, id="QuotaCO2") - network = Network("test") - network.add_node(n1) - network.add_node(n2) - network.add_component(oil1) - network.add_component(coal1) - network.add_component(l12) - network.add_component(demand) - network.add_component(monQuotaCO2) + system = System("test") + system.add_component(n1) + system.add_component(n2) + system.add_component(oil1) + system.add_component(coal1) + system.add_component(l12) + system.add_component(demand) + system.add_component(monQuotaCO2) - network.connect(PortRef(demand, "balance_port"), PortRef(n2, "balance_port")) - network.connect(PortRef(n2, "balance_port"), PortRef(l12, "balance_port_from")) - network.connect(PortRef(l12, "balance_port_to"), PortRef(n1, "balance_port")) - network.connect(PortRef(n1, "balance_port"), PortRef(oil1, "FlowP")) - network.connect(PortRef(n2, "balance_port"), PortRef(coal1, "FlowP")) - network.connect(PortRef(oil1, "OutCO2"), PortRef(monQuotaCO2, "emissionCO2")) - network.connect(PortRef(coal1, "OutCO2"), PortRef(monQuotaCO2, "emissionCO2")) + system.connect(PortRef(demand, "balance_port"), PortRef(n2, "balance_port")) + system.connect(PortRef(n2, "balance_port"), PortRef(l12, "balance_port_from")) + system.connect(PortRef(l12, "balance_port_to"), PortRef(n1, "balance_port")) + system.connect(PortRef(n1, "balance_port"), PortRef(oil1, "FlowP")) + system.connect(PortRef(n2, "balance_port"), PortRef(coal1, "FlowP")) + system.connect(PortRef(oil1, "OutCO2"), PortRef(monQuotaCO2, "emissionCO2")) + system.connect(PortRef(coal1, "OutCO2"), PortRef(monQuotaCO2, "emissionCO2")) database = DataBase() database.add_data("Demand", "demand", ConstantData(100)) @@ -75,16 +84,21 @@ def test_quota_co2() -> None: database.add_data("QuotaCO2", "quota", ConstantData(150)) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - output = OutputValues(problem) - oil1_p = output.component("Oil1").var("p").value - coal1_p = output.component("Coal1").var("p").value - l12_flow = output.component("L12").var("flow").value + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 5500) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 5500) - assert math.isclose(oil1_p, 50) # type: ignore - assert math.isclose(coal1_p, 50) # type: ignore - assert math.isclose(l12_flow, -50) # type: ignore + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("Oil1").output("p").value(time_index=0, scenario_index=0), 50 + ) + assert math.isclose( + df.component("Coal1").output("p").value(time_index=0, scenario_index=0), 50 + ) + assert math.isclose( + df.component("L12").output("flow").value(time_index=0, scenario_index=0), -50 + ) diff --git a/tests/e2e/models/poc-various-models/test_quota_co2_yaml.py b/tests/e2e/models/poc-various-models/test_quota_co2_yaml.py index 812a75cc..0cafcc47 100644 --- a/tests/e2e/models/poc-various-models/test_quota_co2_yaml.py +++ b/tests/e2e/models/poc-various-models/test_quota_co2_yaml.py @@ -17,8 +17,17 @@ import math from gems.model.library import Library -from gems.simulation import OutputValues, TimeBlock, build_problem -from gems.study import ConstantData, DataBase, Network, Node, PortRef, create_component +from gems.simulation import TimeBlock, build_problem +from gems.simulation.simulation_table import SimulationTableBuilder +from gems.study import ( + Component, + ConstantData, + DataBase, + PortRef, + Study, + System, + create_component, +) def test_quota_co2( @@ -36,36 +45,36 @@ def test_quota_co2( QuotaCO2 Test of a generation of energy and co2 with a quota to limit the emission""" - gen_model = lib_dict_sc["basic"].models["generator_with_co2"] - node_model = lib_dict["basic"].models["node"] - quota_co2_model = lib_dict_sc["basic"].models["quota_co2"] - demand_model = lib_dict["basic"].models["demand"] - link_model = lib_dict_sc["basic"].models["link"] + gen_model = lib_dict_sc["basic"].models["basic.generator_with_co2"] + node_model = lib_dict["basic"].models["basic.node"] + quota_co2_model = lib_dict_sc["basic"].models["basic.quota_co2"] + demand_model = lib_dict["basic"].models["basic.demand"] + link_model = lib_dict_sc["basic"].models["basic.link"] - n1 = Node(model=node_model, id="N1") - n2 = Node(model=node_model, id="N2") + n1 = Component(model=node_model, id="N1") + n2 = Component(model=node_model, id="N2") oil1 = create_component(model=gen_model, id="Oil1") coal1 = create_component(model=gen_model, id="Coal1") l12 = create_component(model=link_model, id="L12") demand = create_component(model=demand_model, id="Demand") monQuotaCO2 = create_component(model=quota_co2_model, id="QuotaCO2") - network = Network("test") - network.add_node(n1) - network.add_node(n2) - network.add_component(oil1) - network.add_component(coal1) - network.add_component(l12) - network.add_component(demand) - network.add_component(monQuotaCO2) + system = System("test") + system.add_component(n1) + system.add_component(n2) + system.add_component(oil1) + system.add_component(coal1) + system.add_component(l12) + system.add_component(demand) + system.add_component(monQuotaCO2) - network.connect(PortRef(demand, "injection_port"), PortRef(n2, "injection_port")) - network.connect(PortRef(n2, "injection_port"), PortRef(l12, "injection_port_from")) - network.connect(PortRef(l12, "injection_port_to"), PortRef(n1, "injection_port")) - network.connect(PortRef(n1, "injection_port"), PortRef(oil1, "injection_port")) - network.connect(PortRef(n2, "injection_port"), PortRef(coal1, "injection_port")) - network.connect(PortRef(oil1, "co2_port"), PortRef(monQuotaCO2, "emission_port")) - network.connect(PortRef(coal1, "co2_port"), PortRef(monQuotaCO2, "emission_port")) + system.connect(PortRef(demand, "injection_port"), PortRef(n2, "injection_port")) + system.connect(PortRef(n2, "injection_port"), PortRef(l12, "injection_port_from")) + system.connect(PortRef(l12, "injection_port_to"), PortRef(n1, "injection_port")) + system.connect(PortRef(n1, "injection_port"), PortRef(oil1, "injection_port")) + system.connect(PortRef(n2, "injection_port"), PortRef(coal1, "injection_port")) + system.connect(PortRef(oil1, "co2_port"), PortRef(monQuotaCO2, "emission_port")) + system.connect(PortRef(coal1, "co2_port"), PortRef(monQuotaCO2, "emission_port")) database = DataBase() database.add_data("Demand", "demand", ConstantData(100)) @@ -81,16 +90,21 @@ def test_quota_co2( database.add_data("QuotaCO2", "quota", ConstantData(150)) scenarios = 1 - problem = build_problem(network, database, TimeBlock(1, [0]), scenarios) - status = problem.solver.Solve() + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), list(range(scenarios)) + ) + problem.solve(solver_name="highs") - output = OutputValues(problem) - oil1_p = output.component("Oil1").var("p").value - coal1_p = output.component("Coal1").var("p").value - l12_flow = output.component("L12").var("flow").value + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, 5500) - assert status == problem.solver.OPTIMAL - assert math.isclose(problem.solver.Objective().Value(), 5500) - assert math.isclose(oil1_p, 50) # type: ignore - assert math.isclose(coal1_p, 50) # type: ignore - assert math.isclose(l12_flow, -50) # type: ignore + df = SimulationTableBuilder().build(problem) + assert math.isclose( + df.component("Oil1").output("p").value(time_index=0, scenario_index=0), 50 + ) + assert math.isclose( + df.component("Coal1").output("p").value(time_index=0, scenario_index=0), 50 + ) + assert math.isclose( + df.component("L12").output("flow").value(time_index=0, scenario_index=0), -50 + ) diff --git a/tests/e2e/models/poc-various-models/test_short_term_storage_complex.py b/tests/e2e/models/poc-various-models/test_short_term_storage_complex.py index bef087dd..156daff3 100644 --- a/tests/e2e/models/poc-various-models/test_short_term_storage_complex.py +++ b/tests/e2e/models/poc-various-models/test_short_term_storage_complex.py @@ -9,13 +9,14 @@ ) from libs.standard_sc import SHORT_TERM_STORAGE_COMPLEX -from gems.simulation import BlockBorderManagement, TimeBlock, build_problem +from gems.simulation import TimeBlock, build_problem from gems.study import ( + Component, ConstantData, DataBase, - Network, - Node, PortRef, + Study, + System, TimeScenarioSeriesData, create_component, ) @@ -66,7 +67,7 @@ def short_term_storage_base(efficiency: float, horizon: int, result: int) -> Non database.add_data("STS1", "Pgrad+s_penality", ConstantData(0)) database.add_data("STS1", "Pgrad-s_penality", ConstantData(0)) - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") spillage = create_component(model=SPILLAGE_MODEL, id="S") unsupplied = create_component(model=UNSUPPLIED_ENERGY_MODEL, id="U") @@ -78,49 +79,27 @@ def short_term_storage_base(efficiency: float, horizon: int, result: int) -> Non id="STS1", ) - network = Network("test") - network.add_node(node) + system = System("test") + system.add_component(node) for component in [demand, short_term_storage, spillage, unsupplied]: - network.add_component(component) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect( + system.add_component(component) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect( PortRef(short_term_storage, "balance_port"), PortRef(node, "balance_port") ) - network.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(unsupplied, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(spillage, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(unsupplied, "balance_port"), PortRef(node, "balance_port")) problem = build_problem( - network, - database, + Study(system, database), time_blocks[0], - scenarios, - border_management=BlockBorderManagement.CYCLE, + list(range(scenarios)), ) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - - assert math.isclose(problem.solver.Objective().Value(), result) - - count_variables = 0 - for variable in problem.solver.variables(): - if "injection" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 100 - print(variable.name()) - print(variable.solution_value()) - elif "withdrawal" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 50 - print(variable.name()) - print(variable.solution_value()) - elif "level" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 1000 - print(variable.name()) - print(variable.solution_value()) - - assert count_variables == 3 * horizon + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, result) + + # TODO: update variable access database.add_data("STS1", "withdrawal_penality", ConstantData(0)) database.add_data("STS1", "level_penality", ConstantData(5)) @@ -129,31 +108,11 @@ def short_term_storage_base(efficiency: float, horizon: int, result: int) -> Non database.add_data("STS1", "Pgrad+s_penality", ConstantData(0)) database.add_data("STS1", "Pgrad-s_penality", ConstantData(0)) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - - assert math.isclose(problem.solver.Objective().Value(), result) + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, result) - count_variables = 0 - for variable in problem.solver.variables(): - if "injection" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 100 - print(variable.name()) - print(variable.solution_value()) - elif "withdrawal" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 50 - print(variable.name()) - print(variable.solution_value()) - elif "level" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 1000 - print(variable.name()) - print(variable.solution_value()) - - assert count_variables == 3 * horizon + # TODO: update variable access database.add_data("STS1", "withdrawal_penality", ConstantData(0)) database.add_data("STS1", "level_penality", ConstantData(0)) @@ -162,31 +121,11 @@ def short_term_storage_base(efficiency: float, horizon: int, result: int) -> Non database.add_data("STS1", "Pgrad+s_penality", ConstantData(0)) database.add_data("STS1", "Pgrad-s_penality", ConstantData(0)) - status = problem.solver.Solve() - - assert status == problem.solver.OPTIMAL - - assert math.isclose(problem.solver.Objective().Value(), result) - - count_variables = 0 - for variable in problem.solver.variables(): - if "injection" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 100 - print(variable.name()) - print(variable.solution_value()) - elif "withdrawal" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 50 - print(variable.name()) - print(variable.solution_value()) - elif "level" in variable.name(): - count_variables += 1 - assert 0 <= variable.solution_value() <= 1000 - print(variable.name()) - print(variable.solution_value()) - - assert count_variables == 3 * horizon + problem.solve(solver_name="highs") + assert problem.termination_condition == "optimal" + assert math.isclose(problem.objective_value, result) + + # TODO: update variable access def test_short_test_horizon_10() -> None: diff --git a/tests/e2e/integration/__init__.py b/tests/unittests/data/__init__.py similarity index 100% rename from tests/e2e/integration/__init__.py rename to tests/unittests/data/__init__.py diff --git a/tests/unittests/data/test_data.py b/tests/unittests/data/test_data.py new file mode 100644 index 00000000..6bbbcbae --- /dev/null +++ b/tests/unittests/data/test_data.py @@ -0,0 +1,264 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. +"""Unit tests for gems.study.data — data structures, converters, and DataBase.""" + +import io +from pathlib import Path +from typing import Optional + +import numpy as np +import pandas as pd +import pytest + +from gems.study.data import ( + ConstantData, + DataBase, + ScenarioSeriesData, + TimeScenarioSeriesData, + TimeSeriesData, + dataframe_to_scenario_series, + dataframe_to_time_series, + load_ts_from_file, +) +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import _build_data, build_data_base +from gems.study.scenario_builder import ScenarioBuilder + +# --------------------------------------------------------------------------- +# load_ts_from_file +# --------------------------------------------------------------------------- + + +def test_load_ts_from_file_txt(tmp_path: Path) -> None: + """Reads a whitespace-separated .txt file into a DataFrame.""" + (tmp_path / "series.txt").write_text("1 2 3\n4 5 6\n") + df = load_ts_from_file("series", tmp_path) + assert df.shape == (2, 3) + assert df.iloc[0, 0] == 1 + assert df.iloc[1, 2] == 6 + + +def test_load_ts_from_file_tsv(tmp_path: Path) -> None: + """Reads a tab-separated .tsv file when no .txt exists.""" + (tmp_path / "series.tsv").write_text("10\t20\t30\n40\t50\t60\n") + df = load_ts_from_file("series", tmp_path) + assert df.shape == (2, 3) + assert df.iloc[0, 1] == 20 + assert df.iloc[1, 2] == 60 + + +def test_load_ts_from_file_not_found_raises(tmp_path: Path) -> None: + """Raises FileNotFoundError when neither .txt nor .tsv exists.""" + with pytest.raises(FileNotFoundError): + load_ts_from_file("missing", tmp_path) + + +def test_load_ts_from_file_none_inputs_raises() -> None: + """Raises FileNotFoundError when inputs are None.""" + with pytest.raises(FileNotFoundError): + load_ts_from_file(None, None) + + +# --------------------------------------------------------------------------- +# dataframe_to_time_series +# --------------------------------------------------------------------------- + + +def test_dataframe_to_time_series_single_column() -> None: + df = pd.DataFrame([1.0, 2.0, 3.0]) + series = dataframe_to_time_series(df) + assert list(series) == [1.0, 2.0, 3.0] + + +def test_dataframe_to_time_series_multi_column_raises() -> None: + """Raises ValueError when the DataFrame has more than one column.""" + df = pd.DataFrame([[1.0, 2.0], [3.0, 4.0]]) + with pytest.raises(ValueError, match="exactly one column"): + dataframe_to_time_series(df) + + +# --------------------------------------------------------------------------- +# dataframe_to_scenario_series +# --------------------------------------------------------------------------- + + +def test_dataframe_to_scenario_series_single_row() -> None: + df = pd.DataFrame([[10.0, 20.0, 30.0]]) + arr = dataframe_to_scenario_series(df) + assert list(arr) == [10.0, 20.0, 30.0] + + +def test_dataframe_to_scenario_series_multi_row_raises() -> None: + """Raises ValueError when the DataFrame has more than one row.""" + df = pd.DataFrame([[1.0, 2.0], [3.0, 4.0]]) + with pytest.raises(ValueError, match="exactly one line"): + dataframe_to_scenario_series(df) + + +# --------------------------------------------------------------------------- +# Data structure get_value error paths +# --------------------------------------------------------------------------- + + +def test_time_series_data_requires_timestep() -> None: + """TimeSeriesData.get_value raises KeyError when timestep is None.""" + ts = TimeSeriesData(pd.Series([1.0, 2.0, 3.0])) + with pytest.raises(KeyError): + ts.get_value(None, None) + + +def test_scenario_series_data_requires_scenario() -> None: + """ScenarioSeriesData.get_value raises KeyError when scenario is None.""" + ss = ScenarioSeriesData(np.array([10.0, 20.0])) + with pytest.raises(KeyError): + ss.get_value([0], None) + + +def test_time_scenario_series_data_requires_timestep() -> None: + """TimeScenarioSeriesData.get_value raises KeyError when timestep is None.""" + df = pd.DataFrame([[1.0, 2.0], [3.0, 4.0]]) + tss = TimeScenarioSeriesData(df) + with pytest.raises(KeyError): + tss.get_value(None, np.array([0])) + + +def test_time_scenario_series_data_requires_scenario() -> None: + """TimeScenarioSeriesData.get_value raises KeyError when scenario is None.""" + df = pd.DataFrame([[1.0, 2.0], [3.0, 4.0]]) + tss = TimeScenarioSeriesData(df) + with pytest.raises(KeyError): + tss.get_value([0], None) + + +# --------------------------------------------------------------------------- +# _build_data error paths +# --------------------------------------------------------------------------- + + +def test_build_data_str_not_time_not_scenario_raises(tmp_path: Path) -> None: + """String param value with neither flag set raises ValueError.""" + (tmp_path / "series.txt").write_text("1 2\n") + with pytest.raises(ValueError, match="float value is expected"): + _build_data( + time_dependent=False, + scenario_dependent=False, + param_value="series", + timeseries_dir=tmp_path, + ) + + +def test_build_data_float_time_dependent_raises() -> None: + """Float param value with time_dependent=True raises ValueError.""" + with pytest.raises(ValueError, match="timeseries name is expected"): + _build_data( + time_dependent=True, + scenario_dependent=False, + param_value=5.0, + timeseries_dir=None, + ) + + +def test_build_data_float_scenario_dependent_raises() -> None: + """Float param value with scenario_dependent=True raises ValueError.""" + with pytest.raises(ValueError, match="timeseries name is expected"): + _build_data( + time_dependent=False, + scenario_dependent=True, + param_value=5.0, + timeseries_dir=None, + ) + + +# --------------------------------------------------------------------------- +# build_data_base — scenario_group priority +# --------------------------------------------------------------------------- + +_SYSTEM_WITH_PARAM_GROUP = """\ +system: + model-libraries: basic + components: + - id: G + model: basic.generator + scenario-group: component-group + parameters: + - id: cost + value: 0 + - id: p_max + scenario-dependent: true + scenario-group: param-group + value: series +""" + + +def test_build_data_base_param_group_overrides_component_group( + tmp_path: Path, +) -> None: + """Parameter-level scenario_group takes precedence over component-level group.""" + (tmp_path / "series.txt").write_text("100 200\n") + sb = ScenarioBuilder() + db = build_data_base( + parse_yaml_components(io.StringIO(_SYSTEM_WITH_PARAM_GROUP)), + tmp_path, + scenario_builder=sb, + ) + from gems.study.data import ComponentParameterIndex + + # p_max has param-group; cost has component-group (constant so group is irrelevant) + idx_pmax = ComponentParameterIndex("G", "p_max") + assert db._scenario_groups[idx_pmax] == "param-group" + + idx_cost = ComponentParameterIndex("G", "cost") + assert db._scenario_groups[idx_cost] == "component-group" + + +# --------------------------------------------------------------------------- +# DataBase.get_values — no ScenarioBuilder path +# --------------------------------------------------------------------------- + + +def test_database_get_values_no_builder_passes_mc_through() -> None: + """Without a ScenarioBuilder mc_scenarios are used as col indices directly.""" + db = DataBase() # no scenario_builder + db.add_data("C", "param", ScenarioSeriesData(np.array([10.0, 20.0, 30.0]))) + result = db.get_values("C", "param", timesteps=None, mc_scenarios=[2, 0, 1]) + assert list(result) == [30.0, 10.0, 20.0] + + +def test_database_get_values_mc_scenarios_none() -> None: + """mc_scenarios=None keeps cols=None (used for time-only or constant data).""" + db = DataBase() + db.add_data("C", "param", ConstantData(42.0)) + result = db.get_values("C", "param", timesteps=None, mc_scenarios=None) + assert result == 42.0 + + +# --------------------------------------------------------------------------- +# DataBase.get_value — scalar path +# --------------------------------------------------------------------------- + + +def test_database_get_value_constant_returns_scalar() -> None: + """get_value on ConstantData returns a plain float, not a numpy array.""" + db = DataBase() + db.add_data("C", "val", ConstantData(7.5)) + from gems.study.data import ComponentParameterIndex + + result = db.get_value(ComponentParameterIndex("C", "val"), 0, 0) + assert result == 7.5 + assert not isinstance(result, np.ndarray) + + +def test_database_get_data_missing_key_raises() -> None: + """get_data raises KeyError for an unknown (component, parameter) pair.""" + db = DataBase() + with pytest.raises(KeyError): + db.get_data("unknown", "param") diff --git a/tests/unittests/expressions/linear_expressions/test_linear_expressions.py b/tests/unittests/expressions/linear_expressions/test_linear_expressions.py deleted file mode 100644 index 4bde63ce..00000000 --- a/tests/unittests/expressions/linear_expressions/test_linear_expressions.py +++ /dev/null @@ -1,257 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from typing import Dict - -import pytest - -from gems.simulation.linear_expression import LinearExpression, Term, TermKey - - -@pytest.mark.parametrize( - "term, expected", - [ - ( - Term(-2.50, "c", "x", time_index=10, scenario_index=15), - "-2.5c.x[10,15]", - ), - ], -) -def test_printing_term(term: Term, expected: str) -> None: - assert str(term) == expected - - -@pytest.mark.parametrize( - "coeff, var_name, constant, expec_str", - [ - (0, "x", 0, "0"), - (1, "x", 0, "+c.x[0,0]"), - (1, "x", 1, "+c.x[0,0]+1"), - (3.7, "x", 1, "+3.7c.x[0,0]+1"), - (0, "x", 1, "+1"), - ], -) -def test_affine_expression_printing_should_reflect_required_formatting( - coeff: float, var_name: str, constant: float, expec_str: str -) -> None: - expr = LinearExpression([Term(coeff, "c", var_name, 0, 0)], constant) - assert str(expr) == expec_str - - -@pytest.mark.parametrize( - "lhs, rhs", - [ - (LinearExpression([], 1) + LinearExpression([], 3), LinearExpression([], 4)), - (LinearExpression([], 4) / LinearExpression([], 2), LinearExpression([], 2)), - (LinearExpression([], 4) * LinearExpression([], 2), LinearExpression([], 8)), - (LinearExpression([], 4) - LinearExpression([], 2), LinearExpression([], 2)), - ], -) -def test_constant_expressions(lhs: LinearExpression, rhs: LinearExpression) -> None: - assert lhs == rhs - - -@pytest.mark.parametrize( - "terms_dict, constant, exp_terms, exp_constant", - [ - ({"x": Term(0, "c", "x", 0, 0)}, 1, {}, 1), - ({"x": Term(1, "c", "x", 0, 0)}, 1, {"x": Term(1, "c", "x", 0, 0)}, 1), - ], -) -def test_instantiate_linear_expression_from_dict( - terms_dict: Dict[TermKey, Term], - constant: float, - exp_terms: Dict[str, Term], - exp_constant: float, -) -> None: - expr = LinearExpression(terms_dict, constant) - assert expr.terms == exp_terms - assert expr.constant == exp_constant - - -@pytest.mark.parametrize( - "e1, e2, expected", - [ - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 1), - LinearExpression([Term(5, "c", "x", 0, 0)], 2), - LinearExpression([Term(15, "c", "x", 0, 0)], 3), - ), - ( - LinearExpression([Term(10, "c1", "x", 0, 0)], 1), - LinearExpression([Term(5, "c2", "x", 0, 0)], 2), - LinearExpression([Term(10, "c1", "x", 0, 0), Term(5, "c2", "x", 0, 0)], 3), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "y", 0, 0)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "y", 0, 0)], 0), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "x", 1, 0)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "x", 1, 0)], 0), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "x", 0, 1)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(5, "c", "x", 0, 1)], 0), - ), - ], -) -def test_addition( - e1: LinearExpression, e2: LinearExpression, expected: LinearExpression -) -> None: - assert e1 + e2 == expected - - -def test_operation_that_leads_to_term_with_zero_coefficient_should_be_removed_from_terms() -> ( - None -): - e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 1) - e2 = LinearExpression([Term(10, "c", "x", 0, 0)], 2) - e3 = e2 - e1 - assert e3.terms == {} - - -@pytest.mark.parametrize( - "e1, e2, expected", - [ - ( - LinearExpression([Term(10, "c", "x", 1, 0)], 3), - LinearExpression([], 2), - LinearExpression([Term(20, "c", "x", 1, 0)], 6), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 1)], 3), - LinearExpression([], 1), - LinearExpression([Term(10, "c", "x", 0, 1)], 3), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 3), - LinearExpression(), - LinearExpression(), - ), - ], -) -def test_multiplication( - e1: LinearExpression, e2: LinearExpression, expected: LinearExpression -) -> None: - assert e1 * e2 == expected - assert e2 * e1 == expected - - -def test_multiplication_of_two_non_constant_terms_should_raise_value_error() -> None: - e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 0) - e2 = LinearExpression([Term(5, "c", "x", 0, 0)], 0) - with pytest.raises(ValueError) as exc: - _ = e1 * e2 - assert str(exc.value) == "Cannot multiply two non constant expression" - - -@pytest.mark.parametrize( - "e1, expected", - [ - ( - LinearExpression([Term(10, "c", "x", 1, 2)], 5), - LinearExpression([Term(-10, "c", "x", 1, 2)], -5), - ), - ], -) -def test_negation(e1: LinearExpression, expected: LinearExpression) -> None: - assert -e1 == expected - - -@pytest.mark.parametrize( - "e1, e2, expected", - [ - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 1), - LinearExpression([Term(5, "c", "x", 0, 0)], 2), - LinearExpression([Term(5, "c", "x", 0, 0)], -1), - ), - ( - LinearExpression([Term(10, "c1", "x", 0, 0)], 1), - LinearExpression([Term(5, "c2", "x", 0, 0)], 2), - LinearExpression( - [Term(10, "c1", "x", 0, 0), Term(-5, "c2", "x", 0, 0)], -1 - ), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "y", 0, 0)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "y", 0, 0)], 0), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "x", 1, 0)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "x", 1, 0)], 0), - ), - ( - LinearExpression([Term(10, "c", "x", 0, 0)], 0), - LinearExpression([Term(5, "c", "x", 0, 1)], 0), - LinearExpression([Term(10, "c", "x", 0, 0), Term(-5, "c", "x", 0, 1)], 0), - ), - ], -) -def test_substraction( - e1: LinearExpression, e2: LinearExpression, expected: LinearExpression -) -> None: - assert e1 - e2 == expected - - -@pytest.mark.parametrize( - "e1, e2, expected", - [ - ( - LinearExpression([Term(10, "c", "x", 0, 1)], 15), - LinearExpression([], 5), - LinearExpression([Term(2, "c", "x", 0, 1)], 3), - ), - ( - LinearExpression([Term(10, "c", "x", 1, 0)], 15), - LinearExpression([], 1), - LinearExpression([Term(10, "c", "x", 1, 0)], 15), - ), - ], -) -def test_division( - e1: LinearExpression, e2: LinearExpression, expected: LinearExpression -) -> None: - assert e1 / e2 == expected - - -def test_division_by_zero_sould_raise_zero_division_error() -> None: - e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 15) - e2 = LinearExpression() - with pytest.raises(ZeroDivisionError) as exc: - _ = e1 / e2 - assert str(exc.value) == "Cannot divide expression by zero" - - -def test_division_by_non_constant_expr_sould_raise_value_error() -> None: - e1 = LinearExpression([Term(10, "c", "x", 0, 0)], 15) - e2 = LinearExpression() - with pytest.raises(ValueError) as exc: - _ = e2 / e1 - assert str(exc.value) == "Cannot divide by a non constant expression" - - -def test_imul_preserve_identity() -> None: - # technical test to check the behaviour of reassigning "self" in imul operator: - # it did not preserve identity, which could lead to weird behaviour - e1 = LinearExpression([], 15) - e2 = e1 - e1 *= LinearExpression([], 2) - assert e1 == LinearExpression([], 30) - assert e2 == e1 - assert e2 is e1 diff --git a/tests/unittests/expressions/visitor/test_copy.py b/tests/unittests/expressions/visitor/test_copy.py index 3727a759..b4a1aead 100644 --- a/tests/unittests/expressions/visitor/test_copy.py +++ b/tests/unittests/expressions/visitor/test_copy.py @@ -24,51 +24,12 @@ from gems.expression.equality import expressions_equal from gems.expression.expression import ( AllTimeSumNode, - ComponentParameterNode, MultiplicationNode, TimeEvalNode, TimeShiftNode, ) -def test_copy_large_addition_is_linear() -> None: - """ - Copying an AdditionNode with T operands must be O(T), not O(T²). - - Before the fix, CopyVisitor inherited the default addition() from - ExpressionVisitorOperations which accumulated results with `res = res + o`. - Each call to __add__ flattens the AdditionNode by copying the accumulated - operand list, giving 1+2+...+(T-1) = O(T²) list copies in total. - - The fix overrides addition() in CopyVisitor to build AdditionNode directly - from a list comprehension, reducing the cost to O(T). - - We verify linearity by checking that the time ratio between T=10_000 and - T=1_000 stays below 20 (linear ≈ 10, quadratic ≈ 100). - """ - small_n = 1_000 - large_n = 10_000 - - small_node = AdditionNode([VariableNode(f"x{i}") for i in range(small_n)]) - large_node = AdditionNode([VariableNode(f"x{i}") for i in range(large_n)]) - - t0 = time.perf_counter() - copy_expression(small_node) - small_time = time.perf_counter() - t0 - - t0 = time.perf_counter() - copy_expression(large_node) - large_time = time.perf_counter() - t0 - - ratio = large_time / small_time - assert ratio < 20, ( - f"copy_expression scaling looks super-linear: " - f"T={small_n} took {small_time:.4f}s, " - f"T={large_n} took {large_time:.4f}s, " - f"ratio={ratio:.1f} (expected <20 for O(T))" - ) - - def test_copy_ast() -> None: ast = AllTimeSumNode( DivisionNode( @@ -77,7 +38,7 @@ def test_copy_ast() -> None: ), TimeShiftNode( MultiplicationNode(LiteralNode(1), VariableNode("x")), - ComponentParameterNode("comp1", "p"), + ParameterNode("p"), ), ), ) diff --git a/tests/unittests/expressions/visitor/test_evaluation.py b/tests/unittests/expressions/visitor/test_evaluation.py index efd110ff..8c555fcb 100644 --- a/tests/unittests/expressions/visitor/test_evaluation.py +++ b/tests/unittests/expressions/visitor/test_evaluation.py @@ -10,9 +10,6 @@ # # This file is part of the Antares project. -from dataclasses import dataclass, field -from typing import Dict - import pytest from gems.expression import ( @@ -33,52 +30,6 @@ visit, ) from gems.expression.equality import expressions_equal -from gems.expression.expression import ComponentParameterNode, ComponentVariableNode - - -@dataclass(frozen=True) -class ComponentValueKey: - component_id: str - variable_name: str - - -def comp_key(component_id: str, variable_name: str) -> ComponentValueKey: - return ComponentValueKey(component_id, variable_name) - - -@dataclass(frozen=True) -class ComponentEvaluationContext(ValueProvider): - """ - Simple value provider relying on dictionaries. - Does not support component variables/parameters. - """ - - variables: Dict[ComponentValueKey, float] = field(default_factory=dict) - parameters: Dict[ComponentValueKey, float] = field(default_factory=dict) - - def get_variable_value(self, name: str) -> float: - raise NotImplementedError() - - def get_parameter_value(self, name: str) -> float: - raise NotImplementedError() - - def get_component_variable_value(self, component_id: str, name: str) -> float: - return self.variables[comp_key(component_id, name)] - - def get_component_parameter_value(self, component_id: str, name: str) -> float: - return self.parameters[comp_key(component_id, name)] - - -def test_comp_parameter() -> None: - add_node = AdditionNode([LiteralNode(1), ComponentVariableNode("comp1", "x")]) - expr = DivisionNode(add_node, ComponentParameterNode("comp1", "p")) - - assert visit(expr, PrinterVisitor()) == "((1 + comp1.x) / comp1.p)" - - context = ComponentEvaluationContext( - variables={comp_key("comp1", "x"): 3}, parameters={comp_key("comp1", "p"): 4} - ) - assert visit(expr, EvaluationVisitor(context)) == 1 def test_ast() -> None: diff --git a/tests/unittests/expressions/visitor/test_indexing.py b/tests/unittests/expressions/visitor/test_indexing.py index 6a40f0d7..28d707d5 100644 --- a/tests/unittests/expressions/visitor/test_indexing.py +++ b/tests/unittests/expressions/visitor/test_indexing.py @@ -33,6 +33,9 @@ def get_parameter_structure(self, name: str) -> IndexingStructure: def get_variable_structure(self, name: str) -> IndexingStructure: return IndexingStructure(True, True) + def get_constraint_structure(self, name: str) -> IndexingStructure: + return IndexingStructure(True, True) + def test_shift() -> None: x = var("x") @@ -102,6 +105,9 @@ def get_parameter_structure(self, name: str) -> IndexingStructure: def get_variable_structure(self, name: str) -> IndexingStructure: return IndexingStructure(True, True) + def get_constraint_structure(self, name: str) -> IndexingStructure: + raise NotImplementedError() + provider = CustomStructureProvider() assert compute_indexation(expr, provider) == IndexingStructure(True, True) diff --git a/tests/unittests/expressions/visitor/test_linearization.py b/tests/unittests/expressions/visitor/test_linearization.py deleted file mode 100644 index d64741e1..00000000 --- a/tests/unittests/expressions/visitor/test_linearization.py +++ /dev/null @@ -1,220 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from unittest.mock import Mock - -import pytest - -from gems.expression import ExpressionNode, LiteralNode, literal, var -from gems.expression.expression import ( - ComponentVariableNode, - CurrentScenarioIndex, - TimeShift, - comp_param, - comp_var, - maximum, - minimum, - problem_var, -) -from gems.expression.indexing import IndexingStructureProvider -from gems.expression.indexing_structure import IndexingStructure -from gems.expression.operators_expansion import ( - ProblemDimensions, - ProblemIndex, - expand_operators, -) -from gems.simulation.linear_expression import LinearExpression, Term -from gems.simulation.linearize import ParameterGetter, linearize_expression - - -class AllTimeScenarioDependent(IndexingStructureProvider): - def get_parameter_structure(self, name: str) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_variable_structure(self, name: str) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return IndexingStructure(True, True) - - -P = comp_param("c", "p") -X = comp_var("c", "x") -Y = comp_var("c", "y") - - -def var_at(var: ComponentVariableNode, timestep, scenario) -> LinearExpression: - return LinearExpression( - terms=[ - Term( - 1, - var.component_id, - var.name, - time_index=timestep, - scenario_index=scenario, - ) - ], - constant=0, - ) - - -def X_at(t: int = 0, s: int = 0) -> LinearExpression: - return var_at(X, timestep=t, scenario=s) - - -def Y_at(t: int = 0, s: int = 0) -> LinearExpression: - return var_at(Y, timestep=t, scenario=s) - - -def constant(c: float) -> LinearExpression: - return LinearExpression([], c) - - -def evaluate_literal(node: ExpressionNode) -> int: - if isinstance(node, LiteralNode): - return int(node.value) - raise NotImplementedError("Can only evaluate literal nodes.") - - -def test_linearization_before_operator_substitution_raises_an_error() -> None: - x = var("x") - expr = x.variance() - - with pytest.raises( - ValueError, match="Scenario operators need to be expanded before linearization" - ): - linearize_expression(expr, timestep=0, scenario=0) - - -def _expand_and_linearize( - expr: ExpressionNode, - dimensions: ProblemDimensions, - index: ProblemIndex, - parameter_value_provider: ParameterGetter, -) -> LinearExpression: - expanded = expand_operators( - expr, dimensions, evaluate_literal, AllTimeScenarioDependent() - ) - return linearize_expression( - expanded, index.timestep, index.scenario, parameter_value_provider - ) - - -@pytest.mark.parametrize( - "expr,expected", - [ - ((5 * X + 3) / 2, constant(2.5) * X_at(t=0) + constant(1.5)), - ((X + Y).time_sum(), X_at(t=0) + Y_at(t=0) + X_at(t=1) + Y_at(t=1)), - (X.shift(-1).shift(+1), X_at(t=0)), - (X.shift(-1).time_sum(), X_at(t=-1) + X_at(t=0)), - (X.shift(-1).time_sum(-1, +1), X_at(t=-2) + X_at(t=-1) + X_at(t=0)), - (X.time_sum().shift(-1), X_at(t=-1) + X_at(t=0)), - (X.time_sum(-1, +1).shift(-1), X_at(t=-2) + X_at(t=-1) + X_at(t=0)), - (X.eval(2).time_sum(), X_at(t=2) + X_at(t=2)), - ((X + 2).time_sum(), X_at(t=0) + X_at(t=1) + constant(4)), - ((X + 2).time_sum(-1, 0), X_at(t=-1) + X_at(t=0) + constant(4)), - ((X + 2).time_sum(-1, 0), X_at(t=-1) + X_at(t=0) + constant(4)), - ], -) -def test_linearization_of_nested_time_operations( - expr: ExpressionNode, expected: LinearExpression -) -> None: - dimensions = ProblemDimensions(timesteps_count=2, scenarios_count=1) - index = ProblemIndex(timestep=0, scenario=0) - params = Mock(spec=ParameterGetter) - - assert _expand_and_linearize(expr, dimensions, index, params) == expected - - -def test_invalid_multiplication() -> None: - params = Mock(spec=ParameterGetter) - - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - expression = x * x - with pytest.raises(ValueError, match="constant"): - linearize_expression(expression, 0, 0, params) - - -def test_invalid_division() -> None: - params = Mock(spec=ParameterGetter) - - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - expression = literal(1) / x - with pytest.raises(ValueError, match="constant"): - linearize_expression(expression, 0, 0, params) - - -def test_floor_of_constant() -> None: - assert linearize_expression( - literal(2.7).floor(), timestep=0, scenario=0 - ) == constant(2) - - -def test_ceil_of_constant() -> None: - assert linearize_expression( - literal(2.3).ceil(), timestep=0, scenario=0 - ) == constant(3) - - -def test_max_of_constants() -> None: - assert linearize_expression( - maximum(literal(3), literal(5)), timestep=0, scenario=0 - ) == constant(5) - - -def test_min_of_constants() -> None: - assert linearize_expression( - minimum(literal(3), literal(5)), timestep=0, scenario=0 - ) == constant(3) - - -def test_floor_of_variable_raises() -> None: - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - with pytest.raises(ValueError, match="non-constant"): - linearize_expression(x.floor(), timestep=0, scenario=0) - - -def test_ceil_of_variable_raises() -> None: - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - with pytest.raises(ValueError, match="non-constant"): - linearize_expression(x.ceil(), timestep=0, scenario=0) - - -def test_max_with_variable_raises() -> None: - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - with pytest.raises(ValueError, match="non-constant"): - linearize_expression(maximum(x, literal(5)), timestep=0, scenario=0) - - -def test_min_with_variable_raises() -> None: - x = problem_var( - "c", "x", time_index=TimeShift(0), scenario_index=CurrentScenarioIndex() - ) - with pytest.raises(ValueError, match="non-constant"): - linearize_expression(minimum(x, literal(5)), timestep=0, scenario=0) diff --git a/tests/unittests/expressions/visitor/test_operators_expansion.py b/tests/unittests/expressions/visitor/test_operators_expansion.py deleted file mode 100644 index 40f8862e..00000000 --- a/tests/unittests/expressions/visitor/test_operators_expansion.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from dataclasses import dataclass -from typing import Dict - -import pytest - -from gems.expression import ExpressionNode, LiteralNode -from gems.expression.equality import expressions_equal -from gems.expression.expression import ( - CurrentScenarioIndex, - NoScenarioIndex, - NoTimeIndex, - ProblemParameterNode, - ProblemVariableNode, - TimeShift, - TimeStep, - comp_param, - comp_var, - problem_param, - problem_var, -) -from gems.expression.indexing import IndexingStructureProvider -from gems.expression.indexing_structure import IndexingStructure -from gems.expression.operators_expansion import ProblemDimensions, expand_operators - -P = comp_param("c", "p") -X = comp_var("c", "x") -CONST = comp_var("c", "const") - - -def shifted_P(t: int = 0) -> ProblemParameterNode: - return problem_param("c", "p", TimeShift(t), CurrentScenarioIndex()) - - -def P_at(t: int = 0) -> ProblemParameterNode: - return problem_param("c", "p", TimeStep(t), CurrentScenarioIndex()) - - -def X_at(t: int = 0) -> ProblemVariableNode: - return problem_var("c", "x", TimeStep(t), CurrentScenarioIndex()) - - -def shifted_X(t: int = 0) -> ProblemVariableNode: - return problem_var("c", "x", TimeShift(t), CurrentScenarioIndex()) - - -def const() -> ProblemVariableNode: - return problem_var("c", "x", NoTimeIndex(), NoScenarioIndex()) - - -def evaluate_literal(node: ExpressionNode) -> int: - if isinstance(node, LiteralNode): - return int(node.value) - raise NotImplementedError("Can only evaluate literal nodes.") - - -@dataclass(frozen=True) -class AllTimeScenarioDependent(IndexingStructureProvider): - def get_parameter_structure(self, name: str) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_variable_structure(self, name: str) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return IndexingStructure(True, True) - - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return IndexingStructure(True, True) - - -@dataclass(frozen=True) -class StructureProviderDict(IndexingStructureProvider): - """ - Defines indexing structure through dictionaries. Default is time-scenario dependent. - """ - - variables: Dict[str, IndexingStructure] - parameters: Dict[str, IndexingStructure] - - def get_parameter_structure(self, name: str) -> IndexingStructure: - return self.parameters.get(name, IndexingStructure(True, True)) - - def get_variable_structure(self, name: str) -> IndexingStructure: - return self.variables.get(name, IndexingStructure(True, True)) - - def get_component_variable_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return self.variables.get(name, IndexingStructure(True, True)) - - def get_component_parameter_structure( - self, component_id: str, name: str - ) -> IndexingStructure: - return self.parameters.get(name, IndexingStructure(True, True)) - - -@pytest.mark.parametrize( - "expr,expected", - [ - (X.time_sum(), X_at(0) + X_at(1)), - (X.shift(-1), shifted_X(-1)), - (X.time_sum(-2, 0), shifted_X(-2) + (shifted_X(-1) + shifted_X(0))), - ((P * X).shift(-1), shifted_P(-1) * shifted_X(-1)), - (X.shift(-1).shift(+1), shifted_X(0)), - ( - P * (P * X).time_sum(0, 1), - shifted_P(0) * (shifted_P(0) * shifted_X(0) + shifted_P(1) * shifted_X(1)), - ), - (X.eval(2).time_sum(), X_at(2) + X_at(2)), - ], -) -def test_operators_expansion(expr: ExpressionNode, expected: ExpressionNode) -> None: - expanded = expand_operators( - expr, ProblemDimensions(2, 1), evaluate_literal, AllTimeScenarioDependent() - ) - assert expressions_equal(expanded, expected) - - -def test_time_scenario_independent_var_has_no_time_or_scenario_index(): - structure_provider = StructureProviderDict( - parameters={}, variables={"const": IndexingStructure(False, False)} - ) - expr = (X + CONST).time_sum() - expanded = expand_operators( - expr, ProblemDimensions(2, 1), evaluate_literal, structure_provider - ) - assert expanded == X_at(0) + const() + X_at(1) + const() diff --git a/tests/unittests/expressions/visitor/test_parameter_resolver.py b/tests/unittests/expressions/visitor/test_parameter_resolver.py deleted file mode 100644 index 1dd20d12..00000000 --- a/tests/unittests/expressions/visitor/test_parameter_resolver.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -from gems.expression import ParameterValueProvider, param, resolve_parameters, var - - -def test_parameters_resolution() -> None: - class TestParamProvider(ParameterValueProvider): - def get_component_parameter_value(self, component_id: str, name: str) -> float: - raise NotImplementedError() - - def get_parameter_value(self, name: str) -> float: - return 2 - - x = var("x") - p = param("p") - expr = (5 * x + 3) / p - assert resolve_parameters(expr, TestParamProvider()) == (5 * x + 3) / 2 diff --git a/tests/unittests/lib_parsing/test_lib_parsing.py b/tests/unittests/lib_parsing/test_lib_parsing.py index 23492fb2..7cc307b7 100644 --- a/tests/unittests/lib_parsing/test_lib_parsing.py +++ b/tests/unittests/lib_parsing/test_lib_parsing.py @@ -9,6 +9,7 @@ # SPDX-License-Identifier: MPL-2.0 # # This file is part of the Antares project. +import io from pathlib import Path import pytest @@ -22,6 +23,7 @@ ModelPort, PortField, PortType, + ValueType, float_parameter, float_variable, model, @@ -48,9 +50,9 @@ def test_library_parsing(libs_dir: Path) -> None: assert len(lib[input_lib.id].port_types) == 1 port_type = lib[input_lib.id].port_types["flow"] assert port_type == PortType(id="flow", fields=[PortField(name="flow")]) - gen_model = lib[input_lib.id].models["generator"] + gen_model = lib[input_lib.id].models["basic.generator"] assert gen_model == model( - id="generator", + id="basic.generator", parameters=[ float_parameter("cost", structure=CONSTANT), float_parameter("p_max", structure=CONSTANT), @@ -71,9 +73,9 @@ def test_library_parsing(libs_dir: Path) -> None: "operational": (param("cost") * var("generation")).time_sum().expec() }, ) - short_term_storage = lib[input_lib.id].models["short-term-storage"] + short_term_storage = lib[input_lib.id].models["basic.short-term-storage"] assert short_term_storage == model( - id="short-term-storage", + id="basic.short-term-storage", parameters=[ float_parameter("efficiency", structure=CONSTANT), float_parameter("level_min", structure=CONSTANT), @@ -119,6 +121,22 @@ def test_library_parsing(libs_dir: Path) -> None: ) +def test_binary_variable_parsing() -> None: + yaml_content = """ +library: + id: test + models: + - id: binary_model + variables: + - id: on_off + variable-type: binary +""" + input_lib = parse_yaml_library(io.StringIO(yaml_content)) + lib = resolve_library([input_lib]) + on_off = lib["test"].models["test.binary_model"].variables["on_off"] + assert on_off.data_type == ValueType.BINARY + + def test_library_error_parsing(libs_dir: Path) -> None: lib_file = libs_dir / "model_port_definition_ko.yml" @@ -142,9 +160,9 @@ def test_library_port_model_ok_parsing(libs_dir: Path) -> None: lib = resolve_library([input_lib]) port_type = lib[input_lib.id].port_types["flow"] assert port_type == PortType(id="flow", fields=[PortField(name="flow")]) - short_term_storage = lib[input_lib.id].models["short-term-storage-2"] + short_term_storage = lib[input_lib.id].models["basic.short-term-storage-2"] assert short_term_storage == model( - id="short-term-storage-2", + id="basic.short-term-storage-2", parameters=[ float_parameter("p_max_withdrawal", structure=CONSTANT), float_parameter("p_max_injection", structure=CONSTANT), diff --git a/tests/unittests/lib_parsing/test_objective_contribution.py b/tests/unittests/lib_parsing/test_objective_contribution.py deleted file mode 100644 index 18b48991..00000000 --- a/tests/unittests/lib_parsing/test_objective_contribution.py +++ /dev/null @@ -1,507 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# SPDX-License-Identifier: MPL-2.0 - -"""Tests for objective creation logic and coefficient accumulation.""" - -from typing import Any, Dict, Optional -from unittest.mock import Mock, patch - -import ortools.linear_solver.pywraplp as lp -import pytest - -# --- Mocks for External Dependencies --- - - -class TermKey: - """Minimal mock for the TermKey.""" - - def __init__(self, component_id: str, variable_name: str, scenario: int = 0): - self.component_id = component_id - self.variable_name = variable_name - self.scenario = scenario - - def __eq__(self, other: object) -> bool: - if not isinstance(other, TermKey): - return NotImplemented - return self.__dict__ == other.__dict__ - - def __hash__(self) -> int: - return hash((self.component_id, self.variable_name, self.scenario)) - - -class Term: - """Minimal mock for a single term in LinearExpression.""" - - def __init__(self, key: TermKey, coefficient: float): - self.key = key - self.coefficient = coefficient - - -class LinearExpression: - """Minimal mock for a linearized expression.""" - - def __init__(self, constant: float, terms: Dict[TermKey, Term]): - self.constant = constant - self.terms = terms - - -class Component: - """Minimal mock for a component.""" - - def __init__( - self, - id: str, - objective_contributions: Optional[Dict[str, LinearExpression]] = None, - ): - self.id = id - self.model = Mock() - self.model.objective_contributions = objective_contributions - - -class ComponentVariableKey: - """Minimal mock for variable key used by the context.""" - - def __init__( - self, - component_id: str, - variable_name: str, - scenario: int = 0, - block_timestep: int = 0, - ): - self.component_id = component_id - self.variable_name = variable_name - self.scenario = scenario - self.block_timestep = block_timestep - - def name(self) -> str: - return f"{self.component_id}_{self.variable_name}_s{self.scenario}_t{self.block_timestep}" - - def __eq__(self, other: object) -> bool: - if not isinstance(other, ComponentVariableKey): - return NotImplemented - return self.__dict__ == other.__dict__ - - def __hash__(self) -> int: - return hash(self.name()) - - -class MockSolverVariable: - """Mocks an lp.Variable wrapper for the context.""" - - def __init__(self, name: str, real_lp_var: lp.Variable): - self._name = name - self.is_in_objective = False - self.mock_lp_var = real_lp_var - - def name(self) -> str: - return self._name - - -def _setup_mock_optimization_environment( - linear_expressions: Dict[Any, LinearExpression], -) -> Any: - """Sets up a mock context and problem with real OR-Tools solver objects.""" - - mock_problem = Mock() - mock_problem.solver = lp.Solver.CreateSolver("GLOP") - solver = mock_problem.solver - - mock_lp_vars: Dict[str, lp.Variable] = {} - mock_context_vars: Dict[str, MockSolverVariable] = {} - - all_term_keys = set() - for linear_expr in linear_expressions.values(): - if isinstance(linear_expr, LinearExpression): - all_term_keys.update(linear_expr.terms.keys()) - - for term_key in all_term_keys: - comp_var_key = ComponentVariableKey( - term_key.component_id, term_key.variable_name, term_key.scenario - ) - name = comp_var_key.name() - - real_lp_var = solver.NumVar(0, solver.infinity(), name) - mock_lp_vars[name] = real_lp_var - - mock_context_vars[name] = MockSolverVariable(name, real_lp_var) - - opt_context = Mock() - opt_context._solver_variables = mock_context_vars - mock_problem.context = opt_context - - def mock_get_solver_var(term: Term, context: Any) -> lp.Variable: - key = ComponentVariableKey( - term.key.component_id, term.key.variable_name, term.key.scenario - ) - return mock_lp_vars[key.name()] - - opt_context._mock_get_solver_var = mock_get_solver_var - - linear_expressions_copy = linear_expressions.copy() - - def mock_linearize_expression(expanded_expr: Any) -> LinearExpression: - if expanded_expr in linear_expressions_copy: - return linear_expressions_copy.pop(expanded_expr) - - if isinstance(expanded_expr, Mock): - key_to_pop = next( - k - for k, v in linear_expressions_copy.items() - if isinstance(k, Mock) and k == expanded_expr - ) - return linear_expressions_copy.pop(key_to_pop) - - raise ValueError( - f"Expression {expanded_expr} not found for mock linearization." - ) - - opt_context.linearize_expression.side_effect = mock_linearize_expression - - return mock_problem, opt_context, mock_lp_vars, mock_get_solver_var - - -# --- Helper Functions (Mimicking OptimizationContext logic) --- - - -@patch( - "gems.simulation.optimization._instantiate_model_expression", - new=Mock(side_effect=lambda expr, *args: expr), -) -def _create_objective( - solver: lp.Solver, - opt_context: Any, - component: Component, - objective_contribution: LinearExpression, -) -> None: - """Helper to create a single objective from a LinearExpression.""" - instantiated_expr = objective_contribution - opt_context.expand_operators.return_value = instantiated_expr - expanded = opt_context.expand_operators(instantiated_expr) - linear_expr = opt_context.linearize_expression(expanded) - - with patch( - "gems.simulation.optimization._get_solver_var", - new=opt_context._mock_get_solver_var, - ): - _add_linear_expression_to_objective(solver, opt_context, linear_expr) - - -def _add_linear_expression_to_objective( - solver: lp.Solver, - context: Any, - linear_expr: LinearExpression, - existing_obj: Optional[lp.Objective] = None, -) -> lp.Objective: - """Helper to accumulate a LinearExpression to the objective.""" - obj: lp.Objective = existing_obj if existing_obj is not None else solver.Objective() - - def _get_solver_var(term: Term, ctx: Any) -> lp.Variable: - return context._mock_get_solver_var(term, ctx) - - for term in linear_expr.terms.values(): - solver_var = _get_solver_var(term, context) - - obj.SetCoefficient( - solver_var, - obj.GetCoefficient(solver_var) + term.coefficient, - ) - - context._solver_variables[solver_var.name()].is_in_objective = True - - obj.SetOffset(linear_expr.constant + obj.offset()) - return obj - - -# ============================================================================== -# --- Tests Start Here --- -# ============================================================================== - - -@patch( - "gems.simulation.optimization._add_linear_expression_to_objective", - new=_add_linear_expression_to_objective, -) -class TestObjectiveContributions: - def test_objective_contribution_value_accumulation(self) -> None: - """ - Verifies that multiple objective contributions correctly accumulate into - a single OR-Tools objective. - - This test compares: - 1. A 'legacy' objective with a constant (20.0) and a single variable coefficient (10.0). - 2. A 'contribution' objective formed by summing two contributions (A: 7.0/3.0, B: 13.0/7.0). - In other words: - 1. 10x + 20 - 2. 3x + 7 - 7x + 13 - It asserts that the accumulated objective has the doubled values (20.0 constant, 10.0 coeff), - demonstrating correct merging logic for both offsets and coefficients. - """ - comp_id = "test_comp" - var_name = "X_invest" - term_key = TermKey(comp_id, var_name) - - # --- LEGACY SETUP (10.0 constant, 5.0 coeff) --- - legacy_expr = LinearExpression( - constant=20.0, terms={term_key: Term(term_key, 10.0)} - ) - - # --- CONTRIBUTION SETUP (Total 20.0 constant, 10.0 coeff) --- - contrib_a_mock = Mock() - contrib_b_mock = Mock() - contrib_a = LinearExpression( - constant=7.0, terms={term_key: Term(term_key, 3.0)} - ) - contrib_b = LinearExpression( - constant=13.0, terms={term_key: Term(term_key, 7.0)} - ) - - legacy_comp = Component(comp_id, objective_contributions=None) - contrib_comp = Component( - comp_id, - objective_contributions={ - "contrib_a": contrib_a_mock, - "contrib_b": contrib_b_mock, - }, - ) - - legacy_mock_expressions = {legacy_expr: legacy_expr} - legacy_problem, legacy_context, _, _ = _setup_mock_optimization_environment( - legacy_mock_expressions - ) - - contrib_mock_expressions = { - contrib_a_mock: contrib_a, - contrib_b_mock: contrib_b, - } - contrib_problem, contrib_context, _, _ = _setup_mock_optimization_environment( - contrib_mock_expressions - ) - - def run_legacy_creation(): - legacy_context.build_strategy.get_objectives.return_value = [legacy_expr] - _create_objective( - legacy_problem.solver, legacy_context, legacy_comp, legacy_expr - ) - return legacy_problem.solver.Objective() - - def run_contrib_creation(problem: Any, context: Any, component: Component): - main_objective: Optional[lp.Objective] = None - for _, expr in component.model.objective_contributions.items(): - instantiated = expr - context.expand_operators.return_value = instantiated - expanded = context.expand_operators(instantiated) - linear_expr = context.linearize_expression(expanded) - - with patch( - "gems.simulation.optimization._get_solver_var", - new=context._mock_get_solver_var, - ): - main_objective = _add_linear_expression_to_objective( - problem.solver, - context, - linear_expr, - existing_obj=main_objective, - ) - return main_objective - - # --- EXECUTE --- - legacy_obj = run_legacy_creation() - contrib_obj = run_contrib_creation( - contrib_problem, contrib_context, contrib_comp - ) - - # --- ASSERTIONS --- - assert contrib_obj is not None - - # 1. Check Constant Offset - assert legacy_obj.offset() == pytest.approx(20.0) - assert contrib_obj.offset() == pytest.approx(20.0) - - # 2. Check Variable Coefficient - solver_var_name = ComponentVariableKey(comp_id, var_name).name() - - # Get the correct solver variable instances for each environment - legacy_solver_var = legacy_context._solver_variables[ - solver_var_name - ].mock_lp_var - contrib_solver_var = contrib_context._solver_variables[ - solver_var_name - ].mock_lp_var - - assert legacy_obj.GetCoefficient(legacy_solver_var) == pytest.approx(10.0) - assert contrib_obj.GetCoefficient(contrib_solver_var) == pytest.approx(10.0) - - def test_investment_single_continuous_candidate(self) -> None: - """ - Verifies correct accumulation of production costs and investment costs for - a single continuous investment candidate. - - The test combines two contributions: - - prod_cost: terms for P_gen_a (45.0) and P_unsup_a (501.0). - - invest_cost: terms for I_cand_cont (400.0) and P_cand_cont (10.0). - - It asserts that the final objective contains all four unique terms with - the expected coefficients and a zero offset. - """ - comp_id = "node_a" - - term_gen = TermKey(comp_id, "P_gen_a") - term_unsup = TermKey(comp_id, "P_unsup_a") - term_invest_cand = TermKey(comp_id, "I_cand_cont") - term_prod_cand = TermKey(comp_id, "P_cand_cont") - - # --- Contributions as Linear Expressions --- - prod_cost_expr = LinearExpression( - constant=0.0, - terms={ - term_gen: Term(term_gen, 45.0), - term_unsup: Term(term_unsup, 501.0), - }, - ) - - invest_cost_expr = LinearExpression( - constant=0.0, - terms={ - term_invest_cand: Term(term_invest_cand, 400.0), - term_prod_cand: Term(term_prod_cand, 10.0), - }, - ) - - test_comp = Component( - comp_id, - objective_contributions={ - "prod_cost": prod_cost_expr, - "invest_cost": invest_cost_expr, - }, - ) - - mock_expressions = { - prod_cost_expr: prod_cost_expr, - invest_cost_expr: invest_cost_expr, - } - - problem, context, _, _ = _setup_mock_optimization_environment(mock_expressions) - context.expand_operators.side_effect = lambda expr: expr - - main_objective: Optional[lp.Objective] = None - for _, expr in test_comp.model.objective_contributions.items(): - linear_expr = context.linearize_expression(expr) - - with patch( - "gems.simulation.optimization._get_solver_var", - new=context._mock_get_solver_var, - ): - main_objective = _add_linear_expression_to_objective( - problem.solver, context, linear_expr, existing_obj=main_objective - ) - - # --- ASSERTIONS --- - assert main_objective is not None - assert main_objective.offset() == pytest.approx(0.0) - - def get_coeff(term_key: TermKey) -> float: - solver_var_name = ComponentVariableKey( - term_key.component_id, term_key.variable_name - ).name() - solver_var = context._solver_variables[solver_var_name].mock_lp_var - return main_objective.GetCoefficient(solver_var) - - assert get_coeff(term_gen) == pytest.approx(45.0) - assert get_coeff(term_unsup) == pytest.approx(501.0) - assert get_coeff(term_invest_cand) == pytest.approx(400.0) - assert get_coeff(term_prod_cand) == pytest.approx(10.0) - - def test_investment_two_candidates_discrete(self) -> None: - """ - Verifies the accumulation of costs involving two different investment candidates - (one continuous, one discrete), resulting from three separate contributions. - - The test combines three contributions: - - prod_cost: terms for P_gen_b (45.0) and P_unsup_b (501.0). - - cont_cost: terms for continuous investment (490.0) and continuous production (10.0). - - disc_cost: terms for discrete investment (200.0) and discrete production (10.0). - - It asserts that the final objective correctly contains coefficients for all six - unique optimization variables. - """ - comp_id = "node_b" - - term_gen = TermKey(comp_id, "P_gen_b") - term_unsup = TermKey(comp_id, "P_unsup_b") - term_invest_cont = TermKey(comp_id, "I_cand_cont") - term_prod_cont = TermKey(comp_id, "P_cand_cont") - term_invest_disc = TermKey(comp_id, "I_cand_disc") - term_prod_disc = TermKey(comp_id, "P_cand_disc") - - # --- Contributions as Linear Expressions --- - prod_cost_expr = LinearExpression( - constant=0.0, - terms={ - term_gen: Term(term_gen, 45.0), - term_unsup: Term(term_unsup, 501.0), - }, - ) - - cont_cost_expr = LinearExpression( - constant=0.0, - terms={ - term_invest_cont: Term(term_invest_cont, 490.0), - term_prod_cont: Term(term_prod_cont, 10.0), - }, - ) - - disc_cost_expr = LinearExpression( - constant=0.0, - terms={ - term_invest_disc: Term(term_invest_disc, 200.0), - term_prod_disc: Term(term_prod_disc, 10.0), - }, - ) - - test_comp = Component( - comp_id, - objective_contributions={ - "prod_cost": prod_cost_expr, - "cont_cost": cont_cost_expr, - "disc_cost": disc_cost_expr, - }, - ) - - mock_expressions = { - prod_cost_expr: prod_cost_expr, - cont_cost_expr: cont_cost_expr, - disc_cost_expr: disc_cost_expr, - } - - problem, context, _, _ = _setup_mock_optimization_environment(mock_expressions) - context.expand_operators.side_effect = lambda expr: expr - - main_objective: Optional[lp.Objective] = None - for _, expr in test_comp.model.objective_contributions.items(): - linear_expr = context.linearize_expression(expr) - - with patch( - "gems.simulation.optimization._get_solver_var", - new=context._mock_get_solver_var, - ): - main_objective = _add_linear_expression_to_objective( - problem.solver, context, linear_expr, existing_obj=main_objective - ) - - # --- ASSERTIONS --- - assert main_objective is not None - - def get_coeff(term_key: TermKey) -> float: - solver_var_name = ComponentVariableKey( - term_key.component_id, term_key.variable_name - ).name() - solver_var = context._solver_variables[solver_var_name].mock_lp_var - return main_objective.GetCoefficient(solver_var) - - assert get_coeff(term_gen) == pytest.approx(45.0) - assert get_coeff(term_unsup) == pytest.approx(501.0) - assert get_coeff(term_invest_cont) == pytest.approx(490.0) - assert get_coeff(term_prod_cont) == pytest.approx(10.0) - assert get_coeff(term_invest_disc) == pytest.approx(200.0) - assert get_coeff(term_prod_disc) == pytest.approx(10.0) diff --git a/tests/unittests/scenario_builder/series/scenariobuilder.dat b/tests/unittests/scenario_builder/series/scenariobuilder.dat new file mode 100644 index 00000000..5eefa376 --- /dev/null +++ b/tests/unittests/scenario_builder/series/scenariobuilder.dat @@ -0,0 +1,8 @@ +load, 0 = 1 +load, 1 = 2 +load, 2 = 1 +load, 3 = 2 +cost-group, 0 = 1 +cost-group, 1 = 1 +cost-group, 2 = 2 +cost-group, 3 = 2 diff --git a/tests/unittests/scenario_builder/systems/with_scenarization.yml b/tests/unittests/scenario_builder/systems/with_scenarization.yml index 0f0e558e..594a9192 100644 --- a/tests/unittests/scenario_builder/systems/with_scenarization.yml +++ b/tests/unittests/scenario_builder/systems/with_scenarization.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/unittests/scenario_builder/test_scenario_builder.py b/tests/unittests/scenario_builder/test_scenario_builder.py index 750612a1..e347fcdd 100644 --- a/tests/unittests/scenario_builder/test_scenario_builder.py +++ b/tests/unittests/scenario_builder/test_scenario_builder.py @@ -12,13 +12,14 @@ from pathlib import Path -import pandas as pd +import numpy as np import pytest from gems.study import DataBase from gems.study.data import ComponentParameterIndex -from gems.study.parsing import parse_scenario_builder, parse_yaml_components -from gems.study.resolve_components import build_scenarized_data_base +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import build_data_base +from gems.study.scenario_builder import ScenarioBuilder @pytest.fixture(scope="session") @@ -26,46 +27,56 @@ def series_dir() -> Path: return Path(__file__).parent / "series" -@pytest.fixture -def scenario_builder(series_dir: Path) -> pd.DataFrame: - buider_path = series_dir / "scenario_builder.csv" - return parse_scenario_builder(buider_path) +@pytest.fixture(scope="session") +def scenario_builder(series_dir: Path) -> ScenarioBuilder: + return ScenarioBuilder.load(series_dir / "scenariobuilder.dat") @pytest.fixture -def database(series_dir: Path, scenario_builder: pd.DataFrame) -> DataBase: +def database(series_dir: Path, scenario_builder: ScenarioBuilder) -> DataBase: system_path = Path(__file__).parent / "systems/with_scenarization.yml" with system_path.open() as components: - return build_scenarized_data_base( - parse_yaml_components(components), scenario_builder, series_dir + return build_data_base( + parse_yaml_components(components), series_dir, scenario_builder ) -def test_parser(scenario_builder: pd.DataFrame) -> None: - builder = pd.DataFrame( - { - "name": [ - "load", - "load", - "load", - "load", - "cost-group", - "cost-group", - "cost-group", - "cost-group", - ], - "year": [0, 1, 2, 3, 0, 1, 2, 3], - "scenario": [0, 1, 0, 1, 0, 0, 1, 1], - } - ) - - assert builder.equals(scenario_builder) - - -# cost-group group isnt use in following test because sum can't take time dependant parameters -def test_scenarized_data_base(database: DataBase) -> None: +def test_scenario_builder_load(scenario_builder: ScenarioBuilder) -> None: + """ScenarioBuilder.load() parses the .dat file into correct 0-based col_idx arrays.""" + mc = np.array([0, 1, 2, 3], dtype=int) + assert list(scenario_builder.resolve_vectorized("load", mc)) == [0, 1, 0, 1] + assert list(scenario_builder.resolve_vectorized("cost-group", mc)) == [0, 0, 1, 1] + + +def test_data_base_with_scenario_builder(database: DataBase) -> None: load_index = ComponentParameterIndex("D", "demand") + # MC scenario 0 → col 0 (value 50), MC scenario 1 → col 1 (value 100), etc. assert database.get_value(load_index, 0, 0) == 50 assert database.get_value(load_index, 0, 1) == 100 assert database.get_value(load_index, 0, 2) == 50 assert database.get_value(load_index, 0, 3) == 100 + + +def test_resolve_vectorized_subset_playlist(scenario_builder: ScenarioBuilder) -> None: + """A subset (playlist) of MC scenarios resolves to the correct columns.""" + mc = np.array([0, 2], dtype=int) # skip scenarios 1 and 3 + assert list(scenario_builder.resolve_vectorized("load", mc)) == [0, 0] + assert list(scenario_builder.resolve_vectorized("cost-group", mc)) == [0, 1] + + +def test_resolve_vectorized_out_of_bounds_raises( + scenario_builder: ScenarioBuilder, +) -> None: + """Requesting an MC scenario index beyond the defined range raises ValueError.""" + mc = np.array([0, 99], dtype=int) + with pytest.raises(ValueError, match="not defined for group"): + scenario_builder.resolve_vectorized("load", mc) + + +def test_resolve_vectorized_unknown_group_raises( + scenario_builder: ScenarioBuilder, +) -> None: + """Requesting an unknown scenario group raises ValueError.""" + mc = np.array([0, 1], dtype=int) + with pytest.raises(ValueError, match="not defined in the scenario builder"): + scenario_builder.resolve_vectorized("nonexistent-group", mc) diff --git a/tests/unittests/scenario_builder/test_scenario_builder_dispatch.py b/tests/unittests/scenario_builder/test_scenario_builder_dispatch.py new file mode 100644 index 00000000..1ab7c7d5 --- /dev/null +++ b/tests/unittests/scenario_builder/test_scenario_builder_dispatch.py @@ -0,0 +1,78 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +from pathlib import Path + +import numpy as np +import pytest + +from gems.study.parsing import parse_yaml_components +from gems.study.resolve_components import build_data_base +from gems.study.scenario_builder import ScenarioBuilder + + +@pytest.fixture(scope="session") +def dispatch_series_dir(tmp_path_factory: pytest.TempPathFactory) -> Path: + """Series directory with 3-column loads and a scenariobuilder.dat.""" + d = tmp_path_factory.mktemp("dispatch_series") + # 1 row × 3 columns: col 0 = 10, col 1 = 20, col 2 = 30 + (d / "loads.txt").write_text("10 20 30\n") + # MC scenario 0 → column 3 (1-based) → col_idx 2 → value 30 + # MC scenario 1 → column 1 (1-based) → col_idx 0 → value 10 + # MC scenario 2 → column 2 (1-based) → col_idx 1 → value 20 + (d / "scenariobuilder.dat").write_text( + "load, 0 = 3\n" "load, 1 = 1\n" "load, 2 = 2\n" + ) + return d + + +@pytest.fixture(scope="session") +def dispatch_system_yml() -> str: + return """\ +system: + model-libraries: basic + components: + - id: D + model: basic.demand + scenario-group: load + parameters: + - id: demand + scenario-dependent: true + value: loads +""" + + +def test_scenario_builder_load(dispatch_series_dir: Path) -> None: + """ScenarioBuilder.load() parses the .dat file into correct 0-based col_idx arrays.""" + sb = ScenarioBuilder.load(dispatch_series_dir / "scenariobuilder.dat") + mc = np.array([0, 1, 2], dtype=int) + cols = sb.resolve_vectorized("load", mc) + assert list(cols) == [2, 0, 1] + + +def test_dispatch_mc_scenarios_to_columns( + dispatch_series_dir: Path, dispatch_system_yml: str +) -> None: + """DataBase.get_values() dispatches each MC scenario to the correct data column.""" + sb = ScenarioBuilder.load(dispatch_series_dir / "scenariobuilder.dat") + + import io + + db = build_data_base( + parse_yaml_components(io.StringIO(dispatch_system_yml)), + dispatch_series_dir, + scenario_builder=sb, + ) + + result = db.get_values("D", "demand", timesteps=None, mc_scenarios=[0, 1, 2]) + # MC scenario 0 → col 2 → 30, scenario 1 → col 0 → 10, scenario 2 → col 1 → 20 + assert list(result) == [30, 10, 20] diff --git a/tests/unittests/simulation/test_generate_network_on_tree.py b/tests/unittests/simulation/test_generate_network_on_tree.py deleted file mode 100644 index 0bf21f83..00000000 --- a/tests/unittests/simulation/test_generate_network_on_tree.py +++ /dev/null @@ -1,101 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# -# See AUTHORS.txt -# -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. -# -# SPDX-License-Identifier: MPL-2.0 -# -# This file is part of the Antares project. - -import pytest - -from gems.simulation import TimeBlock -from gems.simulation.decision_tree import ( - DecisionTreeNode, - InterDecisionTimeScenarioConfig, -) -from gems.study.network import Network - - -def test_decision_tree_generation() -> None: - scenarios = 1 - blocks = [TimeBlock(1, [0])] - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - - network = Network("network_id") - root = DecisionTreeNode("root", config, network) - - assert root.id == "root" - assert root.parent is None - assert root.prob == 1.0 - assert not root.children # No children - - child = DecisionTreeNode("child", config, network, parent=root, prob=0.8) - - assert child.parent == root - assert child.prob == 0.8 - assert child in root.children - - grandchild = DecisionTreeNode("grandchild", config, network, parent=child, prob=0.6) - - assert grandchild.parent == child - assert grandchild.prob == (0.8 * 0.6) - assert (grandchild not in root.children) and (grandchild in child.children) - - with pytest.raises(ValueError, match="Probability must be a value in the range"): - great_grandchild = DecisionTreeNode( - "greatgrandchild", config, network, parent=grandchild, prob=2.0 - ) - - with pytest.raises(ValueError, match="Probability must be a value in the range"): - great_grandchild = DecisionTreeNode( - "greatgrandchild", config, network, parent=grandchild, prob=-0.3 - ) - - -def test_decision_tree_probabilities() -> None: - scenarios = 1 - blocks = [TimeBlock(1, [0])] - config = InterDecisionTimeScenarioConfig(blocks, scenarios) - network = Network("network_id") - - """ - root (p = 1) - |- l_child (p = 0.7) - | |- ll_child (p = 0.5) - | | `- lll_child (p = 1) - | | - | `- lr_child (p = 0.5) - | - `- r_child (p = 0.3) - |- rl_child (p = 0.4) - `- rr_child (p = 0.5) - """ - - # Root - root = DecisionTreeNode("root", config, network) - - # 1st level - l_child = DecisionTreeNode("l_child", config, network, parent=root, prob=0.7) - r_child = DecisionTreeNode("r_child", config, network, parent=root, prob=0.3) - - # 2nd level - ll_child = DecisionTreeNode("ll_child", config, network, parent=l_child, prob=0.5) - lr_child = DecisionTreeNode("lr_child", config, network, parent=l_child, prob=0.5) - - rl_child = DecisionTreeNode("rl_child", config, network, parent=r_child, prob=0.4) - rr_child = DecisionTreeNode("rr_child", config, network, parent=r_child, prob=0.5) - - # 3rd level - lll_child = DecisionTreeNode("lll_child", config, network, parent=ll_child, prob=1) - - assert ll_child.is_leaves_prob_sum_one() # One child with p = 1 - - assert l_child.is_leaves_prob_sum_one() # Two children w/ p1 = p2 = 0.5 - - assert not r_child.is_leaves_prob_sum_one() # Two children w/ p1 + p2 != 1 - - assert not root.is_leaves_prob_sum_one() diff --git a/tests/unittests/simulation/test_output_values.py b/tests/unittests/simulation/test_output_values.py deleted file mode 100644 index 181bd44c..00000000 --- a/tests/unittests/simulation/test_output_values.py +++ /dev/null @@ -1,94 +0,0 @@ -# Copyright (c) 2024, RTE (https://www.rte-france.com) -# SPDX-License-Identifier: MPL-2.0 - -from unittest.mock import Mock, patch - -import ortools.linear_solver.pywraplp as lp - -from gems.simulation.optimization import ( - OptimizationContext, - OptimizationProblem, - TimestepComponentVariableKey, -) -from gems.simulation.output_values import OutputComponent, OutputValues - - -def test_component_and_flow_output_object() -> None: - mock_variable_component = Mock(spec=lp.Variable) - mock_variable_component.solution_value.side_effect = lambda: 1.0 - - opt_context = Mock(spec=OptimizationContext) - opt_context.get_all_component_variables.return_value = { - TimestepComponentVariableKey( - component_id="component_id_test", - variable_name="component_var_name", - block_timestep=0, - scenario=0, - ): mock_variable_component, - TimestepComponentVariableKey( - component_id="component_id_test", - variable_name="component_approx_var_name", - block_timestep=0, - scenario=0, - ): mock_variable_component, - } - opt_context.block_length.return_value = 1 - opt_context.network = Mock() - opt_context.network.all_components = [] - - mock_solver = Mock() - mock_solver.IsMip.return_value = False - - mock_problem = Mock(spec=OptimizationProblem) - mock_problem.context = opt_context - mock_problem.solver = mock_solver - - with patch.object( - OutputComponent, - "evaluate_extra_outputs", - return_value={}, - ): - actual_output = OutputValues(mock_problem) - - expected_output = OutputValues() - assert actual_output != expected_output - - expected_output.component("component_id_test").ignore = True - assert actual_output == expected_output - - expected_output.component("component_id_test").ignore = False - expected_output.component("component_id_test").var("component_var_name").value = 1.0 - expected_output.component("component_id_test").var( - "component_approx_var_name" - ).ignore = True - assert actual_output == expected_output - - expected_output.component("component_id_test").var( - "component_approx_var_name" - ).ignore = False - expected_output.component("component_id_test").var( - "component_approx_var_name" - ).value = 1.000_000_001 - assert actual_output != expected_output and not actual_output.is_close( - expected_output - ) - - expected_output.component("component_id_test").var( - "component_approx_var_name" - ).value = 1.000_000_000_1 - assert actual_output != expected_output and actual_output.is_close(expected_output) - - expected_output.component("component_id_test").var( - "component_approx_var_name" - ).ignore = True - expected_output.component("component_id_test").var( - "wrong_component_var_name" - ).value = 1.0 - assert actual_output != expected_output - - expected_output.component("component_id_test").var( - "wrong_component_var_name" - ).ignore = True - assert actual_output == expected_output - - print(actual_output) diff --git a/tests/unittests/simulation/test_shift_validity_visitor.py b/tests/unittests/simulation/test_shift_validity_visitor.py new file mode 100644 index 00000000..bdb1f1b5 --- /dev/null +++ b/tests/unittests/simulation/test_shift_validity_visitor.py @@ -0,0 +1,243 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +"""Unit tests for ShiftValidityVisitor and _ShiftAmountEvaluator.""" + +import numpy as np +import pytest +import xarray as xr + +from gems.expression.expression import literal, param, var +from gems.expression.visitor import visit +from gems.simulation.vectorized_builder import ShiftValidityVisitor + + +def _visitor(param_arrays=None, block_length=4): + return ShiftValidityVisitor( + model_id="m", + param_arrays=param_arrays or {}, + block_length=block_length, + ) + + +@pytest.fixture +def per_comp_lag(): + """Two components with lag values 1 and 2.""" + return { + ("m", "lag"): xr.DataArray( + [1.0, 2.0], + dims=["component"], + coords={"component": ["c1", "c2"]}, + ) + } + + +# --------------------------------------------------------------------------- +# Leaf nodes — must return None (no time-shift constraints) +# --------------------------------------------------------------------------- + + +def test_literal_returns_none(): + assert visit(literal(3.0), _visitor()) is None + + +def test_parameter_returns_none(per_comp_lag): + assert visit(param("lag"), _visitor(per_comp_lag)) is None + + +def test_variable_returns_none(): + assert visit(var("x"), _visitor()) is None + + +# --------------------------------------------------------------------------- +# Arithmetic over leaves — None propagates +# --------------------------------------------------------------------------- + + +def test_addition_of_params_returns_none(per_comp_lag): + expr = param("lag") + literal(1.0) + assert visit(expr, _visitor(per_comp_lag)) is None + + +def test_negation_of_param_returns_none(per_comp_lag): + expr = -param("lag") + assert visit(expr, _visitor(per_comp_lag)) is None + + +# --------------------------------------------------------------------------- +# time_shift with compile-time constant shift +# --------------------------------------------------------------------------- + + +def test_time_shift_negative_literal_blocks_t0(): + # shift = -1: t=0 accesses t-1 = -1, out of bounds + expr = var("x").shift(-1) + mask = visit(expr, _visitor(block_length=3)) + assert mask is not None + assert mask.dims == ("time",) + np.testing.assert_array_equal(mask.values, [False, True, True]) + + +def test_time_shift_positive_literal_blocks_last_t(): + # shift = +1: t=2 accesses t+1 = 3 >= 3, out of bounds (block=3) + expr = var("x").shift(1) + mask = visit(expr, _visitor(block_length=3)) + assert mask is not None + np.testing.assert_array_equal(mask.values, [True, True, False]) + + +def test_time_shift_zero_returns_all_true(): + expr = var("x").shift(0) + mask = visit(expr, _visitor(block_length=3)) + assert mask is not None + assert bool(mask.all()) + + +# --------------------------------------------------------------------------- +# time_shift with per-component parameter shift +# --------------------------------------------------------------------------- + + +def test_time_shift_per_component_param(per_comp_lag): + # shift = -lag: c1 lag=1 → shift=-1, c2 lag=2 → shift=-2 + # block=4: c1 invalid at t=0, c2 invalid at t=0 and t=1 + expr = var("x").shift(-param("lag")) + mask = visit(expr, _visitor(per_comp_lag, block_length=4)) + assert mask is not None + assert "time" in mask.dims + assert "component" in mask.dims + c1_mask = mask.sel(component="c1").values + c2_mask = mask.sel(component="c2").values + np.testing.assert_array_equal(c1_mask, [False, True, True, True]) + np.testing.assert_array_equal(c2_mask, [False, False, True, True]) + + +# --------------------------------------------------------------------------- +# time_sum with compile-time constant bounds +# --------------------------------------------------------------------------- + + +def test_time_sum_negative_from_blocks_t0(): + # sum from -1 to 0: t=0 accesses t-1 = -1, invalid; t=1,2,3 valid + expr = var("x").time_sum(-1, 0) + mask = visit(expr, _visitor(block_length=4)) + assert mask is not None + np.testing.assert_array_equal(mask.values, [False, True, True, True]) + + +def test_time_sum_positive_to_blocks_last_t(): + # sum from 0 to 1: t=3 accesses t+1=4 >= 4, invalid; t=0,1,2 valid + expr = var("x").time_sum(0, 1) + mask = visit(expr, _visitor(block_length=4)) + assert mask is not None + np.testing.assert_array_equal(mask.values, [True, True, True, False]) + + +# --------------------------------------------------------------------------- +# time_sum with per-component parameter bounds +# --------------------------------------------------------------------------- + + +def test_time_sum_per_component_param(per_comp_lag): + # sum from -lag to 0: c1 from=-1, c2 from=-2 + # block=4: c1 invalid at t=0; c2 invalid at t=0 and t=1 + expr = var("x").time_sum(-param("lag"), 0) + mask = visit(expr, _visitor(per_comp_lag, block_length=4)) + assert mask is not None + assert "component" in mask.dims + c1 = mask.sel(component="c1").values + c2 = mask.sel(component="c2").values + np.testing.assert_array_equal(c1, [False, True, True, True]) + np.testing.assert_array_equal(c2, [False, False, True, True]) + + +# --------------------------------------------------------------------------- +# Composition — AND of sub-tree masks +# --------------------------------------------------------------------------- + + +def test_nested_shifts_and_combined(): + # Two time_shift nodes in an addition: shift -1 and shift +1 + # shift -1 blocks t=0; shift +1 blocks t=2 (block=3) + # combined: t=0 and t=2 invalid, only t=1 valid + expr = var("x").shift(-1) + var("x").shift(1) + mask = visit(expr, _visitor(block_length=3)) + assert mask is not None + np.testing.assert_array_equal(mask.values, [False, True, False]) + + +def test_shift_inside_multiplication(): + # shift -1 inside a multiplication: same validity as bare shift -1 + expr = literal(2.0) * var("x").shift(-1) + mask = visit(expr, _visitor(block_length=3)) + assert mask is not None + np.testing.assert_array_equal(mask.values, [False, True, True]) + + +# --------------------------------------------------------------------------- +# Unevaluable shift — raises ValueError +# --------------------------------------------------------------------------- + + +def test_unevaluable_shift_raises(): + # shift = var("p") is a variable, not a parameter → unevaluable → error + shift_expr = var("p") + expr = var("x").shift(shift_expr) + with pytest.raises(ValueError, match="not evaluable"): + visit(expr, _visitor(block_length=4)) + + +# --------------------------------------------------------------------------- +# Time- or scenario-dependent parameter in shift — raises specific error +# --------------------------------------------------------------------------- + + +@pytest.fixture +def time_dependent_lag(): + """Lag parameter that varies over both component and time.""" + return { + ("m", "lag"): xr.DataArray( + [[1.0, 2.0, 1.0, 2.0], [2.0, 1.0, 2.0, 1.0]], + dims=["component", "time"], + coords={"component": ["c1", "c2"], "time": [0, 1, 2, 3]}, + ) + } + + +@pytest.fixture +def scenario_dependent_lag(): + """Lag parameter that varies over scenario.""" + return { + ("m", "lag"): xr.DataArray( + [1.0, 2.0], + dims=["scenario"], + coords={"scenario": ["s1", "s2"]}, + ) + } + + +def test_time_dependent_param_in_shift_raises(time_dependent_lag): + expr = var("x").shift(param("lag")) + with pytest.raises(ValueError, match="depends on time"): + visit(expr, _visitor(time_dependent_lag, block_length=4)) + + +def test_time_dependent_param_in_time_sum_bound_raises(time_dependent_lag): + expr = var("x").time_sum(-param("lag"), 0) + with pytest.raises(ValueError, match="depends on time"): + visit(expr, _visitor(time_dependent_lag, block_length=4)) + + +def test_scenario_dependent_param_in_shift_raises(scenario_dependent_lag): + expr = var("x").shift(param("lag")) + with pytest.raises(ValueError, match="depends on scenario"): + visit(expr, _visitor(scenario_dependent_lag, block_length=4)) diff --git a/tests/unittests/simulation/test_simulation_table_accessor.py b/tests/unittests/simulation/test_simulation_table_accessor.py new file mode 100644 index 00000000..ce0d953e --- /dev/null +++ b/tests/unittests/simulation/test_simulation_table_accessor.py @@ -0,0 +1,308 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# SPDX-License-Identifier: MPL-2.0 + +"""Tests for SimulationTable fluent accessor API (component / output / value).""" + +from dataclasses import dataclass, field +from typing import Optional + +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from gems.simulation.simulation_table import ( + ComponentView, + OutputView, + SimulationTable, + SimulationTableBuilder, +) + +# --------------------------------------------------------------------------- +# Minimal stubs (reused from test_simulation_table_mock.py) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class FakeBlock: + id: int = 1 + timesteps: tuple = (0, 1) + + +@dataclass +class FakeLinopyVar: + name: str + coords: dict + + +@dataclass +class FakeModel: + extra_outputs: dict = field(default_factory=dict) + + +@dataclass +class FakeStudy: + model_components: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) + + +@dataclass +class FakeLinopyModel: + solution: dict + + @property + def dual(self) -> xr.Dataset: + return xr.Dataset() + + solver_model = None + + +@dataclass +class FakeProblem: + block: FakeBlock = field(default_factory=FakeBlock) + block_length: int = 3 + objective_value: float = 0.0 + linopy_model: Optional[FakeLinopyModel] = None + _linopy_vars: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) + model_components: dict = field(default_factory=dict) + study: FakeStudy = field(default_factory=FakeStudy) + scenarios: int = 1 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _make_single_scenario_problem() -> FakeProblem: + """One component, two time steps, one scenario.""" + sol_da = xr.DataArray( + np.array([[[10.0], [20.0]]]), + dims=["component", "time", "scenario"], + coords={"component": ["compA"], "time": [0, 1], "scenario": [0]}, + ) + fake_var = FakeLinopyVar( + name="test_model__p", + coords={"component": xr.DataArray(["compA"])}, + ) + return FakeProblem( + block_length=2, + linopy_model=FakeLinopyModel(solution={"test_model__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + scenarios=1, + ) + + +def _make_multi_scenario_problem() -> FakeProblem: + """One component, two time steps, two scenarios.""" + # values[comp, time, scenario]: compA, t0s0=10, t0s1=11, t1s0=20, t1s1=21 + sol_da = xr.DataArray( + np.array([[[10.0, 11.0], [20.0, 21.0]]]), + dims=["component", "time", "scenario"], + coords={"component": ["compA"], "time": [0, 1], "scenario": [0, 1]}, + ) + fake_var = FakeLinopyVar( + name="test_model__p", + coords={"component": xr.DataArray(["compA"])}, + ) + return FakeProblem( + block_length=2, + linopy_model=FakeLinopyModel(solution={"test_model__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + scenarios=2, + ) + + +# --------------------------------------------------------------------------- +# Tests: return types +# --------------------------------------------------------------------------- + + +def test_build_returns_simulation_table() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + assert isinstance(st, SimulationTable) + + +def test_data_property_returns_dataframe() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + assert isinstance(st.data, pd.DataFrame) + + +def test_component_returns_component_view() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + assert isinstance(st.component("compA"), ComponentView) + + +def test_output_returns_output_view() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + assert isinstance(st.component("compA").output("p"), OutputView) + + +# --------------------------------------------------------------------------- +# Tests: value() with no arguments → full Time × Scenario DataFrame +# --------------------------------------------------------------------------- + + +def test_value_no_args_returns_dataframe_single_scenario() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + result = st.component("compA").output("p").value() + assert isinstance(result, pd.DataFrame) + assert result.shape == (2, 1) # 2 time steps × 1 scenario + assert list(result.index) == [0, 1] + assert list(result.columns) == [0] + + +def test_value_no_args_returns_dataframe_multi_scenario() -> None: + st = SimulationTableBuilder().build(_make_multi_scenario_problem()) # type: ignore[arg-type] + result = st.component("compA").output("p").value() + assert isinstance(result, pd.DataFrame) + assert result.shape == (2, 2) # 2 time steps × 2 scenarios + assert list(result.index) == [0, 1] + assert list(result.columns) == [0, 1] + + +# --------------------------------------------------------------------------- +# Tests: value(scenario_index=s) → Series over time +# --------------------------------------------------------------------------- + + +def test_value_scenario_index_returns_series() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + result = st.component("compA").output("p").value(scenario_index=0) + assert isinstance(result, pd.Series) + assert list(result.index) == [0, 1] + assert result.iloc[0] == pytest.approx(10.0) + assert result.iloc[1] == pytest.approx(20.0) + + +def test_value_scenario_index_multi_scenario() -> None: + st = SimulationTableBuilder().build(_make_multi_scenario_problem()) # type: ignore[arg-type] + s0 = st.component("compA").output("p").value(scenario_index=0) + s1 = st.component("compA").output("p").value(scenario_index=1) + assert s0.iloc[0] == pytest.approx(10.0) + assert s0.iloc[1] == pytest.approx(20.0) + assert s1.iloc[0] == pytest.approx(11.0) + assert s1.iloc[1] == pytest.approx(21.0) + + +# --------------------------------------------------------------------------- +# Tests: value(time_index=t) → Series over scenarios +# --------------------------------------------------------------------------- + + +def test_value_time_index_returns_series() -> None: + st = SimulationTableBuilder().build(_make_multi_scenario_problem()) # type: ignore[arg-type] + result = st.component("compA").output("p").value(time_index=0) + assert isinstance(result, pd.Series) + assert list(result.index) == [0, 1] + assert result.iloc[0] == pytest.approx(10.0) + assert result.iloc[1] == pytest.approx(11.0) + + +# --------------------------------------------------------------------------- +# Tests: value(time_index=t, scenario_index=s) → scalar float +# --------------------------------------------------------------------------- + + +def test_value_both_indices_returns_float() -> None: + st = SimulationTableBuilder().build(_make_multi_scenario_problem()) # type: ignore[arg-type] + view = st.component("compA").output("p") + assert view.value(time_index=0, scenario_index=0) == pytest.approx(10.0) + assert view.value(time_index=0, scenario_index=1) == pytest.approx(11.0) + assert view.value(time_index=1, scenario_index=0) == pytest.approx(20.0) + assert view.value(time_index=1, scenario_index=1) == pytest.approx(21.0) + + +def test_value_both_indices_single_scenario() -> None: + st = SimulationTableBuilder().build(_make_single_scenario_problem()) # type: ignore[arg-type] + val = st.component("compA").output("p").value(time_index=0, scenario_index=0) + assert isinstance(val, float) + assert val == pytest.approx(10.0) + + +# --------------------------------------------------------------------------- +# Tests: OutputView.data exposes the pivot DataFrame +# --------------------------------------------------------------------------- + + +def test_output_view_data_property() -> None: + st = SimulationTableBuilder().build(_make_multi_scenario_problem()) # type: ignore[arg-type] + view = st.component("compA").output("p") + df = view.data + assert isinstance(df, pd.DataFrame) + assert df.loc[0, 0] == pytest.approx(10.0) + + +# --------------------------------------------------------------------------- +# Tests: dimension-independent outputs are accessible via the fluent API +# --------------------------------------------------------------------------- + + +def _make_scenario_independent_problem() -> FakeProblem: + """One component, two time steps, NO scenario dimension.""" + sol_da = xr.DataArray( + np.array([[10.0, 20.0]]), + dims=["component", "time"], + coords={"component": ["compA"], "time": [0, 1]}, + ) + fake_var = FakeLinopyVar( + name="test_model__p", + coords={"component": xr.DataArray(["compA"])}, + ) + return FakeProblem( + block_length=2, + linopy_model=FakeLinopyModel(solution={"test_model__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + scenarios=1, + ) + + +def _make_scalar_output_problem() -> FakeProblem: + """One component, NO time dimension, NO scenario dimension.""" + sol_da = xr.DataArray( + np.array([99.0]), + dims=["component"], + coords={"component": ["compA"]}, + ) + fake_var = FakeLinopyVar( + name="test_model__p", + coords={"component": xr.DataArray(["compA"])}, + ) + return FakeProblem( + block_length=1, + linopy_model=FakeLinopyModel(solution={"test_model__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + scenarios=1, + ) + + +def test_scenario_independent_value_accessible_by_scenario_index() -> None: + """value(time_index=t, scenario_index=0) works even without a scenario dim.""" + st = SimulationTableBuilder().build(_make_scenario_independent_problem()) # type: ignore[arg-type] + assert st.component("compA").output("p").value( + time_index=0, scenario_index=0 + ) == pytest.approx(10.0) + assert st.component("compA").output("p").value( + time_index=1, scenario_index=0 + ) == pytest.approx(20.0) + + +def test_scalar_output_accessible_via_fluent_api() -> None: + """value(time_index=0, scenario_index=0) works for a fully scalar output.""" + st = SimulationTableBuilder().build(_make_scalar_output_problem()) # type: ignore[arg-type] + assert st.component("compA").output("p").value( + time_index=0, scenario_index=0 + ) == pytest.approx(99.0) diff --git a/tests/unittests/simulation/test_simulation_table_export.py b/tests/unittests/simulation/test_simulation_table_export.py new file mode 100644 index 00000000..75c3459d --- /dev/null +++ b/tests/unittests/simulation/test_simulation_table_export.py @@ -0,0 +1,214 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# SPDX-License-Identifier: MPL-2.0 + +"""Tests for SimulationTable.to_dataset(), write_parquet(), and write_netcdf().""" + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional + +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from gems.simulation.simulation_table import ( + SimulationColumns, + SimulationTable, + SimulationTableBuilder, +) + +# --------------------------------------------------------------------------- +# Minimal stubs (shared with other simulation table tests) +# --------------------------------------------------------------------------- + + +@dataclass(frozen=True) +class FakeBlock: + id: int = 1 + timesteps: tuple = (0, 1) + + +@dataclass +class FakeLinopyVar: + name: str + coords: dict + + +@dataclass +class FakeModel: + extra_outputs: dict = field(default_factory=dict) + + +@dataclass +class FakeStudy: + model_components: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) + + +@dataclass +class FakeLinopyModel: + solution: dict + + @property + def dual(self) -> xr.Dataset: + return xr.Dataset() + + solver_model = None + + +@dataclass +class FakeProblem: + block: FakeBlock = field(default_factory=FakeBlock) + block_length: int = 2 + objective_value: float = 99.0 + linopy_model: Optional[FakeLinopyModel] = None + _linopy_vars: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) + model_components: dict = field(default_factory=dict) + study: FakeStudy = field(default_factory=FakeStudy) + scenarios: int = 1 + + +def _make_problem(n_scenarios: int = 1) -> FakeProblem: + """Two time steps, configurable number of scenarios, one component.""" + values = np.arange(1.0 * 2 * n_scenarios).reshape(1, 2, n_scenarios) + sol_da = xr.DataArray( + values, + dims=["component", "time", "scenario"], + coords={ + "component": ["comp1"], + "time": [0, 1], + "scenario": list(range(n_scenarios)), + }, + ) + fake_var = FakeLinopyVar( + name="mod__p", + coords={"component": xr.DataArray(["comp1"])}, + ) + return FakeProblem( + linopy_model=FakeLinopyModel(solution={"mod__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + scenarios=n_scenarios, + ) + + +# --------------------------------------------------------------------------- +# Tests: to_dataset() +# --------------------------------------------------------------------------- + + +def test_to_dataset_returns_xr_dataset() -> None: + st = SimulationTableBuilder().build(_make_problem()) # type: ignore[arg-type] + ds = st.to_dataset() + assert isinstance(ds, xr.Dataset) + + +def test_to_dataset_contains_expected_variable() -> None: + st = SimulationTableBuilder().build(_make_problem()) # type: ignore[arg-type] + ds = st.to_dataset() + assert "p" in ds.data_vars + + +def test_to_dataset_values_match_data_single_scenario() -> None: + st = SimulationTableBuilder().build(_make_problem(n_scenarios=1)) # type: ignore[arg-type] + ds = st.to_dataset() + + # Check that values in the Dataset match those in the flat DataFrame + for _, row in st.data.dropna(subset=[SimulationColumns.COMPONENT.value]).iterrows(): + output = row[SimulationColumns.OUTPUT.value] + comp = row[SimulationColumns.COMPONENT.value] + t = int(row[SimulationColumns.ABSOLUTE_TIME_INDEX.value]) + s = int(row[SimulationColumns.SCENARIO_INDEX.value]) + expected = float(row[SimulationColumns.VALUE.value]) + actual = float( + ds[output].sel( + component=comp, **{"absolute-time-index": t, "scenario-index": s} + ) + ) + assert actual == pytest.approx(expected) + + +def test_to_dataset_values_match_data_multi_scenario() -> None: + st = SimulationTableBuilder().build(_make_problem(n_scenarios=3)) # type: ignore[arg-type] + ds = st.to_dataset() + + for _, row in st.data.dropna(subset=[SimulationColumns.COMPONENT.value]).iterrows(): + output = row[SimulationColumns.OUTPUT.value] + comp = row[SimulationColumns.COMPONENT.value] + t = int(row[SimulationColumns.ABSOLUTE_TIME_INDEX.value]) + s = int(row[SimulationColumns.SCENARIO_INDEX.value]) + expected = float(row[SimulationColumns.VALUE.value]) + actual = float( + ds[output].sel( + component=comp, **{"absolute-time-index": t, "scenario-index": s} + ) + ) + assert actual == pytest.approx(expected) + + +def test_to_dataset_includes_objective_value_scalar() -> None: + st = SimulationTableBuilder().build(_make_problem()) # type: ignore[arg-type] + ds = st.to_dataset() + assert "objective-value" in ds.data_vars + assert ds["objective-value"].shape == () # scalar (no dims) + assert float(ds["objective-value"]) == pytest.approx(99.0) + + +# --------------------------------------------------------------------------- +# Tests: write_parquet() +# --------------------------------------------------------------------------- + + +def test_write_parquet_creates_file(tmp_path: Path) -> None: + pytest.importorskip("pyarrow") + st = SimulationTableBuilder().build(_make_problem(), table_id="test") # type: ignore[arg-type] + path = st.to_parquet(tmp_path) + assert path.exists() + assert path.suffix == ".parquet" + + +def _to_object_dtype(frame: pd.DataFrame) -> pd.DataFrame: + """Cast every column to numpy object dtype, normalising all nulls to None.""" + return pd.DataFrame( + {col: frame[col].to_numpy(dtype=object, na_value=None) for col in frame.columns} + ) + + +def test_write_parquet_content_matches_original(tmp_path: Path) -> None: + pytest.importorskip("pyarrow") + st = SimulationTableBuilder().build(_make_problem(), table_id="test") # type: ignore[arg-type] + path = st.to_parquet(tmp_path) + + loaded = pd.read_parquet(path) + pd.testing.assert_frame_equal( + _to_object_dtype(loaded.reset_index(drop=True)), + _to_object_dtype(st.data.reset_index(drop=True)), + check_dtype=False, + ) + + +# --------------------------------------------------------------------------- +# Tests: write_netcdf() +# --------------------------------------------------------------------------- + + +def test_write_netcdf_creates_file(tmp_path: Path) -> None: + st = SimulationTableBuilder().build(_make_problem(), table_id="test") # type: ignore[arg-type] + path = st.to_netcdf(tmp_path) + assert path.exists() + assert path.suffix == ".nc" + + +def test_write_netcdf_readable_as_dataset(tmp_path: Path) -> None: + st = SimulationTableBuilder().build(_make_problem(), table_id="test") # type: ignore[arg-type] + path = st.to_netcdf(tmp_path) + + ds = xr.open_dataset(path) + assert isinstance(ds, xr.Dataset) + assert "p" in ds.data_vars + assert "objective-value" in ds.data_vars + ds.close() diff --git a/tests/unittests/simulation/test_simulation_table_extra_outputs.py b/tests/unittests/simulation/test_simulation_table_extra_outputs.py new file mode 100644 index 00000000..5e220843 --- /dev/null +++ b/tests/unittests/simulation/test_simulation_table_extra_outputs.py @@ -0,0 +1,118 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# SPDX-License-Identifier: MPL-2.0 + +import pytest + +from gems.simulation.simulation_table import SimulationTableBuilder + + +def test_extra_output_with_sum_connections() -> None: + """ + Extra output using sum_connections is evaluated correctly via + VectorizedExtraOutputBuilder + build_port_arrays. + + Setup: gen_1 (GEN model, variable gen=5) connects to node_1 (NODE model). + NODE model has extra output 'total_flow' = sum_connections(balance_port.flow). + GEN model defines balance_port.flow = var("gen"). + Expected: total_flow at node_1 == 5.0. + """ + from gems.expression import var + from gems.expression.expression import literal, port_field + from gems.model.model import ModelPort, model + from gems.model.port import PortField, PortFieldDefinition, PortFieldId, PortType + from gems.model.variable import float_variable + from gems.simulation import TimeBlock, build_problem + from gems.study import Component, DataBase, PortRef, Study, System, create_component + + BALANCE_PORT_TYPE = PortType(id="balance", fields=[PortField("flow")]) + + # Generator: variable gen fixed to 5, exposes it as port flow. + GEN_MODEL = model( + id="GEN_EXTRA", + variables=[ + float_variable("gen", lower_bound=literal(5), upper_bound=literal(5)) + ], + ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], + port_fields_definitions=[ + PortFieldDefinition( + port_field=PortFieldId("balance_port", "flow"), + definition=var("gen"), + ) + ], + ) + + # Node: slave port, extra output = sum of incoming flows (no binding constraint). + NODE_MODEL = model( + id="NODE_EXTRA", + ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], + extra_outputs={ + "total_flow": port_field("balance_port", "flow").sum_connections() + }, + ) + + database = DataBase() + + gen_comp = create_component(model=GEN_MODEL, id="gen_1") + node_comp = Component(model=NODE_MODEL, id="node_1") + + system = System("test_sum_connections") + system.add_component(gen_comp) + system.add_component(node_comp) + system.connect( + PortRef(gen_comp, "balance_port"), PortRef(node_comp, "balance_port") + ) + + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), scenario_ids=list(range(1)) + ) + problem.solve(solver_name="highs") + + df = SimulationTableBuilder().build(problem) + total_flow = ( + df.component("node_1") + .output("total_flow") + .value(time_index=0, scenario_index=0) + ) + assert total_flow == pytest.approx(5.0) + + +def test_extra_output_nonlinear() -> None: + """ + Nonlinear extra output (var * var) is correctly evaluated. + + VectorizedExtraOutputBuilder allows products of variables since extra + outputs are not solver constraints. Equivalent VectorizedLinearExprBuilder + would raise NotImplementedError for the same expression. + + Setup: one component with variable a=3 (fixed). Extra output squared = a*a. + Expected: squared = 9.0. + """ + from gems.expression import var + from gems.expression.expression import literal + from gems.model.model import model + from gems.model.variable import float_variable + from gems.simulation import TimeBlock, build_problem + from gems.study import DataBase, Study, System, create_component + + SIMPLE_MODEL = model( + id="SIMPLE_NL", + variables=[float_variable("a", lower_bound=literal(3), upper_bound=literal(3))], + extra_outputs={"squared": var("a") * var("a")}, + ) + + database = DataBase() + comp = create_component(model=SIMPLE_MODEL, id="comp_1") + + system = System("test_nonlinear") + system.add_component(comp) + + problem = build_problem( + Study(system, database), TimeBlock(1, [0]), scenario_ids=list(range(1)) + ) + problem.solve(solver_name="highs") + + df = SimulationTableBuilder().build(problem) + squared = ( + df.component("comp_1").output("squared").value(time_index=0, scenario_index=0) + ) + assert squared == pytest.approx(9.0) diff --git a/tests/unittests/simulation/test_simulation_table_mock.py b/tests/unittests/simulation/test_simulation_table_mock.py index fd323c20..f1886a64 100644 --- a/tests/unittests/simulation/test_simulation_table_mock.py +++ b/tests/unittests/simulation/test_simulation_table_mock.py @@ -1,104 +1,97 @@ # Copyright (c) 2024, RTE (https://www.rte-france.com) # SPDX-License-Identifier: MPL-2.0 -from dataclasses import dataclass +from dataclasses import dataclass, field +from pathlib import Path +from typing import Optional +import numpy as np import pandas as pd +import pytest +import xarray as xr -from gems.simulation.output_values import OutputValues -from gems.simulation.simulation_table import ( - SimulationColumns, - SimulationTableBuilder, - SimulationTableWriter, -) +from gems.simulation.simulation_table import SimulationColumns, SimulationTableBuilder @dataclass(frozen=True) -class FakeTimeIndex: - """Represents a (time, scenario) pair for testing.""" +class FakeBlock: + """Fake time block with an id and absolute timestep list.""" - time: int - scenario: int + id: int = 1 + timesteps: tuple = (0, 1, 2) -@dataclass(frozen=True) -class FakeVariable: - """Mimics a solver variable with values and basis statuses.""" - - _name: str - _value: dict[FakeTimeIndex, float] - _basis_status: dict[FakeTimeIndex, str] +@dataclass +class FakeLinopyVar: + """Minimal linopy variable stub exposing name and component coords.""" + name: str + coords: dict # {"component": xr.DataArray} -@dataclass(frozen=True) -class FakeComponent: - """Container for fake variables.""" - - _variables: dict[str, FakeVariable] +@dataclass +class FakeModel: + """Fake model with no extra outputs.""" -@dataclass(frozen=True) -class FakeSolver: - """Fake solver providing a fixed objective value.""" + extra_outputs: dict = field(default_factory=dict) - @dataclass(frozen=True) - class Obj: - def Value(self) -> float: - return 42.0 - def Objective(self): - return self.Obj() +@dataclass +class FakeStudy: + model_components: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) -@dataclass(frozen=True) -class FakeContext: - """Fake optimization context with a single block and block length.""" +@dataclass +class FakeLinopyModel: + """Fake linopy model exposing a solution dataset.""" - @dataclass(frozen=True) - class Block: - id: int = 1 + solution: dict # lv.name -> xr.DataArray - _block: Block = Block() + @property + def dual(self) -> xr.Dataset: + return xr.Dataset() - def block_length(self) -> int: - return 3 + solver_model = None -@dataclass(frozen=True) +@dataclass class FakeProblem: - """Fake problem that binds context and solver.""" - - context: FakeContext = FakeContext() - solver: FakeSolver = FakeSolver() - - -class FakeOutputValues(OutputValues): - """Simplified OutputValues holding fake components and optional extras.""" - - def __init__(self, problem: FakeProblem, components: dict, extra_outputs=None): - self.problem = problem # type: ignore - self._components = components - self._extra_outputs = extra_outputs or {} - - -def test_simulation_table_builder_manual(tmp_path): - """Test SimulationTableBuilder and SimulationTableWriter with fake data.""" - problem = FakeProblem() - - ts0 = FakeTimeIndex(time=0, scenario=0) - ts1 = FakeTimeIndex(time=1, scenario=0) + """Fake OptimizationProblem with the attributes used by SimulationTableBuilder.""" + + block: FakeBlock = field(default_factory=FakeBlock) + block_length: int = 3 + objective_value: float = 42.0 + linopy_model: Optional[FakeLinopyModel] = None + _linopy_vars: dict = field(default_factory=dict) + models: dict = field(default_factory=dict) + model_components: dict = field(default_factory=dict) + study: FakeStudy = field(default_factory=FakeStudy) + + +def test_simulation_table_builder_manual(tmp_path: Path) -> None: + """Test SimulationTableBuilder with fake data.""" + sol_da = xr.DataArray( + np.array([[[10.0], [20.0]]]), + dims=["component", "time", "scenario"], + coords={"component": ["compA"], "time": [0, 1], "scenario": [0]}, + ) - var = FakeVariable( - _name="p", - _value={ts0: 10.0, ts1: 20.0}, - _basis_status={ts0: "BASIC", ts1: "NONBASIC"}, + fake_var = FakeLinopyVar( + name="test_model__p", + coords={"component": xr.DataArray(["compA"])}, ) - component = FakeComponent(_variables={"var1": var}) - output_values = FakeOutputValues(problem=problem, components={"compA": component}) + problem = FakeProblem( + linopy_model=FakeLinopyModel(solution={"test_model__p": sol_da}), + _linopy_vars={(0, "p"): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + ) builder = SimulationTableBuilder(simulation_id="test") - df = builder.build(output_values) # type: ignore + df = builder.build(problem, table_id="test") # type: ignore expected_rows = [ { @@ -109,7 +102,7 @@ def test_simulation_table_builder_manual(tmp_path): SimulationColumns.BLOCK_TIME_INDEX: 0, SimulationColumns.SCENARIO_INDEX: 0, SimulationColumns.VALUE: 10.0, - SimulationColumns.BASIS_STATUS: "BASIC", + SimulationColumns.BASIS_STATUS: None, }, { SimulationColumns.BLOCK: 1, @@ -119,7 +112,7 @@ def test_simulation_table_builder_manual(tmp_path): SimulationColumns.BLOCK_TIME_INDEX: 1, SimulationColumns.SCENARIO_INDEX: 0, SimulationColumns.VALUE: 20.0, - SimulationColumns.BASIS_STATUS: "NONBASIC", + SimulationColumns.BASIS_STATUS: None, }, { SimulationColumns.BLOCK: 1, @@ -134,14 +127,22 @@ def test_simulation_table_builder_manual(tmp_path): ] expected_df = pd.DataFrame(expected_rows) + def _to_object_dtype(frame: pd.DataFrame) -> pd.DataFrame: + """Cast every column to numpy object dtype, normalising all nulls to None.""" + return pd.DataFrame( + { + col: frame[col].to_numpy(dtype=object, na_value=None) + for col in frame.columns + } + ) + pd.testing.assert_frame_equal( - df.reset_index(drop=True), - expected_df, + _to_object_dtype(df.data.reset_index(drop=True)), + _to_object_dtype(expected_df), check_dtype=False, ) - writer = SimulationTableWriter(df) - csv_path = writer.write_csv(tmp_path, simulation_id="test", optim_nb=1) + csv_path = df.to_csv(tmp_path) assert csv_path.exists(), "CSV file was not created" @@ -152,3 +153,77 @@ def test_simulation_table_builder_manual(tmp_path): assert first_line == expected_header, "CSV header does not match expected columns" csv_path.unlink() + + pytest.importorskip("pyarrow") + parquet_path = df.to_parquet(tmp_path) + assert parquet_path.exists(), "Parquet file was not created" + loaded = pd.read_parquet(parquet_path) + assert list(loaded.columns) == [col.value for col in SimulationColumns] + parquet_path.unlink() + + +def _make_problem_with_da(da: xr.DataArray, var_name: str = "p") -> "FakeProblem": + """Build a FakeProblem whose only variable has the given DataArray as solution.""" + fake_var = FakeLinopyVar( + name=f"mod__{var_name}", + coords={"component": xr.DataArray(["compA"])}, + ) + return FakeProblem( + block_length=3, + linopy_model=FakeLinopyModel(solution={f"mod__{var_name}": da}), + _linopy_vars={(0, var_name): fake_var}, + models={0: FakeModel()}, + model_components={}, + study=FakeStudy(models={0: FakeModel()}, model_components={}), + ) + + +def test_time_independent_output_has_none_time_indices() -> None: + """A var with no time dim produces None for both time index columns.""" + da = xr.DataArray( + np.array([[5.0, 6.0]]), # shape [component=1, scenario=2] + dims=["component", "scenario"], + coords={"component": ["compA"], "scenario": [0, 1]}, + ) + problem = _make_problem_with_da(da) + st = SimulationTableBuilder().build(problem) # type: ignore[arg-type] + rows = st.data[st.data[SimulationColumns.OUTPUT.value] == "p"] + + assert rows[SimulationColumns.ABSOLUTE_TIME_INDEX.value].isna().all() + assert rows[SimulationColumns.BLOCK_TIME_INDEX.value].isna().all() + assert list(rows[SimulationColumns.SCENARIO_INDEX.value]) == [0, 1] + assert list(rows[SimulationColumns.VALUE.value]) == [5.0, 6.0] + + +def test_scenario_independent_output_has_none_scenario_index() -> None: + """A var with no scenario dim produces None for the scenario index column.""" + da = xr.DataArray( + np.array([[10.0, 20.0, 30.0]]), # shape [component=1, time=3] + dims=["component", "time"], + coords={"component": ["compA"], "time": [0, 1, 2]}, + ) + problem = _make_problem_with_da(da) + st = SimulationTableBuilder().build(problem) # type: ignore[arg-type] + rows = st.data[st.data[SimulationColumns.OUTPUT.value] == "p"] + + assert rows[SimulationColumns.SCENARIO_INDEX.value].isna().all() + assert list(rows[SimulationColumns.ABSOLUTE_TIME_INDEX.value]) == [0, 1, 2] + assert list(rows[SimulationColumns.VALUE.value]) == [10.0, 20.0, 30.0] + + +def test_scalar_output_has_none_time_and_scenario_indices() -> None: + """A var with no time and no scenario dim produces None for all index columns.""" + da = xr.DataArray( + np.array([99.0]), # shape [component=1] + dims=["component"], + coords={"component": ["compA"]}, + ) + problem = _make_problem_with_da(da) + st = SimulationTableBuilder().build(problem) # type: ignore[arg-type] + rows = st.data[st.data[SimulationColumns.OUTPUT.value] == "p"] + + assert len(rows) == 1 + assert pd.isna(rows.iloc[0][SimulationColumns.ABSOLUTE_TIME_INDEX.value]) + assert pd.isna(rows.iloc[0][SimulationColumns.BLOCK_TIME_INDEX.value]) + assert pd.isna(rows.iloc[0][SimulationColumns.SCENARIO_INDEX.value]) + assert rows.iloc[0][SimulationColumns.VALUE.value] == 99.0 diff --git a/tests/unittests/simulation/test_vectorized_linear_expr_builder.py b/tests/unittests/simulation/test_vectorized_linear_expr_builder.py new file mode 100644 index 00000000..192d4d0e --- /dev/null +++ b/tests/unittests/simulation/test_vectorized_linear_expr_builder.py @@ -0,0 +1,821 @@ +# Copyright (c) 2024, RTE (https://www.rte-france.com) +# +# See AUTHORS.txt +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. +# +# SPDX-License-Identifier: MPL-2.0 +# +# This file is part of the Antares project. + +""" +Nasty unit tests for VectorizedLinearExprBuilder and the _linopy_add helper. + +Covers every overridden method, all error paths, boundary conditions, and the +linopy-specific operand-swap quirk in addition. +""" + +import linopy +import numpy as np +import pandas as pd +import pytest +import xarray as xr + +from gems.expression.expression import ( + AllTimeSumNode, + LiteralNode, + MaxNode, + MinNode, + PortFieldAggregatorNode, + PortFieldNode, + ScenarioOperatorNode, + literal, + maximum, + minimum, + param, + var, +) +from gems.expression.visitor import visit +from gems.model.port import PortFieldId +from gems.simulation.linearize import VectorizedLinearExprBuilder +from gems.simulation.vectorized_builder import VectorizedBuilderBase, _linopy_add + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture +def lp_model() -> linopy.Model: + """Bare linopy model — each test gets a fresh one.""" + return linopy.Model() + + +@pytest.fixture +def comp_time_var(lp_model: linopy.Model) -> linopy.Variable: + """Variable with dims (component, time): 2 components × 3 timesteps.""" + coords = [ + pd.Index(["c1", "c2"], name="component"), + pd.Index([0, 1, 2], name="time"), + ] + return lp_model.add_variables(lower=0, upper=10, coords=coords, name="x") + + +@pytest.fixture +def param_da() -> xr.DataArray: + """Parameter DataArray shaped (component=2, time=3).""" + return xr.DataArray( + [[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]], + dims=["component", "time"], + coords={"component": ["c1", "c2"], "time": [0, 1, 2]}, + ) + + +@pytest.fixture +def builder( + comp_time_var: linopy.Variable, param_da: xr.DataArray +) -> VectorizedLinearExprBuilder: + """Builder pre-loaded with one variable and one parameter, block=3, scenarios=2.""" + return VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={("m", "x"): comp_time_var}, + param_arrays={("m", "p"): param_da}, + port_arrays={}, + block_length=3, + ) + + +@pytest.fixture +def empty_builder() -> VectorizedLinearExprBuilder: + """Builder with nothing registered — for all KeyError paths.""" + return VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={}, + port_arrays={}, + block_length=3, + ) + + +# --------------------------------------------------------------------------- +# 1. _linopy_add helper +# --------------------------------------------------------------------------- + + +def test_linopy_add_da_plus_da_returns_da() -> None: + """Two DataArrays: no swap, result is DataArray.""" + a = xr.DataArray(3.0) + b = xr.DataArray(4.0) + result = _linopy_add(a, b) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(7.0) + + +def test_linopy_add_da_plus_variable_swaps( + lp_model: linopy.Model, comp_time_var: linopy.Variable +) -> None: + """DataArray on left, linopy.Variable on right — must swap so linopy goes left.""" + da = xr.DataArray(1.0) + result = _linopy_add(da, comp_time_var) + # xr.DataArray.__add__(Variable) would raise; the swap makes it work + assert isinstance(result, linopy.LinearExpression) + + +def test_linopy_add_da_plus_linear_expr_swaps( + lp_model: linopy.Model, comp_time_var: linopy.Variable +) -> None: + """DataArray + LinearExpression must swap.""" + da = xr.DataArray(2.0) + lin_expr = comp_time_var + comp_time_var # LinearExpression + result = _linopy_add(da, lin_expr) + assert isinstance(result, linopy.LinearExpression) + + +def test_linopy_add_variable_plus_da_no_swap( + lp_model: linopy.Model, comp_time_var: linopy.Variable +) -> None: + """Variable on left already — no swap needed.""" + da = xr.DataArray(5.0) + result = _linopy_add(comp_time_var, da) + assert isinstance(result, linopy.LinearExpression) + + +def test_linopy_add_variable_plus_variable( + lp_model: linopy.Model, comp_time_var: linopy.Variable +) -> None: + """Two linopy Variables — no DataArray involved, returns LinearExpression.""" + result = _linopy_add(comp_time_var, comp_time_var) + assert isinstance(result, linopy.LinearExpression) + + +# --------------------------------------------------------------------------- +# 2. literal() +# --------------------------------------------------------------------------- + + +def test_literal_scalar(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(42.0), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(42.0) + + +def test_literal_zero(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(0.0), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(0.0) + + +def test_literal_negative(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(-99.5), builder) + assert float(result) == pytest.approx(-99.5) + + +# --------------------------------------------------------------------------- +# 3. parameter() +# --------------------------------------------------------------------------- + + +def test_parameter_found( + builder: VectorizedLinearExprBuilder, param_da: xr.DataArray +) -> None: + result = visit(param("p"), builder) + assert isinstance(result, xr.DataArray) + xr.testing.assert_equal(result, param_da) + + +def test_parameter_missing_raises_key_error( + empty_builder: VectorizedLinearExprBuilder, +) -> None: + with pytest.raises(KeyError, match="MISSING"): + visit(param("MISSING"), empty_builder) + + +def test_parameter_wrong_model_id_raises(param_da: xr.DataArray) -> None: + """Params keyed under model 'A', but builder has model_id 'B' — KeyError.""" + b = VectorizedLinearExprBuilder( + model_id="B", + linopy_vars={}, + param_arrays={("A", "p"): param_da}, # wrong model id + port_arrays={}, + block_length=3, + ) + with pytest.raises(KeyError): + visit(param("p"), b) + + +# --------------------------------------------------------------------------- +# 4. variable() +# --------------------------------------------------------------------------- + + +def test_variable_found( + builder: VectorizedLinearExprBuilder, comp_time_var: linopy.Variable +) -> None: + result = visit(var("x"), builder) + assert isinstance(result, linopy.Variable) + + +def test_variable_returns_correct_dims( + builder: VectorizedLinearExprBuilder, comp_time_var: linopy.Variable +) -> None: + result = visit(var("x"), builder) + assert set(result.dims) == {"component", "time"} + + +def test_variable_missing_raises_key_error( + empty_builder: VectorizedLinearExprBuilder, +) -> None: + with pytest.raises(KeyError, match="GHOST"): + visit(var("GHOST"), empty_builder) + + +def test_variable_key_includes_model_id( + comp_time_var: linopy.Variable, param_da: xr.DataArray +) -> None: + """Variable keyed under wrong model id must raise KeyError.""" + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={("OTHER_MODEL", "x"): comp_time_var}, # wrong model id + param_arrays={}, + port_arrays={}, + block_length=3, + ) + with pytest.raises(KeyError): + visit(var("x"), b) + + +# --------------------------------------------------------------------------- +# 5. negation() +# --------------------------------------------------------------------------- + + +def test_negation_of_literal(builder: VectorizedLinearExprBuilder) -> None: + result = visit(-literal(3), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(-3.0) + + +def test_negation_of_variable(builder: VectorizedLinearExprBuilder) -> None: + result = visit(-var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_double_negation(builder: VectorizedLinearExprBuilder) -> None: + result = visit(-(-literal(5)), builder) + assert float(result) == pytest.approx(5.0) + + +# --------------------------------------------------------------------------- +# 6. addition() — overridden with _linopy_add +# --------------------------------------------------------------------------- + + +def test_addition_two_literals(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(1) + literal(2), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(3.0) + + +def test_addition_da_plus_variable(builder: VectorizedLinearExprBuilder) -> None: + """param("p") + var("x"): DataArray first, linopy second — requires swap.""" + result = visit(param("p") + var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_addition_variable_plus_da(builder: VectorizedLinearExprBuilder) -> None: + """var("x") + param("p"): linopy first, DataArray second — no swap needed.""" + result = visit(var("x") + param("p"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_addition_three_operands_da_da_variable( + builder: VectorizedLinearExprBuilder, +) -> None: + """literal(1) + literal(2) + var("x"): last linopy operand still works.""" + result = visit(literal(1) + literal(2) + var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_addition_variable_plus_variable(builder: VectorizedLinearExprBuilder) -> None: + result = visit(var("x") + var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_addition_three_operands_variable_da_da( + builder: VectorizedLinearExprBuilder, +) -> None: + """var("x") + literal(1) + literal(2): first is linopy, rest are DataArray.""" + result = visit(var("x") + literal(1) + literal(2), builder) + assert isinstance(result, linopy.LinearExpression) + + +# --------------------------------------------------------------------------- +# 7. multiplication() and division() +# --------------------------------------------------------------------------- + + +def test_multiplication_scalar_times_variable( + builder: VectorizedLinearExprBuilder, +) -> None: + result = visit(literal(2) * var("x"), builder) + assert isinstance(result, linopy.LinearExpression) + + +def test_multiplication_two_literals(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(3) * literal(4), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(12.0) + + +def test_division_da_by_da(builder: VectorizedLinearExprBuilder) -> None: + result = visit(param("p") / literal(2), builder) + assert isinstance(result, xr.DataArray) + expected = xr.DataArray( + [[0.5, 1.0, 1.5], [2.0, 2.5, 3.0]], + dims=["component", "time"], + coords={"component": ["c1", "c2"], "time": [0, 1, 2]}, + ) + xr.testing.assert_allclose(result, expected) + + +def test_division_by_zero_produces_inf(builder: VectorizedLinearExprBuilder) -> None: + """xarray silently produces inf on /0 — document expected behavior.""" + result = visit(literal(5) / literal(0), builder) + assert not np.isfinite(float(result)) + + +# --------------------------------------------------------------------------- +# 8. comparison() always raises +# --------------------------------------------------------------------------- + + +def test_comparison_always_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match="ComparisonNode"): + visit(var("x") <= param("p"), builder) + + +def test_comparison_equal_also_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError): + visit(literal(1) == literal(1), builder) + + +# --------------------------------------------------------------------------- +# 9. floor() / ceil() — linopy guard +# --------------------------------------------------------------------------- + + +def test_floor_on_da_works(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(2.7).floor(), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(2.0) + + +def test_floor_on_variable_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match="floor"): + visit(var("x").floor(), builder) + + +def test_floor_on_negative_da(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(-2.1).floor(), builder) + assert float(result) == pytest.approx(-3.0) + + +def test_ceil_on_da_works(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(2.1).ceil(), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(3.0) + + +def test_ceil_on_variable_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match="ceil"): + visit(var("x").ceil(), builder) + + +def test_ceil_on_exact_integer_da(builder: VectorizedLinearExprBuilder) -> None: + result = visit(literal(3.0).ceil(), builder) + assert float(result) == pytest.approx(3.0) + + +# --------------------------------------------------------------------------- +# 10. maximum() / minimum() — linopy guard +# --------------------------------------------------------------------------- + + +def test_maximum_all_da_works(builder: VectorizedLinearExprBuilder) -> None: + result = visit(maximum(literal(3), literal(1), literal(4)), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(4.0) + + +def test_maximum_two_da_returns_larger(builder: VectorizedLinearExprBuilder) -> None: + result = visit(maximum(literal(7), literal(3)), builder) + assert float(result) == pytest.approx(7.0) + + +def test_maximum_with_variable_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match="maximum"): + visit(maximum(var("x"), literal(5)), builder) + + +def test_maximum_variable_first_also_raises( + builder: VectorizedLinearExprBuilder, +) -> None: + with pytest.raises(NotImplementedError): + visit(maximum(literal(5), var("x")), builder) + + +def test_minimum_all_da_works(builder: VectorizedLinearExprBuilder) -> None: + result = visit(minimum(literal(3), literal(1), literal(4)), builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(1.0) + + +def test_minimum_with_variable_raises(builder: VectorizedLinearExprBuilder) -> None: + with pytest.raises(NotImplementedError, match="minimum"): + visit(minimum(literal(2), var("x")), builder) + + +# --------------------------------------------------------------------------- +# 11. time_shift() +# --------------------------------------------------------------------------- + + +def _scalar_time_da(values: list) -> xr.DataArray: + """Helper: single-component DataArray with time dim.""" + return xr.DataArray( + [values], + dims=["component", "time"], + coords={"component": ["c1"], "time": list(range(len(values)))}, + ) + + +def test_time_shift_by_zero(empty_builder: VectorizedLinearExprBuilder) -> None: + da = _scalar_time_da([1.0, 2.0, 3.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(0), b) + xr.testing.assert_allclose(result, da) + + +def test_time_shift_by_one_cycles(empty_builder: VectorizedLinearExprBuilder) -> None: + """Shift by +1: [1,2,3] → [2,3,1] (element at position t = original[t+1 % T]).""" + da = _scalar_time_da([1.0, 2.0, 3.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(1), b) + expected = _scalar_time_da([2.0, 3.0, 1.0]) + xr.testing.assert_allclose(result, expected) + + +def test_time_shift_by_negative_one_cycles( + empty_builder: VectorizedLinearExprBuilder, +) -> None: + """Shift by -1: [1,2,3] → [3,1,2].""" + da = _scalar_time_da([1.0, 2.0, 3.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(-1), b) + expected = _scalar_time_da([3.0, 1.0, 2.0]) + xr.testing.assert_allclose(result, expected) + + +def test_time_shift_by_block_length_is_identity() -> None: + """Shift by block_length should produce the same array (full cycle).""" + da = _scalar_time_da([10.0, 20.0, 30.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(3), b) # shift by block_length + xr.testing.assert_allclose(result, da) + + +def test_time_shift_on_no_time_dim_is_noop() -> None: + """Scalar (no time dim) DataArray: shift is a no-op.""" + scalar = xr.DataArray(99.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): scalar}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(1), b) + assert float(result) == pytest.approx(99.0) + + +def test_time_shift_coordinates_are_reassigned() -> None: + """After a shift, time coords must be [0,1,2] not the shifted positions.""" + da = _scalar_time_da([1.0, 2.0, 3.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").shift(1), b) + assert list(result.coords["time"].values) == [0, 1, 2] + + +# --------------------------------------------------------------------------- +# 12. time_eval() +# --------------------------------------------------------------------------- + + +def test_time_eval_selects_correct_timestep() -> None: + da = _scalar_time_da([10.0, 20.0, 30.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").eval(1), b) + # Should select time=1, squeezing the time dim + assert "time" not in result.dims + np.testing.assert_allclose(result.values.flatten(), [20.0]) + + +def test_time_eval_wraps_beyond_block() -> None: + """eval(block_length + 1) == eval(1) via modulo.""" + da = _scalar_time_da([10.0, 20.0, 30.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + r1 = visit(param("s").eval(1), b) + r4 = visit(param("s").eval(4), b) # 4 % 3 = 1 + xr.testing.assert_allclose(r1, r4) + + +def test_time_eval_on_no_time_dim() -> None: + scalar = xr.DataArray(7.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): scalar}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").eval(2), b) + assert float(result) == pytest.approx(7.0) + + +# --------------------------------------------------------------------------- +# 13. all_time_sum() +# --------------------------------------------------------------------------- + + +def test_all_time_sum_with_time_dim() -> None: + da = _scalar_time_da([1.0, 2.0, 3.0]) # sum = 6 + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").time_sum(), b) + assert "time" not in result.dims + np.testing.assert_allclose(result.values.flatten(), [6.0]) + + +def test_all_time_sum_without_time_dim_multiplies_block_length() -> None: + scalar = xr.DataArray(5.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): scalar}, + port_arrays={}, + block_length=4, + ) + result = visit(param("s").time_sum(), b) + assert float(result) == pytest.approx(20.0) # 5.0 * 4 + + +# --------------------------------------------------------------------------- +# 14. time_sum(from, to) +# --------------------------------------------------------------------------- + + +def test_time_sum_same_from_and_to_equals_single_shift() -> None: + """time_sum(0, 0) should equal shift(0) — the operand itself.""" + da = _scalar_time_da([1.0, 2.0, 3.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").time_sum(0, 0), b) + xr.testing.assert_allclose(result, da) + + +def test_time_sum_range_sums_correctly() -> None: + """time_sum(0, 2) on [1,1,1] = 3 per position (each position accumulates 3 cyclic copies).""" + da = _scalar_time_da([1.0, 1.0, 1.0]) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=3, + ) + result = visit(param("s").time_sum(0, 2), b) + np.testing.assert_allclose(result.values.flatten(), [3.0, 3.0, 3.0]) + + +def test_time_sum_with_variable_returns_linear_expression( + builder: VectorizedLinearExprBuilder, +) -> None: + result = visit(var("x").time_sum(0, 1), builder) + assert isinstance(result, linopy.LinearExpression) + + +# --------------------------------------------------------------------------- +# 15. scenario_operator() +# --------------------------------------------------------------------------- + + +def test_expectation_with_scenario_dim() -> None: + da = xr.DataArray( + [[10.0, 20.0]], + dims=["component", "scenario"], + coords={"component": ["c1"], "scenario": [0, 1]}, + ) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): da}, + port_arrays={}, + block_length=1, + ) + result = visit(param("s").expec(), b) + # sum([10, 20]) / 2 = 15 + assert "scenario" not in result.dims + np.testing.assert_allclose(result.values.flatten(), [15.0]) + + +def test_expectation_without_scenario_dim_is_noop() -> None: + scalar = xr.DataArray(42.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): scalar}, + port_arrays={}, + block_length=1, + ) + result = visit(param("s").expec(), b) + assert float(result) == pytest.approx(42.0) + + +def test_variance_scenario_operator_raises_at_node_construction() -> None: + """'Variance' is a recognized operator name but raises ValueError on construction + because the ScenarioOperatorNode validates names at __post_init__... actually, + 'Variance' IS in the valid list. The builder raises NotImplementedError at visit time. + """ + scalar = xr.DataArray(1.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={("m", "s"): scalar}, + port_arrays={}, + block_length=1, + ) + # var("s").variance() builds ScenarioOperatorNode with name="Variance" + expr = param("s").variance() + with pytest.raises(NotImplementedError, match="Variance"): + visit(expr, b) + + +# --------------------------------------------------------------------------- +# 16. port_field() +# --------------------------------------------------------------------------- + + +def test_port_field_found() -> None: + key = PortFieldId("port_a", "flow") + da = xr.DataArray(99.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={}, + port_arrays={key: da}, + block_length=1, + ) + from gems.expression.expression import port_field + + result = visit(port_field("port_a", "flow"), b) + assert float(result) == pytest.approx(99.0) + + +def test_port_field_missing_raises_key_error( + empty_builder: VectorizedLinearExprBuilder, +) -> None: + from gems.expression.expression import port_field + + with pytest.raises(KeyError): + visit(port_field("no_such_port", "flow"), empty_builder) + + +# --------------------------------------------------------------------------- +# 17. port_field_aggregator() +# --------------------------------------------------------------------------- + + +def test_port_sum_with_connection_returns_expression() -> None: + key = PortFieldId("port_a", "flow") + da = xr.DataArray(7.0) + b = VectorizedLinearExprBuilder( + model_id="m", + linopy_vars={}, + param_arrays={}, + port_arrays={key: da}, + block_length=1, + ) + from gems.expression.expression import port_field + + expr = port_field("port_a", "flow").sum_connections() + result = visit(expr, b) + assert float(result) == pytest.approx(7.0) + + +def test_port_sum_no_connection_returns_zero( + empty_builder: VectorizedLinearExprBuilder, +) -> None: + """PortFieldAggregatorNode with no matching port returns DataArray(0.0).""" + from gems.expression.expression import port_field + + expr = port_field("absent_port", "flow").sum_connections() + result = visit(expr, empty_builder) + assert isinstance(result, xr.DataArray) + assert float(result) == pytest.approx(0.0) + + +def test_unsupported_port_aggregator_raises_at_node_construction() -> None: + """Only 'PortSum' is valid; other aggregator names raise at node construction.""" + from gems.expression.expression import PortFieldAggregatorNode, port_field + + with pytest.raises(NotImplementedError): + PortFieldAggregatorNode(operand=port_field("p", "f"), aggregator="PortMax") + + +# --------------------------------------------------------------------------- +# 18. Private helpers +# --------------------------------------------------------------------------- + + +def test_eval_int_from_integer_literal() -> None: + result = VectorizedBuilderBase._eval_int(literal(3)) + assert result == 3 + assert isinstance(result, int) + + +def test_eval_int_from_float_that_is_integer() -> None: + result = VectorizedBuilderBase._eval_int(literal(3.0)) + assert result == 3 + assert isinstance(result, int) + + +def test_eval_int_from_non_integer_float_raises() -> None: + with pytest.raises(ValueError, match="integer"): + VectorizedBuilderBase._eval_int(literal(3.5)) + + +def test_da_to_int_from_integer_array() -> None: + da = xr.DataArray(2.0) + result = VectorizedBuilderBase._da_to_int(da) + assert result == 2 + assert isinstance(result, int) + + +def test_da_to_int_from_non_integer_raises() -> None: + da = xr.DataArray(2.5) + with pytest.raises(ValueError, match="integer"): + VectorizedBuilderBase._da_to_int(da) diff --git a/tests/unittests/system/libs/standard.py b/tests/unittests/system/libs/standard.py index a8f6cd7f..ca6a93a1 100644 --- a/tests/unittests/system/libs/standard.py +++ b/tests/unittests/system/libs/standard.py @@ -17,7 +17,6 @@ from gems.expression import literal, param, var from gems.expression.expression import port_field from gems.expression.indexing_structure import IndexingStructure -from gems.model.common import ProblemContext from gems.model.constraint import Constraint from gems.model.model import ModelPort, model from gems.model.parameter import float_parameter, int_parameter @@ -58,12 +57,14 @@ == var("spillage") - var("unsupplied_energy"), ) ], - objective_operational_contribution=( - param("spillage_cost") * var("spillage") - + param("ens_cost") * var("unsupplied_energy") - ) - .time_sum() - .expec(), + objective_contributions={ + "operational": ( + param("spillage_cost") * var("spillage") + + param("ens_cost") * var("unsupplied_energy") + ) + .time_sum() + .expec() + }, ) """ @@ -130,13 +131,13 @@ name="Max generation", expression=var("generation") <= param("p_max") ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) GENERATOR_MODEL_WITH_PMIN = model( - id="GEN", + id="GEN_WITH_PMIN", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("p_min", CONSTANT), @@ -160,9 +161,9 @@ lower_bound=literal(0), ), # To test both ways of setting constraints ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) """ @@ -170,7 +171,7 @@ and total generation in whole period. It considers a full storage with no replenishing """ GENERATOR_MODEL_WITH_STORAGE = model( - id="GEN", + id="GEN_WITH_STORAGE", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", CONSTANT), @@ -193,14 +194,14 @@ expression=var("generation").time_sum() <= param("full_storage"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # For now, no starting cost THERMAL_CLUSTER_MODEL_HD = model( - id="GEN", + id="THERMAL_CLUSTER_HD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -265,14 +266,14 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) # Same model as previous one, except that starting/stopping variables are now non anticipative THERMAL_CLUSTER_MODEL_DHD = model( - id="GEN", + id="THERMAL_CLUSTER_DHD", parameters=[ float_parameter("p_max", CONSTANT), # p_max of a single unit float_parameter("p_min", CONSTANT), @@ -337,9 +338,9 @@ <= param("nb_units_max").shift(-param("d_min_down")) - var("nb_on"), ), ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) SPILLAGE_MODEL = model( @@ -353,9 +354,9 @@ definition=-var("spillage"), ) ], - objective_operational_contribution=(param("cost") * var("spillage")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("spillage")).time_sum().expec() + }, ) UNSUPPLIED_ENERGY_MODEL = model( @@ -369,9 +370,9 @@ definition=var("unsupplied_energy"), ) ], - objective_operational_contribution=(param("cost") * var("unsupplied_energy")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("unsupplied_energy")).time_sum().expec() + }, ) # Simplified model @@ -418,12 +419,12 @@ == param("inflows"), ), ], - objective_operational_contribution=literal(0), # Implcitement nul ? + objective_contributions={"operational": literal(0)}, # Implcitement nul ? ) """ Simple thermal unit that can be invested on""" THERMAL_CANDIDATE = model( - id="GEN", + id="THERMAL_CANDIDATE", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -436,7 +437,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -449,15 +449,15 @@ constraints=[ Constraint(name="Max generation", expression=var("generation") <= var("p_max")) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("p_max"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("p_max"), + }, ) """ Simple thermal unit that can be invested on and with already installed capacity""" THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA = model( - id="GEN", + id="THERMAL_CANDIDATE_WITH_ALREADY_INSTALLED_CAPA", parameters=[ float_parameter("op_cost", CONSTANT), float_parameter("invest_cost", CONSTANT), @@ -471,7 +471,6 @@ lower_bound=literal(0), upper_bound=param("max_invest"), structure=CONSTANT, - context=ProblemContext.COUPLING, ), ], ports=[ModelPort(port_type=BALANCE_PORT_TYPE, port_name="balance_port")], @@ -488,8 +487,8 @@ <= param("already_installed_capa") + var("invested_capa"), ) ], - objective_operational_contribution=(param("op_cost") * var("generation")) - .time_sum() - .expec(), - objective_investment_contribution=param("invest_cost") * var("invested_capa"), + objective_contributions={ + "operational": (param("op_cost") * var("generation")).time_sum().expec(), + "investment": param("invest_cost") * var("invested_capa"), + }, ) diff --git a/tests/unittests/system/test_data_consistency.py b/tests/unittests/system/test_data_consistency.py index 1d2015b0..5886ffa0 100644 --- a/tests/unittests/system/test_data_consistency.py +++ b/tests/unittests/system/test_data_consistency.py @@ -12,6 +12,7 @@ from pathlib import Path from typing import Union +import numpy as np import pandas as pd import pytest @@ -27,13 +28,13 @@ ) from gems.model.port import PortFieldDefinition, PortFieldId from gems.study import ( + Component, ConstantData, DataBase, - Network, - Node, PortRef, - ScenarioIndex, ScenarioSeriesData, + Study, + System, TimeIndex, TimeScenarioSeriesData, TimeSeriesData, @@ -51,26 +52,26 @@ @pytest.fixture -def mock_network() -> Network: - node = Node(model=NODE_BALANCE_MODEL, id="1") +def mock_network() -> System: + node = Component(model=NODE_BALANCE_MODEL, id="1") demand = create_component(model=DEMAND_MODEL, id="D") gen = create_component(model=GENERATOR_MODEL, id="G") - network = Network("test") - network.add_node(node) - network.add_component(demand) - network.add_component(gen) - network.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(demand) + system.add_component(gen) + system.connect(PortRef(demand, "balance_port"), PortRef(node, "balance_port")) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) - return network + return system @pytest.fixture def mock_generator_with_fixed_scenario_time_varying_param() -> Model: fixed_scenario_time_varying_param_generator = model( - id="GEN", + id="GEN_TIME_VARYING_COST", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", NON_ANTICIPATIVE_TIME_VARYING), @@ -88,9 +89,9 @@ def mock_generator_with_fixed_scenario_time_varying_param() -> Model: name="Max generation", expression=var("generation") <= param("p_max") ) ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) return fixed_scenario_time_varying_param_generator @@ -98,7 +99,7 @@ def mock_generator_with_fixed_scenario_time_varying_param() -> Model: @pytest.fixture def mock_generator_with_scenario_varying_fixed_time_param() -> Model: scenario_varying_fixed_time_generator = model( - id="GEN", + id="GEN_SCENARIO_VARYING_COST", parameters=[ float_parameter("p_max", CONSTANT), float_parameter("cost", IndexingStructure(False, True)), @@ -116,9 +117,9 @@ def mock_generator_with_scenario_varying_fixed_time_param() -> Model: name="Max generation", expression=var("generation") <= param("p_max") ) ], - objective_operational_contribution=(param("cost") * var("generation")) - .time_sum() - .expec(), + objective_contributions={ + "operational": (param("cost") * var("generation")).time_sum().expec() + }, ) return scenario_varying_fixed_time_generator @@ -138,7 +139,7 @@ def demand_data() -> TimeScenarioSeriesData: def test_requirements_consistency_demand_model_fix_ok( - mock_network: Network, demand_data: TimeScenarioSeriesData + mock_network: System, demand_data: TimeScenarioSeriesData ) -> None: # Given # database data for "demand" defined as Time varying @@ -151,10 +152,10 @@ def test_requirements_consistency_demand_model_fix_ok( # When # No ValueError should be raised - database.requirements_consistency(mock_network) + Study(mock_network, database).check_consistency() -def test_requirements_consistency_generator_model_ok(mock_network: Network) -> None: +def test_requirements_consistency_generator_model_ok(mock_network: System) -> None: # Given # database data for "demand" defined as CONSTANT # model "D" DEMAND_MODEL is TIME_AND_SCENARIO_FREE @@ -164,11 +165,11 @@ def test_requirements_consistency_generator_model_ok(mock_network: Network) -> N database.add_data("D", "demand", ConstantData(30)) # When - database.requirements_consistency(mock_network) + Study(mock_network, database).check_consistency() def test_consistency_generation_time_free_for_constant_model_raises_exception( - mock_network: Network, demand_data: TimeScenarioSeriesData + mock_network: System, demand_data: TimeScenarioSeriesData ) -> None: # Given # database data for "p_max" defined as time varying @@ -183,11 +184,11 @@ def test_consistency_generation_time_free_for_constant_model_raises_exception( # When with pytest.raises(ValueError, match="Data inconsistency"): - database.requirements_consistency(mock_network) + Study(mock_network, database).check_consistency() def test_requirements_consistency_demand_model_time_varying_ok( - mock_network: Network, demand_data: TimeScenarioSeriesData + mock_network: System, demand_data: TimeScenarioSeriesData ) -> None: # Given # database data for "demand" defined as constant @@ -199,7 +200,7 @@ def test_requirements_consistency_demand_model_time_varying_ok( # When # No ValueError should be raised - database.requirements_consistency(mock_network) + Study(mock_network, database).check_consistency() def test_requirements_consistency_time_varying_parameter_with_correct_data_passes( @@ -208,29 +209,29 @@ def test_requirements_consistency_time_varying_parameter_with_correct_data_passe # Given # Model for test with parameter NON_ANTICIPATIVE_TIME_VARYING - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") gen = create_component( model=mock_generator_with_fixed_scenario_time_varying_param, id="G" ) - cost_data = TimeSeriesData({TimeIndex(0): 100, TimeIndex(1): 50}) + cost_data = TimeSeriesData(pd.Series({TimeIndex(0): 100, TimeIndex(1): 50})) database = DataBase() database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", cost_data) - network = Network("test") - network.add_node(node) - network.add_component(gen) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(gen) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) # No ValueError should be raised - database.requirements_consistency(network) + Study(system, database).check_consistency() @pytest.mark.parametrize( "cost_data", [ - (ScenarioSeriesData({ScenarioIndex(0): 100, ScenarioIndex(1): 50})), + (ScenarioSeriesData(np.array([100, 50], dtype=float))), ( TimeScenarioSeriesData( pd.DataFrame( @@ -252,7 +253,7 @@ def test_requirements_consistency_time_varying_parameter_with_scenario_varying_d # Given # Model for test with parameter NON_ANTICIPATIVE_TIME_VARYING - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") gen = create_component( model=mock_generator_with_fixed_scenario_time_varying_param, id="G", @@ -261,21 +262,21 @@ def test_requirements_consistency_time_varying_parameter_with_scenario_varying_d database = DataBase() database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", cost_data) - network = Network("test") - network.add_node(node) - network.add_component(gen) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(gen) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) # When # ValueError should be raised with pytest.raises(ValueError, match="Data inconsistency"): - database.requirements_consistency(network) + Study(system, database).check_consistency() @pytest.mark.parametrize( "cost_data", [ - (TimeSeriesData({TimeIndex(0): 100, TimeIndex(1): 50})), + (TimeSeriesData(pd.Series({TimeIndex(0): 100, TimeIndex(1): 50}))), ( TimeScenarioSeriesData( pd.DataFrame({(0, 0): [100, 500], (0, 1): [50, 540]}, index=[0, 1]) @@ -290,7 +291,7 @@ def test_requirements_consistency_scenario_varying_parameter_with_time_varying_d # Given # Model for test with parameter indexed by scenario only - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") gen = create_component( model=mock_generator_with_scenario_varying_fixed_time_param, id="G" ) @@ -298,14 +299,14 @@ def test_requirements_consistency_scenario_varying_parameter_with_time_varying_d database = DataBase() database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", cost_data) - network = Network("test") - network.add_node(node) - network.add_component(gen) - network.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) + system = System("test") + system.add_component(node) + system.add_component(gen) + system.connect(PortRef(gen, "balance_port"), PortRef(node, "balance_port")) # ValueError should be raised with pytest.raises(ValueError, match="Data inconsistency"): - database.requirements_consistency(network) + Study(system, database).check_consistency() def test_requirements_consistency_scenario_varying_parameter_with_correct_data_passes( @@ -314,23 +315,23 @@ def test_requirements_consistency_scenario_varying_parameter_with_correct_data_p # Given # Model for test with parameter indexed by scenario only - node = Node(model=NODE_BALANCE_MODEL, id="1") + node = Component(model=NODE_BALANCE_MODEL, id="1") gen = create_component( model=mock_generator_with_scenario_varying_fixed_time_param, id="G" ) - cost_data = ScenarioSeriesData({ScenarioIndex(0): 100, ScenarioIndex(1): 50}) + cost_data = ScenarioSeriesData(np.array([100, 50], dtype=float)) database = DataBase() database.add_data("G", "p_max", ConstantData(100)) database.add_data("G", "cost", cost_data) - network = Network("test") - network.add_node(node) - network.add_component(gen) - network.add_component(gen) + system = System("test") + system.add_component(node) + system.add_component(gen) + system.add_component(gen) # No ValueError should be raised - database.requirements_consistency(network) + Study(system, database).check_consistency() def test_load_data_from_txt() -> None: diff --git a/tests/unittests/system/test_model.py b/tests/unittests/system/test_model.py index b573966c..6eb0cbc0 100644 --- a/tests/unittests/system/test_model.py +++ b/tests/unittests/system/test_model.py @@ -10,19 +10,23 @@ # # This file is part of the Antares project. +import warnings + import pytest from gems.expression.expression import ( ExpressionNode, - comp_param, - comp_var, + ScenarioOperatorNode, literal, param, port_field, var, ) +from gems.expression.indexing_structure import IndexingStructure from gems.model import Constraint, float_variable, model +from gems.model.common import ValueType from gems.model.port import port_field_def +from gems.model.variable import bool_var, int_variable @pytest.mark.parametrize( @@ -187,7 +191,7 @@ def test_instantiating_a_model_with_non_linear_scenario_operator_in_the_objectiv _ = model( id="model_with_non_linear_op", variables=[float_variable("generation")], - objective_operational_contribution=var("generation").variance(), + objective_contributions={"operational": var("generation").variance()}, ) assert str(exc.value) == "Objective contribution must be a linear expression." @@ -196,8 +200,6 @@ def test_instantiating_a_model_with_non_linear_scenario_operator_in_the_objectiv "expression", [ var("x") <= 0, - comp_var("c", "x"), - comp_param("c", "x"), port_field("p", "f"), port_field("p", "f").sum_connections(), ], @@ -215,3 +217,121 @@ def test_constraint_equals() -> None: assert Constraint(name="c", expression=var("x") <= param("p")) != Constraint( name="c", expression=var("y") <= param("p") ) + + +# --- Issue #76: tolerate absence of expec() in objective contributions --- + + +def test_objective_without_expec_on_scenario_var_emits_warning_and_wraps() -> None: + """ + When a scenario-dependent variable is used in an objective contribution + without expec(), the model() factory should auto-wrap with expec() and + emit a UserWarning. + """ + scenario_var = float_variable("generation", structure=IndexingStructure(True, True)) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + m = model( + id="auto_wrap_model", + variables=[scenario_var], + objective_contributions={"operational": var("generation").time_sum()}, + ) + user_warnings = [w for w in caught if issubclass(w.category, UserWarning)] + assert len(user_warnings) == 1 + assert "expec()" in str(user_warnings[0].message) + assert "operational" in str(user_warnings[0].message) + # The stored expression must now be wrapped in expec() + stored = m.objective_contributions["operational"] + assert isinstance(stored, ScenarioOperatorNode) + assert stored.name == "Expectation" + + +def test_objective_with_explicit_expec_emits_no_warning() -> None: + """ + When expec() is already present in the objective contribution, + no warning should be emitted and the expression is not double-wrapped. + """ + scenario_var = float_variable("generation", structure=IndexingStructure(True, True)) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + m = model( + id="explicit_expec_model", + variables=[scenario_var], + objective_contributions={ + "operational": var("generation").time_sum().expec() + }, + ) + user_warnings = [w for w in caught if issubclass(w.category, UserWarning)] + assert len(user_warnings) == 0 + stored = m.objective_contributions["operational"] + assert isinstance(stored, ScenarioOperatorNode) + assert stored.name == "Expectation" + # Must not be double-wrapped + assert not isinstance(stored.operand, ScenarioOperatorNode) + + +def test_objective_with_non_scenario_var_emits_no_warning() -> None: + """ + When the objective contribution is already a scalar (no scenario dimension), + no auto-wrapping or warning should occur. + """ + non_scenario_var = float_variable( + "generation", structure=IndexingStructure(True, False) + ) + with warnings.catch_warnings(record=True) as caught: + warnings.simplefilter("always") + model( + id="non_scenario_model", + variables=[non_scenario_var], + objective_contributions={"operational": var("generation").time_sum()}, + ) + user_warnings = [w for w in caught if issubclass(w.category, UserWarning)] + assert len(user_warnings) == 0 + + +def test_objective_with_time_dimension_remaining_is_still_rejected() -> None: + """ + Auto-wrapping only applies when scenario=True and time=False. + If time dimension is still present (missing time_sum()), validation must fail. + """ + scenario_var = float_variable("generation", structure=IndexingStructure(True, True)) + with pytest.raises(ValueError, match="real-valued expression"): + model( + id="bad_time_model", + variables=[scenario_var], + # No time_sum() — still has time dimension → rejected regardless + objective_contributions={"operational": var("generation")}, + ) + + +# --- Variable factory functions --- + + +def test_bool_var_has_binary_type_and_unit_bounds() -> None: + """bool_var() creates a Variable with BINARY type and bounds [0, 1].""" + v = bool_var("on_off") + assert v.name == "on_off" + assert v.data_type == ValueType.BINARY + assert v.lower_bound == literal(0) + assert v.upper_bound == literal(1) + + +def test_bool_var_default_structure_is_time_and_scenario() -> None: + v = bool_var("flag") + assert v.structure == IndexingStructure(True, True) + + +def test_int_variable_has_integer_type() -> None: + """int_variable() creates a Variable with INTEGER type.""" + v = int_variable("count", lower_bound=literal(0), upper_bound=literal(10)) + assert v.data_type == ValueType.INTEGER + assert v.lower_bound == literal(0) + assert v.upper_bound == literal(10) + + +def test_variable_eq_with_non_variable_returns_false() -> None: + """Variable.__eq__ returns False when compared with a non-Variable object.""" + v = float_variable("x") + assert v != "x" + assert v != 42 + assert v != None # noqa: E711 diff --git a/tests/unittests/system/test_port.py b/tests/unittests/system/test_port.py index ce2448d2..8350400d 100644 --- a/tests/unittests/system/test_port.py +++ b/tests/unittests/system/test_port.py @@ -15,7 +15,7 @@ from gems.expression import literal from gems.expression.expression import port_field from gems.model import Constraint, ModelPort, PortType, model -from gems.study import Node, PortRef, PortsConnection, create_component +from gems.study import Component, PortRef, PortsConnection, create_component from tests.unittests.system.libs.standard import DEMAND_MODEL @@ -32,7 +32,7 @@ def test_port_type_compatibility_ko() -> None: ) ], ) - node = Node(id="N", model=NODE_BALANCE_MODEL_FAKE) + node = Component(id="N", model=NODE_BALANCE_MODEL_FAKE) demand = create_component( model=DEMAND_MODEL, id="D", diff --git a/tests/unittests/system/test_network.py b/tests/unittests/system/test_system.py similarity index 57% rename from tests/unittests/system/test_network.py rename to tests/unittests/system/test_system.py index f240ca8b..0cca5697 100644 --- a/tests/unittests/system/test_network.py +++ b/tests/unittests/system/test_system.py @@ -17,7 +17,7 @@ from gems.model.library import Library from gems.model.parsing import parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.study.network import Network, Node +from gems.study.system import Component, System @pytest.fixture(scope="session") @@ -36,26 +36,22 @@ def lib_dict(libs_dir: Path) -> dict[str, Library]: return lib_dict -def test_network(lib_dict: dict[str, Library]) -> None: +def test_system(lib_dict: dict[str, Library]) -> None: # This test could be done without parsing the yaml lib, ie. by giving models directly as Python object - network = Network("test") - assert network.id == "test" - assert list(network.nodes) == [] - assert list(network.components) == [] - assert list(network.all_components) == [] - assert list(network.connections) == [] - - with pytest.raises(KeyError): - network.get_node("N") - - node_model = lib_dict["basic"].models["node"] - - N1 = Node(model=node_model, id="N1") - N2 = Node(model=node_model, id="N2") - network.add_node(N1) - network.add_node(N2) - assert list(network.nodes) == [N1, N2] - assert network.get_node(N1.id) == N1 - assert network.get_component("N1") == Node(model=node_model, id="N1") + system = System("test") + assert system.id == "test" + assert list(system.components) == [] + assert list(system.all_components) == [] + assert list(system.connections) == [] + + node_model = lib_dict["basic"].models["basic.node"] + + N1 = Component(model=node_model, id="N1") + N2 = Component(model=node_model, id="N2") + system.add_component(N1) + system.add_component(N2) + assert list(system.components) == [N1, N2] + assert system.get_component(N1.id) == N1 + assert system.get_component("N1") == Component(model=node_model, id="N1") with pytest.raises(KeyError): - network.get_component("unknown") + system.get_component("unknown") diff --git a/tests/unittests/system_parsing/systems/system.yml b/tests/unittests/system_parsing/systems/system.yml index d48c5cc0..5dbbbb7e 100644 --- a/tests/unittests/system_parsing/systems/system.yml +++ b/tests/unittests/system_parsing/systems/system.yml @@ -11,11 +11,9 @@ # This file is part of the Antares project. system: model-libraries: basic - nodes: + components: - id: N model: basic.node - - components: - id: G model: basic.generator parameters: diff --git a/tests/unittests/system_parsing/test_components_parsing.py b/tests/unittests/system_parsing/test_components_parsing.py index e3103af7..1a58d95b 100644 --- a/tests/unittests/system_parsing/test_components_parsing.py +++ b/tests/unittests/system_parsing/test_components_parsing.py @@ -3,22 +3,22 @@ import pytest from yaml import dump, safe_load -from gems.model.parsing import InputLibrary, parse_yaml_library +from gems.model.parsing import LibrarySchema, parse_yaml_library from gems.model.resolve_library import resolve_library -from gems.study.parsing import InputSystem, load_input_system, parse_yaml_components +from gems.study.parsing import SystemSchema, load_input_system, parse_yaml_components from gems.study.resolve_components import consistency_check, resolve_system COMPO_FILE = Path(__file__).parent / "systems/system.yml" @pytest.fixture -def input_system() -> InputSystem: +def input_system() -> SystemSchema: with COMPO_FILE.open() as c: return parse_yaml_components(c) @pytest.fixture -def input_library() -> InputLibrary: +def input_library() -> LibrarySchema: library = Path(__file__).parent / "libs/lib_unittest.yml" with library.open() as lib: @@ -26,25 +26,24 @@ def input_library() -> InputLibrary: def test_parsing_components_ok( - input_system: InputSystem, input_library: InputLibrary + input_system: SystemSchema, input_library: LibrarySchema ) -> None: - assert len(input_system.components) == 2 - assert len(input_system.nodes) == 1 + assert len(input_system.components) == 3 + assert input_system.connections is not None assert len(input_system.connections) == 2 lib_dict = resolve_library([input_library]) result = resolve_system(input_system, lib_dict) - assert len(result.components) == 2 - assert len(result.nodes) == 1 + assert len(result.components) == 3 assert len(result.connections) == 2 def test_consistency_check_ok( - input_system: InputSystem, input_library: InputLibrary + input_system: SystemSchema, input_library: LibrarySchema ) -> None: result_lib = resolve_library([input_library]) result_system = resolve_system(input_system, result_lib) - consistency_check(result_system.components, result_lib["basic"].models) + consistency_check(result_system, result_lib["basic"].models) def test_load_input_system_ok(tmp_path: Path) -> None: @@ -55,10 +54,11 @@ def test_load_input_system_ok(tmp_path: Path) -> None: result = load_input_system(file_for_load) - assert isinstance(result, InputSystem) - assert len(result.components) == 2 - assert result.components[0].id == "G" - assert result.components[1].id == "D" + assert isinstance(result, SystemSchema) + assert len(result.components) == 3 + assert result.components[0].id == "N" + assert result.components[1].id == "G" + assert result.components[2].id == "D" assert result.connections is not None assert len(result.connections) == 2 @@ -82,13 +82,13 @@ def test_load_input_system_missing_file_raises_error() -> None: def test_consistency_check_ko( - input_system: InputSystem, input_library: InputLibrary + input_system: SystemSchema, input_library: LibrarySchema ) -> None: result_lib = resolve_library([input_library]) result_comp = resolve_system(input_system, result_lib) - result_lib["basic"].models.pop("generator") + result_lib["basic"].models.pop("basic.generator") with pytest.raises( ValueError, - match=r"Error: Component G has invalid model ID: generator", + match=r"Error: Component G has invalid model ID: basic.generator", ): - consistency_check(result_comp.components, result_lib["basic"].models) + consistency_check(result_comp, result_lib["basic"].models) diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..c465b551 --- /dev/null +++ b/uv.lock @@ -0,0 +1,2768 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.11'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/5f/2cdf6f7aca3b20d3f316e9f505292e1f256a32089bd702034c29ebde6242/antlr4_python3_runtime-4.13.2.tar.gz", hash = "sha256:909b647e1d2fc2b70180ac586df3933e38919c85f98ccc656a96cd3f25ef3916", size = 117467, upload-time = "2024-08-03T19:00:12.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/03/a851e84fcbb85214dc637b6378121ef9a0dd61b4c65264675d8a5c9b1ae7/antlr4_python3_runtime-4.13.2-py3-none-any.whl", hash = "sha256:fe3835eb8d33daece0e799090eda89719dbccee7aa39ef94eed3818cafa5a7e8", size = 144462, upload-time = "2024-08-03T19:00:11.134Z" }, +] + +[[package]] +name = "antlr4-tools" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "install-jdk" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b8/7c/6033a383b196b885476210ba6ba72b501c439508ea81f1560a94ea116834/antlr4_tools-0.2.2.tar.gz", hash = "sha256:8af6fba512fc168e48eb93690ed21bdbe8848ed812d12b365d9d259a26ad6ba3", size = 6177, upload-time = "2025-04-27T16:43:22.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/e9/f3d327df348a906a83201d2a5e559194945135879b6920dbbce9c1d6fe79/antlr4_tools-0.2.2-py3-none-any.whl", hash = "sha256:79a0b971cf8337db49df076495332e9e401e2217d114ee1ac7797d3b28df001b", size = 4405, upload-time = "2025-04-27T16:43:21.527Z" }, +] + +[[package]] +name = "anytree" +version = "2.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bc/a8/eb55fab589c56f9b6be2b3fd6997aa04bb6f3da93b01154ce6fc8e799db2/anytree-2.13.0.tar.gz", hash = "sha256:c9d3aa6825fdd06af7ebb05b4ef291d2db63e62bb1f9b7d9b71354be9d362714", size = 48389, upload-time = "2025-04-08T21:06:30.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/98/f6aa7fe0783e42be3093d8ef1b0ecdc22c34c0d69640dfb37f56925cb141/anytree-2.13.0-py3-none-any.whl", hash = "sha256:4cbcf10df36b1f1cba131b7e487ff3edafc9d6e932a3c70071b5b768bab901ff", size = 45077, upload-time = "2025-04-08T21:06:29.494Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "babel" +version = "2.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/b2/51899539b6ceeeb420d40ed3cd4b7a40519404f9baf3d4ac99dc413a834b/babel-2.18.0.tar.gz", hash = "sha256:b80b99a14bd085fcacfa15c9165f651fbb3406e66cc603abf11c5750937c992d", size = 9959554, upload-time = "2026-02-01T12:30:56.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, +] + +[[package]] +name = "backrefs" +version = "6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/a6/e325ec73b638d3ede4421b5445d4a0b8b219481826cc079d510100af356c/backrefs-6.2.tar.gz", hash = "sha256:f44ff4d48808b243b6c0cdc6231e22195c32f77046018141556c66f8bab72a49", size = 7012303, upload-time = "2026-02-16T19:10:15.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/39/3765df263e08a4df37f4f43cb5aa3c6c17a4bdd42ecfe841e04c26037171/backrefs-6.2-py310-none-any.whl", hash = "sha256:0fdc7b012420b6b144410342caeb8adc54c6866cf12064abc9bb211302e496f8", size = 381075, upload-time = "2026-02-16T19:10:04.322Z" }, + { url = "https://files.pythonhosted.org/packages/0f/f0/35240571e1b67ffb19dafb29ab34150b6f59f93f717b041082cdb1bfceb1/backrefs-6.2-py311-none-any.whl", hash = "sha256:08aa7fae530c6b2361d7bdcbda1a7c454e330cc9dbcd03f5c23205e430e5c3be", size = 392874, upload-time = "2026-02-16T19:10:06.314Z" }, + { url = "https://files.pythonhosted.org/packages/e3/63/77e8c9745b4d227cce9f5e0a6f68041278c5f9b18588b35905f5f19c1beb/backrefs-6.2-py312-none-any.whl", hash = "sha256:c3f4b9cb2af8cda0d87ab4f57800b57b95428488477be164dd2b47be54db0c90", size = 398787, upload-time = "2026-02-16T19:10:08.274Z" }, + { url = "https://files.pythonhosted.org/packages/c5/71/c754b1737ad99102e03fa3235acb6cb6d3ac9d6f596cbc3e5f236705abd8/backrefs-6.2-py313-none-any.whl", hash = "sha256:12df81596ab511f783b7d87c043ce26bc5b0288cf3bb03610fe76b8189282b2b", size = 400747, upload-time = "2026-02-16T19:10:09.791Z" }, + { url = "https://files.pythonhosted.org/packages/af/75/be12ba31a6eb20dccef2320cd8ccb3f7d9013b68ba4c70156259fee9e409/backrefs-6.2-py314-none-any.whl", hash = "sha256:e5f805ae09819caa1aa0623b4a83790e7028604aa2b8c73ba602c4454e665de7", size = 412602, upload-time = "2026-02-16T19:10:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/21/f8/d02f650c47d05034dcd6f9c8cf94f39598b7a89c00ecda0ecb2911bc27e9/backrefs-6.2-py39-none-any.whl", hash = "sha256:664e33cd88c6840b7625b826ecf2555f32d491800900f5a541f772c485f7cda7", size = 381077, upload-time = "2026-02-16T19:10:13.74Z" }, +] + +[[package]] +name = "black" +version = "26.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "mypy-extensions" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "platformdirs" }, + { name = "pytokens" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/a8/11170031095655d36ebc6664fe0897866f6023892396900eec0e8fdc4299/black-26.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:86a8b5035fce64f5dcd1b794cf8ec4d31fe458cf6ce3986a30deb434df82a1d2", size = 1866562, upload-time = "2026-03-12T03:39:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/69/ce/9e7548d719c3248c6c2abfd555d11169457cbd584d98d179111338423790/black-26.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5602bdb96d52d2d0672f24f6ffe5218795736dd34807fd0fd55ccd6bf206168b", size = 1703623, upload-time = "2026-03-12T03:40:00.347Z" }, + { url = "https://files.pythonhosted.org/packages/7f/0a/8d17d1a9c06f88d3d030d0b1d4373c1551146e252afe4547ed601c0e697f/black-26.3.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c54a4a82e291a1fee5137371ab488866b7c86a3305af4026bdd4dc78642e1ac", size = 1768388, upload-time = "2026-03-12T03:40:01.765Z" }, + { url = "https://files.pythonhosted.org/packages/52/79/c1ee726e221c863cde5164f925bacf183dfdf0397d4e3f94889439b947b4/black-26.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:6e131579c243c98f35bce64a7e08e87fb2d610544754675d4a0e73a070a5aa3a", size = 1412969, upload-time = "2026-03-12T03:40:03.252Z" }, + { url = "https://files.pythonhosted.org/packages/73/a5/15c01d613f5756f68ed8f6d4ec0a1e24b82b18889fa71affd3d1f7fad058/black-26.3.1-cp310-cp310-win_arm64.whl", hash = "sha256:5ed0ca58586c8d9a487352a96b15272b7fa55d139fc8496b519e78023a8dab0a", size = 1220345, upload-time = "2026-03-12T03:40:04.892Z" }, + { url = "https://files.pythonhosted.org/packages/17/57/5f11c92861f9c92eb9dddf515530bc2d06db843e44bdcf1c83c1427824bc/black-26.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:28ef38aee69e4b12fda8dba75e21f9b4f979b490c8ac0baa7cb505369ac9e1ff", size = 1851987, upload-time = "2026-03-12T03:40:06.248Z" }, + { url = "https://files.pythonhosted.org/packages/54/aa/340a1463660bf6831f9e39646bf774086dbd8ca7fc3cded9d59bbdf4ad0a/black-26.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bf162ed91a26f1adba8efda0b573bc6924ec1408a52cc6f82cb73ec2b142c", size = 1689499, upload-time = "2026-03-12T03:40:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/01/b726c93d717d72733da031d2de10b92c9fa4c8d0c67e8a8a372076579279/black-26.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:474c27574d6d7037c1bc875a81d9be0a9a4f9ee95e62800dab3cfaadbf75acd5", size = 1754369, upload-time = "2026-03-12T03:40:09.279Z" }, + { url = "https://files.pythonhosted.org/packages/e3/09/61e91881ca291f150cfc9eb7ba19473c2e59df28859a11a88248b5cbbc4d/black-26.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:5e9d0d86df21f2e1677cc4bd090cd0e446278bcbbe49bf3659c308c3e402843e", size = 1413613, upload-time = "2026-03-12T03:40:10.943Z" }, + { url = "https://files.pythonhosted.org/packages/16/73/544f23891b22e7efe4d8f812371ab85b57f6a01b2fc45e3ba2e52ba985b8/black-26.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9a5e9f45e5d5e1c5b5c29b3bd4265dcc90e8b92cf4534520896ed77f791f4da5", size = 1219719, upload-time = "2026-03-12T03:40:12.597Z" }, + { url = "https://files.pythonhosted.org/packages/dc/f8/da5eae4fc75e78e6dceb60624e1b9662ab00d6b452996046dfa9b8a6025b/black-26.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b5e6f89631eb88a7302d416594a32faeee9fb8fb848290da9d0a5f2903519fc1", size = 1895920, upload-time = "2026-03-12T03:40:13.921Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9f/04e6f26534da2e1629b2b48255c264cabf5eedc5141d04516d9d68a24111/black-26.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cd2012d35b47d589cb8a16faf8a32ef7a336f56356babd9fcf70939ad1897f", size = 1718499, upload-time = "2026-03-12T03:40:15.239Z" }, + { url = "https://files.pythonhosted.org/packages/04/91/a5935b2a63e31b331060c4a9fdb5a6c725840858c599032a6f3aac94055f/black-26.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f76ff19ec5297dd8e66eb64deda23631e642c9393ab592826fd4bdc97a4bce7", size = 1794994, upload-time = "2026-03-12T03:40:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/86e462cdd311a3c2a8ece708d22aba17d0b2a0d5348ca34b40cdcbea512e/black-26.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ddb113db38838eb9f043623ba274cfaf7d51d5b0c22ecb30afe58b1bb8322983", size = 1420867, upload-time = "2026-03-12T03:40:18.83Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e5/22515a19cb7eaee3440325a6b0d95d2c0e88dd180cb011b12ae488e031d1/black-26.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:dfdd51fc3e64ea4f35873d1b3fb25326773d55d2329ff8449139ebaad7357efb", size = 1230124, upload-time = "2026-03-12T03:40:20.425Z" }, + { url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" }, + { url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" }, + { url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" }, + { url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" }, + { url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" }, + { url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" }, +] + +[[package]] +name = "bottleneck" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/d8/6d641573e210768816023a64966d66463f2ce9fc9945fa03290c8a18f87c/bottleneck-1.6.0.tar.gz", hash = "sha256:028d46ee4b025ad9ab4d79924113816f825f62b17b87c9e1d0d8ce144a4a0e31", size = 104311, upload-time = "2025-09-08T16:30:38.617Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/38/144fb32c9efb196f651ddb30e7c48f6047a86972e5b350f3f10c9a5f6a16/bottleneck-1.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40de6be68218ba32cd15addbf4ad7bbbf0075b5c5c4347c579aeae110a5c9a96", size = 100393, upload-time = "2025-09-08T16:29:35.466Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e3/dbbf4b102f4e6aaf49ad3749a6d778f309473a2950c5ce3bb20b94f2ba84/bottleneck-1.6.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1ad1882ba8c8da1f404de2610b45b05291e39eec56150270b03b5b25cf2bbb7f", size = 371509, upload-time = "2025-09-08T16:29:37.037Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/60fcbddee5fdf32923ba33ce2337a4cf12834b69de4f8e07219b5ef7c931/bottleneck-1.6.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f29b14b0ba5a816df6ab559add415c88ea8cf2146364e55f5f4c24ff7c85e494", size = 363480, upload-time = "2025-09-08T16:29:38.311Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/a25434dcadf083e05b0c71ece2de71fad5521268f905721e06e0a7efc5db/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17c227ed361cf9a2ab3751a727620298faca9a1e33dd76711ae80834cf34b254", size = 357120, upload-time = "2025-09-08T16:29:39.541Z" }, + { url = "https://files.pythonhosted.org/packages/8e/b9/99580349c827695dfc094ac672eedba6e1ca244b6e745ff7447c0239d6d8/bottleneck-1.6.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d278b5633cea38bdae6eaf7df23d54ecb5e4db52f2ebc13fe40c0e738842f2a1", size = 367579, upload-time = "2025-09-08T16:29:40.695Z" }, + { url = "https://files.pythonhosted.org/packages/95/06/6326994249388ceb2400d07c6a96a50941749d2d9ec80da22a99046e3a38/bottleneck-1.6.0-cp310-cp310-win32.whl", hash = "sha256:26c87c2f6364d82b67eab7218f0346e9c42f336088ca4e19d77dc76eecf272fc", size = 107838, upload-time = "2025-09-08T16:29:41.907Z" }, + { url = "https://files.pythonhosted.org/packages/2f/75/8f0e8e266ea99ffbc69500a927f0c114a07fe465bfbc59871d6fe22d9ee0/bottleneck-1.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:9d33bcd60a13d0603f5db9d953352a3c098242c46f8f919290fd11c54b42b9e5", size = 113364, upload-time = "2025-09-08T16:29:43.437Z" }, + { url = "https://files.pythonhosted.org/packages/83/96/9d51012d729f97de1e75aad986f3ba50956742a40fc99cbab4c2aa896c1c/bottleneck-1.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:69ef4514782afe39db2497aaea93b1c167ab7ab3bc5e3930500ef9cf11841db7", size = 100400, upload-time = "2025-09-08T16:29:44.464Z" }, + { url = "https://files.pythonhosted.org/packages/16/f4/4fcbebcbc42376a77e395a6838575950587e5eb82edf47d103f8daa7ba22/bottleneck-1.6.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:727363f99edc6dc83d52ed28224d4cb858c07a01c336c7499c0c2e5dd4fd3e4a", size = 375920, upload-time = "2025-09-08T16:29:45.52Z" }, + { url = "https://files.pythonhosted.org/packages/36/13/7fa8cdc41cbf2dfe0540f98e1e0caf9ffbd681b1a0fc679a91c2698adaf9/bottleneck-1.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:847671a9e392220d1dfd2ff2524b4d61ec47b2a36ea78e169d2aa357fd9d933a", size = 367922, upload-time = "2025-09-08T16:29:46.743Z" }, + { url = "https://files.pythonhosted.org/packages/13/7d/dccfa4a2792c1bdc0efdde8267e527727e517df1ff0d4976b84e0268c2f9/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:daef2603ab7b4ec4f032bb54facf5fa92dacd3a264c2fd9677c9fc22bcb5a245", size = 361379, upload-time = "2025-09-08T16:29:48.042Z" }, + { url = "https://files.pythonhosted.org/packages/93/42/21c0fad823b71c3a8904cbb847ad45136d25573a2d001a9cff48d3985fab/bottleneck-1.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fc7f09bda980d967f2e9f1a746eda57479f824f66de0b92b9835c431a8c922d4", size = 371911, upload-time = "2025-09-08T16:29:49.366Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b0/830ff80f8c74577d53034c494639eac7a0ffc70935c01ceadfbe77f590c2/bottleneck-1.6.0-cp311-cp311-win32.whl", hash = "sha256:1f78bad13ad190180f73cceb92d22f4101bde3d768f4647030089f704ae7cac7", size = 107831, upload-time = "2025-09-08T16:29:51.397Z" }, + { url = "https://files.pythonhosted.org/packages/6f/42/01d4920b0aa51fba503f112c90714547609bbe17b6ecfc1c7ae1da3183df/bottleneck-1.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:8f2adef59fdb9edf2983fe3a4c07e5d1b677c43e5669f4711da2c3daad8321ad", size = 113358, upload-time = "2025-09-08T16:29:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/8d/72/7e3593a2a3dd69ec831a9981a7b1443647acb66a5aec34c1620a5f7f8498/bottleneck-1.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3bb16a16a86a655fdbb34df672109a8a227bb5f9c9cf5bb8ae400a639bc52fa3", size = 100515, upload-time = "2025-09-08T16:29:55.141Z" }, + { url = "https://files.pythonhosted.org/packages/b5/d4/e7bbea08f4c0f0bab819d38c1a613da5f194fba7b19aae3e2b3a27e78886/bottleneck-1.6.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0fbf5d0787af9aee6cef4db9cdd14975ce24bd02e0cc30155a51411ebe2ff35f", size = 377451, upload-time = "2025-09-08T16:29:56.718Z" }, + { url = "https://files.pythonhosted.org/packages/fe/80/a6da430e3b1a12fd85f9fe90d3ad8fe9a527ecb046644c37b4b3f4baacfc/bottleneck-1.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d08966f4a22384862258940346a72087a6f7cebb19038fbf3a3f6690ee7fd39f", size = 368303, upload-time = "2025-09-08T16:29:57.834Z" }, + { url = "https://files.pythonhosted.org/packages/30/11/abd30a49f3251f4538430e5f876df96f2b39dabf49e05c5836820d2c31fe/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:604f0b898b43b7bc631c564630e936a8759d2d952641c8b02f71e31dbcd9deaa", size = 361232, upload-time = "2025-09-08T16:29:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ac/1c0e09d8d92b9951f675bd42463ce76c3c3657b31c5bf53ca1f6dd9eccff/bottleneck-1.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d33720bad761e642abc18eda5f188ff2841191c9f63f9d0c052245decc0faeb9", size = 373234, upload-time = "2025-09-08T16:30:00.488Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ea/382c572ae3057ba885d484726bb63629d1f63abedf91c6cd23974eb35a9b/bottleneck-1.6.0-cp312-cp312-win32.whl", hash = "sha256:a1e5907ec2714efbe7075d9207b58c22ab6984a59102e4ecd78dced80dab8374", size = 108020, upload-time = "2025-09-08T16:30:01.773Z" }, + { url = "https://files.pythonhosted.org/packages/48/ad/d71da675eef85ac153eef5111ca0caa924548c9591da00939bcabba8de8e/bottleneck-1.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:81e3822499f057a917b7d3972ebc631ac63c6bbcc79ad3542a66c4c40634e3a6", size = 113493, upload-time = "2025-09-08T16:30:02.872Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/e117cd5ff7056126d3291deb29ac8066476e60b852555b95beb3fc9d62a0/bottleneck-1.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d015de414ca016ebe56440bdf5d3d1204085080527a3c51f5b7b7a3e704fe6fd", size = 100521, upload-time = "2025-09-08T16:30:03.89Z" }, + { url = "https://files.pythonhosted.org/packages/bd/22/05555a9752357e24caa1cd92324d1a7fdde6386aab162fcc451f8f8eedc2/bottleneck-1.6.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:456757c9525b0b12356f472e38020ed4b76b18375fd76e055f8d33fb62956f5e", size = 377719, upload-time = "2025-09-08T16:30:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/11/ee/76593af47097d9633109bed04dbcf2170707dd84313ca29f436f9234bc51/bottleneck-1.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c65254d51b6063c55f6272f175e867e2078342ae75f74be29d6612e9627b2c0", size = 368577, upload-time = "2025-09-08T16:30:06.387Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f7/4dcacaf637d2b8d89ea746c74159adda43858d47358978880614c3fa4391/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a172322895fbb79c6127474f1b0db0866895f0b804a18d5c6b841fea093927fe", size = 361441, upload-time = "2025-09-08T16:30:07.613Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/21eb1eb1c42cb7be2872d0647c292fc75768d14e1f0db66bf907b24b2464/bottleneck-1.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d5e81b642eb0d5a5bf00312598d7ed142d389728b694322a118c26813f3d1fa9", size = 373416, upload-time = "2025-09-08T16:30:08.899Z" }, + { url = "https://files.pythonhosted.org/packages/48/cb/7957ff40367a151139b5f1854616bf92e578f10804d226fbcdecfd73aead/bottleneck-1.6.0-cp313-cp313-win32.whl", hash = "sha256:543d3a89d22880cd322e44caff859af6c0489657bf9897977d1f5d3d3f77299c", size = 108029, upload-time = "2025-09-08T16:30:09.909Z" }, + { url = "https://files.pythonhosted.org/packages/90/a8/735df4156fa5595501d5d96a6ee102f49c13d2ce9e2a287ad51806bc3ba0/bottleneck-1.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:48a44307d604ceb81e256903e5d57d3adb96a461b1d3c6a69baa2c67e823bd36", size = 113497, upload-time = "2025-09-08T16:30:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5c/8c1260df8ade7cebc2a8af513a27082b5e36aa4a5fb762d56ea6d969d893/bottleneck-1.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:547e6715115867c4657c9ae8cc5ddac1fec8fdad66690be3a322a7488721b06b", size = 101606, upload-time = "2025-09-08T16:30:11.935Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ea/f03e2944e91ee962922c834ed21e5be6d067c8395681f5dc6c67a0a26853/bottleneck-1.6.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5e4a4a6e05b6f014c307969129e10d1a0afd18f3a2c127b085532a4a76677aef", size = 391804, upload-time = "2025-09-08T16:30:13.13Z" }, + { url = "https://files.pythonhosted.org/packages/0b/58/2b356b8a81eb97637dccee6cf58237198dd828890e38be9afb4e5e58e38e/bottleneck-1.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2baae0d1589b4a520b2f9cf03528c0c8b20717b3f05675e212ec2200cf628f12", size = 383443, upload-time = "2025-09-08T16:30:14.318Z" }, + { url = "https://files.pythonhosted.org/packages/55/52/cf7d09ed3736ad0d50c624787f9b580ae3206494d95cc0f4814b93eef728/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2e407139b322f01d8d5b6b2e8091b810f48a25c7fa5c678cfcdc420dfe8aea0a", size = 375458, upload-time = "2025-09-08T16:30:15.379Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e9/7c87a34a24e339860064f20fac49f6738e94f1717bc8726b9c47705601d8/bottleneck-1.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1adefb89b92aba6de9c6ea871d99bcd29d519f4fb012cc5197917813b4fc2c7f", size = 386384, upload-time = "2025-09-08T16:30:17.012Z" }, + { url = "https://files.pythonhosted.org/packages/59/57/db51855e18a47671801180be748939b4c9422a0544849af1919116346b5f/bottleneck-1.6.0-cp313-cp313t-win32.whl", hash = "sha256:64b8690393494074923780f6abdf5f5577d844b9d9689725d1575a936e74e5f0", size = 109448, upload-time = "2025-09-08T16:30:18.076Z" }, + { url = "https://files.pythonhosted.org/packages/bd/1e/683c090b624f13a5bf88a0be2241dc301e98b2fb72a45812a7ae6e456cc4/bottleneck-1.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:cb67247f65dcdf62af947c76c6c8b77d9f0ead442cac0edbaa17850d6da4e48d", size = 115190, upload-time = "2025-09-08T16:30:19.106Z" }, + { url = "https://files.pythonhosted.org/packages/77/e2/eb7c08964a3f3c4719f98795ccd21807ee9dd3071a0f9ad652a5f19196ff/bottleneck-1.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:98f1d789042511a0f042b3bdcd2903e8567e956d3aa3be189cce3746daeb8550", size = 100544, upload-time = "2025-09-08T16:30:20.22Z" }, + { url = "https://files.pythonhosted.org/packages/99/ec/c6f3be848f37689f481797ce7d9807d5f69a199d7fc0e46044f9b708c468/bottleneck-1.6.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1fad24c99e39ad7623fc2a76d37feb26bd32e4dd170885edf4dbf4bfce2199a3", size = 378315, upload-time = "2025-09-08T16:30:21.409Z" }, + { url = "https://files.pythonhosted.org/packages/bf/8f/2d6600836e2ea8f14fcefac592dc83497e5b88d381470c958cb9cdf88706/bottleneck-1.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643e61e50a6f993debc399b495a1609a55b3bd76b057e433e4089505d9f605c7", size = 368978, upload-time = "2025-09-08T16:30:23.458Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/bf72b49f5040212873b985feef5050015645e0a02204b591e1d265fc522a/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa668efbe4c6b200524ea0ebd537212da9b9801287138016fdf64119d6fcf201", size = 362074, upload-time = "2025-09-08T16:30:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c8/c4891a0604eb680031390182c6e264247e3a9a8d067d654362245396fadf/bottleneck-1.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9f7dd35262e89e28fedd79d45022394b1fa1aceb61d2e747c6d6842e50546daa", size = 374019, upload-time = "2025-09-08T16:30:26.438Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2d/ed096f8d1b9147e84914045dd89bc64e3c32eee49b862d1e20d573a9ab0d/bottleneck-1.6.0-cp314-cp314-win32.whl", hash = "sha256:bd90bec3c470b7fdfafc2fbdcd7a1c55a4e57b5cdad88d40eea5bc9bab759bf1", size = 110173, upload-time = "2025-09-08T16:30:27.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/70/1414acb6ae378a15063cfb19a0a39d69d1b6baae1120a64d2b069902549b/bottleneck-1.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:b43b6d36a62ffdedc6368cf9a708e4d0a30d98656c2b5f33d88894e1bcfd6857", size = 115899, upload-time = "2025-09-08T16:30:28.524Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ed/4570b5d8c1c85ce3c54963ebc37472231ed54f0b0d8dbb5dde14303f775f/bottleneck-1.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:53296707a8e195b5dcaa804b714bd222b5e446bd93cd496008122277eb43fa87", size = 101615, upload-time = "2025-09-08T16:30:29.556Z" }, + { url = "https://files.pythonhosted.org/packages/2d/93/c148faa07ae91f266be1f3fad1fde95aa2449e12937f3f3df2dd720b86e0/bottleneck-1.6.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6df19cc48a83efd70f6d6874332aa31c3f5ca06a98b782449064abbd564cf0e", size = 392411, upload-time = "2025-09-08T16:30:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1c/e6ad221d345a059e7efb2ad1d46a22d9fdae0486faef70555766e1123966/bottleneck-1.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96bb3a52cb3c0aadfedce3106f93ab940a49c9d35cd4ed612e031f6deb27e80f", size = 384022, upload-time = "2025-09-08T16:30:32.364Z" }, + { url = "https://files.pythonhosted.org/packages/4f/40/5b15c01eb8c59d59bc84c94d01d3d30797c961f10ec190f53c27e05d62ab/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d1db9e831b69d5595b12e79aeb04cb02873db35576467c8dd26cdc1ee6b74581", size = 376004, upload-time = "2025-09-08T16:30:33.731Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/cb228f5949553a5c01d1d5a3c933f0216d78540d9e0bf8dd4343bb449681/bottleneck-1.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4dd7ac619570865fcb7a0e8925df418005f076286ad2c702dd0f447231d7a055", size = 386909, upload-time = "2025-09-08T16:30:34.973Z" }, + { url = "https://files.pythonhosted.org/packages/09/9a/425065c37a67a9120bf53290371579b83d05bf46f3212cce65d8c01d470a/bottleneck-1.6.0-cp314-cp314t-win32.whl", hash = "sha256:7fb694165df95d428fe00b98b9ea7d126ef786c4a4b7d43ae2530248396cadcb", size = 111636, upload-time = "2025-09-08T16:30:36.044Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/c41006e42909ec5114a8961818412310aa54646d1eae0495dbff3598a095/bottleneck-1.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:174b80930ce82bd8456c67f1abb28a5975c68db49d254783ce2cb6983b4fea40", size = 117611, upload-time = "2025-09-08T16:30:37.055Z" }, +] + +[[package]] +name = "certifi" +version = "2026.2.25" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" }, + { url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" }, + { url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" }, + { url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" }, + { url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" }, + { url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" }, + { url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" }, + { url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" }, + { url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" }, + { url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" }, + { url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" }, + { url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" }, + { url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" }, + { url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" }, + { url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" }, + { url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" }, + { url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" }, + { url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" }, + { url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" }, + { url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" }, + { url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" }, + { url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" }, + { url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" }, + { url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" }, + { url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" }, + { url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" }, + { url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" }, + { url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" }, + { url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" }, + { url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" }, + { url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" }, + { url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" }, + { url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" }, + { url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" }, + { url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" }, + { url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" }, + { url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" }, + { url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" }, + { url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" }, + { url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" }, + { url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" }, + { url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" }, + { url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" }, + { url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" }, + { url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" }, + { url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" }, + { url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" }, + { url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" }, + { url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" }, + { url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" }, + { url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" }, + { url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" }, + { url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" }, + { url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" }, + { url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" }, + { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, +] + +[[package]] +name = "click" +version = "8.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/57/75/31212c6bf2503fdf920d87fee5d7a86a2e3bcf444984126f13d8e4016804/click-8.3.2.tar.gz", hash = "sha256:14162b8b3b3550a7d479eafa77dfd3c38d9dc8951f6f69c78913a8f9a7540fd5", size = 302856, upload-time = "2026-04-03T19:14:45.118Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/20/71885d8b97d4f3dde17b1fdb92dbd4908b00541c5a3379787137285f602e/click-8.3.2-py3-none-any.whl", hash = "sha256:1924d2c27c5653561cd2cae4548d1406039cb79b858b747cfea24924bbc1616d", size = 108379, upload-time = "2026-04-03T19:14:43.505Z" }, +] + +[[package]] +name = "cloudpickle" +version = "3.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/27/fb/576f067976d320f5f0114a8d9fa1215425441bb35627b1993e5afd8111e5/cloudpickle-3.1.2.tar.gz", hash = "sha256:7fda9eb655c9c230dab534f1983763de5835249750e85fbcef43aaa30a9a2414", size = 22330, upload-time = "2025-11-03T09:25:26.604Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/39/799be3f2f0f38cc727ee3b4f1445fe6d5e4133064ec2e4115069418a5bb6/cloudpickle-3.1.2-py3-none-any.whl", hash = "sha256:9acb47f6afd73f60dc1df93bb801b472f05ff42fa6c84167d25cb206be1fbf4a", size = 22228, upload-time = "2025-11-03T09:25:25.534Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "coverage" +version = "7.13.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/33/e8c48488c29a73fd089f9d71f9653c1be7478f2ad6b5bc870db11a55d23d/coverage-7.13.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0723d2c96324561b9aa76fb982406e11d93cdb388a7a7da2b16e04719cf7ca5", size = 219255, upload-time = "2026-03-17T10:29:51.081Z" }, + { url = "https://files.pythonhosted.org/packages/da/bd/b0ebe9f677d7f4b74a3e115eec7ddd4bcf892074963a00d91e8b164a6386/coverage-7.13.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52f444e86475992506b32d4e5ca55c24fc88d73bcbda0e9745095b28ef4dc0cf", size = 219772, upload-time = "2026-03-17T10:29:52.867Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/5cb9502f4e01972f54eedd48218bb203fe81e294be606a2bc93970208013/coverage-7.13.5-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:704de6328e3d612a8f6c07000a878ff38181ec3263d5a11da1db294fa6a9bdf8", size = 246532, upload-time = "2026-03-17T10:29:54.688Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d8/3217636d86c7e7b12e126e4f30ef1581047da73140614523af7495ed5f2d/coverage-7.13.5-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a1a6d79a14e1ec1832cabc833898636ad5f3754a678ef8bb4908515208bf84f4", size = 248333, upload-time = "2026-03-17T10:29:56.221Z" }, + { url = "https://files.pythonhosted.org/packages/2b/30/2002ac6729ba2d4357438e2ed3c447ad8562866c8c63fc16f6dfc33afe56/coverage-7.13.5-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79060214983769c7ba3f0cee10b54c97609dca4d478fa1aa32b914480fd5738d", size = 250211, upload-time = "2026-03-17T10:29:57.938Z" }, + { url = "https://files.pythonhosted.org/packages/6c/85/552496626d6b9359eb0e2f86f920037c9cbfba09b24d914c6e1528155f7d/coverage-7.13.5-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:356e76b46783a98c2a2fe81ec79df4883a1e62895ea952968fb253c114e7f930", size = 252125, upload-time = "2026-03-17T10:29:59.388Z" }, + { url = "https://files.pythonhosted.org/packages/44/21/40256eabdcbccdb6acf6b381b3016a154399a75fe39d406f790ae84d1f3c/coverage-7.13.5-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cef0cdec915d11254a7f549c1170afecce708d30610c6abdded1f74e581666d", size = 247219, upload-time = "2026-03-17T10:30:01.199Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/96e2a6c3f21a0ea77d7830b254a1542d0328acc8d7bdf6a284ba7e529f77/coverage-7.13.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dc022073d063b25a402454e5712ef9e007113e3a676b96c5f29b2bda29352f40", size = 248248, upload-time = "2026-03-17T10:30:03.317Z" }, + { url = "https://files.pythonhosted.org/packages/da/ba/8477f549e554827da390ec659f3c38e4b6d95470f4daafc2d8ff94eaa9c2/coverage-7.13.5-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:9b74db26dfea4f4e50d48a4602207cd1e78be33182bc9cbf22da94f332f99878", size = 246254, upload-time = "2026-03-17T10:30:04.832Z" }, + { url = "https://files.pythonhosted.org/packages/55/59/bc22aef0e6aa179d5b1b001e8b3654785e9adf27ef24c93dc4228ebd5d68/coverage-7.13.5-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:ad146744ca4fd09b50c482650e3c1b1f4dfa1d4792e0a04a369c7f23336f0400", size = 250067, upload-time = "2026-03-17T10:30:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/de/1b/c6a023a160806a5137dca53468fd97530d6acad24a22003b1578a9c2e429/coverage-7.13.5-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:c555b48be1853fe3997c11c4bd521cdd9a9612352de01fa4508f16ec341e6fe0", size = 246521, upload-time = "2026-03-17T10:30:08.486Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/3532c85a55aa2f899fa17c186f831cfa1aa434d88ff792a709636f64130e/coverage-7.13.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7034b5c56a58ae5e85f23949d52c14aca2cfc6848a31764995b7de88f13a1ea0", size = 247126, upload-time = "2026-03-17T10:30:09.966Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2e/b9d56af4a24ef45dfbcda88e06870cb7d57b2b0bfa3a888d79b4c8debd76/coverage-7.13.5-cp310-cp310-win32.whl", hash = "sha256:eb7fdf1ef130660e7415e0253a01a7d5a88c9c4d158bcf75cbbd922fd65a5b58", size = 221860, upload-time = "2026-03-17T10:30:11.393Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cc/d938417e7a4d7f0433ad4edee8bb2acdc60dc7ac5af19e2a07a048ecbee3/coverage-7.13.5-cp310-cp310-win_amd64.whl", hash = "sha256:3e1bb5f6c78feeb1be3475789b14a0f0a5b47d505bfc7267126ccbd50289999e", size = 222788, upload-time = "2026-03-17T10:30:12.886Z" }, + { url = "https://files.pythonhosted.org/packages/4b/37/d24c8f8220ff07b839b2c043ea4903a33b0f455abe673ae3c03bbdb7f212/coverage-7.13.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66a80c616f80181f4d643b0f9e709d97bcea413ecd9631e1dedc7401c8e6695d", size = 219381, upload-time = "2026-03-17T10:30:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/35/8b/cd129b0ca4afe886a6ce9d183c44d8301acbd4ef248622e7c49a23145605/coverage-7.13.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:145ede53ccbafb297c1c9287f788d1bc3efd6c900da23bf6931b09eafc931587", size = 219880, upload-time = "2026-03-17T10:30:16.231Z" }, + { url = "https://files.pythonhosted.org/packages/55/2f/e0e5b237bffdb5d6c530ce87cc1d413a5b7d7dfd60fb067ad6d254c35c76/coverage-7.13.5-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0672854dc733c342fa3e957e0605256d2bf5934feeac328da9e0b5449634a642", size = 250303, upload-time = "2026-03-17T10:30:17.748Z" }, + { url = "https://files.pythonhosted.org/packages/92/be/b1afb692be85b947f3401375851484496134c5554e67e822c35f28bf2fbc/coverage-7.13.5-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec10e2a42b41c923c2209b846126c6582db5e43a33157e9870ba9fb70dc7854b", size = 252218, upload-time = "2026-03-17T10:30:19.804Z" }, + { url = "https://files.pythonhosted.org/packages/da/69/2f47bb6fa1b8d1e3e5d0c4be8ccb4313c63d742476a619418f85740d597b/coverage-7.13.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be3d4bbad9d4b037791794ddeedd7d64a56f5933a2c1373e18e9e568b9141686", size = 254326, upload-time = "2026-03-17T10:30:21.321Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d0/79db81da58965bd29dabc8f4ad2a2af70611a57cba9d1ec006f072f30a54/coverage-7.13.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4d2afbc5cc54d286bfb54541aa50b64cdb07a718227168c87b9e2fb8f25e1743", size = 256267, upload-time = "2026-03-17T10:30:23.094Z" }, + { url = "https://files.pythonhosted.org/packages/e5/32/d0d7cc8168f91ddab44c0ce4806b969df5f5fdfdbb568eaca2dbc2a04936/coverage-7.13.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3ad050321264c49c2fa67bb599100456fc51d004b82534f379d16445da40fb75", size = 250430, upload-time = "2026-03-17T10:30:25.311Z" }, + { url = "https://files.pythonhosted.org/packages/4d/06/a055311d891ddbe231cd69fdd20ea4be6e3603ffebddf8704b8ca8e10a3c/coverage-7.13.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7300c8a6d13335b29bb76d7651c66af6bd8658517c43499f110ddc6717bfc209", size = 252017, upload-time = "2026-03-17T10:30:27.284Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/d0fd2d21e29a657b5f77a2fe7082e1568158340dceb941954f776dce1b7b/coverage-7.13.5-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:eb07647a5738b89baab047f14edd18ded523de60f3b30e75c2acc826f79c839a", size = 250080, upload-time = "2026-03-17T10:30:29.481Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ab/0d7fb2efc2e9a5eb7ddcc6e722f834a69b454b7e6e5888c3a8567ecffb31/coverage-7.13.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:9adb6688e3b53adffefd4a52d72cbd8b02602bfb8f74dcd862337182fd4d1a4e", size = 253843, upload-time = "2026-03-17T10:30:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/ba/6f/7467b917bbf5408610178f62a49c0ed4377bb16c1657f689cc61470da8ce/coverage-7.13.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7c8d4bc913dd70b93488d6c496c77f3aff5ea99a07e36a18f865bca55adef8bd", size = 249802, upload-time = "2026-03-17T10:30:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/75/2c/1172fb689df92135f5bfbbd69fc83017a76d24ea2e2f3a1154007e2fb9f8/coverage-7.13.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0e3c426ffc4cd952f54ee9ffbdd10345709ecc78a3ecfd796a57236bfad0b9b8", size = 250707, upload-time = "2026-03-17T10:30:35.2Z" }, + { url = "https://files.pythonhosted.org/packages/67/21/9ac389377380a07884e3b48ba7a620fcd9dbfaf1d40565facdc6b36ec9ef/coverage-7.13.5-cp311-cp311-win32.whl", hash = "sha256:259b69bb83ad9894c4b25be2528139eecba9a82646ebdda2d9db1ba28424a6bf", size = 221880, upload-time = "2026-03-17T10:30:36.775Z" }, + { url = "https://files.pythonhosted.org/packages/af/7f/4cd8a92531253f9d7c1bbecd9fa1b472907fb54446ca768c59b531248dc5/coverage-7.13.5-cp311-cp311-win_amd64.whl", hash = "sha256:258354455f4e86e3e9d0d17571d522e13b4e1e19bf0f8596bcf9476d61e7d8a9", size = 222816, upload-time = "2026-03-17T10:30:38.891Z" }, + { url = "https://files.pythonhosted.org/packages/12/a6/1d3f6155fb0010ca68eba7fe48ca6c9da7385058b77a95848710ecf189b1/coverage-7.13.5-cp311-cp311-win_arm64.whl", hash = "sha256:bff95879c33ec8da99fc9b6fe345ddb5be6414b41d6d1ad1c8f188d26f36e028", size = 221483, upload-time = "2026-03-17T10:30:40.463Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c3/a396306ba7db865bf96fc1fb3b7fd29bcbf3d829df642e77b13555163cd6/coverage-7.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:460cf0114c5016fa841214ff5564aa4864f11948da9440bc97e21ad1f4ba1e01", size = 219554, upload-time = "2026-03-17T10:30:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/a68a19e5384e93f811dccc51034b1fd0b865841c390e3c931dcc4699e035/coverage-7.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e223ce4b4ed47f065bfb123687686512e37629be25cc63728557ae7db261422", size = 219908, upload-time = "2026-03-17T10:30:43.906Z" }, + { url = "https://files.pythonhosted.org/packages/29/72/20b917c6793af3a5ceb7fb9c50033f3ec7865f2911a1416b34a7cfa0813b/coverage-7.13.5-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6e3370441f4513c6252bf042b9c36d22491142385049243253c7e48398a15a9f", size = 251419, upload-time = "2026-03-17T10:30:45.545Z" }, + { url = "https://files.pythonhosted.org/packages/8c/49/cd14b789536ac6a4778c453c6a2338bc0a2fb60c5a5a41b4008328b9acc1/coverage-7.13.5-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:03ccc709a17a1de074fb1d11f217342fb0d2b1582ed544f554fc9fc3f07e95f5", size = 254159, upload-time = "2026-03-17T10:30:47.204Z" }, + { url = "https://files.pythonhosted.org/packages/9d/00/7b0edcfe64e2ed4c0340dac14a52ad0f4c9bd0b8b5e531af7d55b703db7c/coverage-7.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3f4818d065964db3c1c66dc0fbdac5ac692ecbc875555e13374fdbe7eedb4376", size = 255270, upload-time = "2026-03-17T10:30:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/7ffc4ba0f5d0a55c1e84ea7cee39c9fc06af7b170513d83fbf3bbefce280/coverage-7.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:012d5319e66e9d5a218834642d6c35d265515a62f01157a45bcc036ecf947256", size = 257538, upload-time = "2026-03-17T10:30:50.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/bd/73ddf85f93f7e6fa83e77ccecb6162d9415c79007b4bc124008a4995e4a7/coverage-7.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8dd02af98971bdb956363e4827d34425cb3df19ee550ef92855b0acb9c7ce51c", size = 251821, upload-time = "2026-03-17T10:30:52.5Z" }, + { url = "https://files.pythonhosted.org/packages/a0/81/278aff4e8dec4926a0bcb9486320752811f543a3ce5b602cc7a29978d073/coverage-7.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f08fd75c50a760c7eb068ae823777268daaf16a80b918fa58eea888f8e3919f5", size = 253191, upload-time = "2026-03-17T10:30:54.543Z" }, + { url = "https://files.pythonhosted.org/packages/70/ee/fe1621488e2e0a58d7e94c4800f0d96f79671553488d401a612bebae324b/coverage-7.13.5-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:843ea8643cf967d1ac7e8ecd4bb00c99135adf4816c0c0593fdcc47b597fcf09", size = 251337, upload-time = "2026-03-17T10:30:56.663Z" }, + { url = "https://files.pythonhosted.org/packages/37/a6/f79fb37aa104b562207cc23cb5711ab6793608e246cae1e93f26b2236ed9/coverage-7.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9d44d7aa963820b1b971dbecd90bfe5fe8f81cff79787eb6cca15750bd2f79b9", size = 255404, upload-time = "2026-03-17T10:30:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/75/f0/ed15262a58ec81ce457ceb717b7f78752a1713556b19081b76e90896e8d4/coverage-7.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7132bed4bd7b836200c591410ae7d97bf7ae8be6fc87d160b2bd881df929e7bf", size = 250903, upload-time = "2026-03-17T10:31:00.093Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e9/9129958f20e7e9d4d56d51d42ccf708d15cac355ff4ac6e736e97a9393d2/coverage-7.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a698e363641b98843c517817db75373c83254781426e94ada3197cabbc2c919c", size = 252780, upload-time = "2026-03-17T10:31:01.916Z" }, + { url = "https://files.pythonhosted.org/packages/a4/d7/0ad9b15812d81272db94379fe4c6df8fd17781cc7671fdfa30c76ba5ff7b/coverage-7.13.5-cp312-cp312-win32.whl", hash = "sha256:bdba0a6b8812e8c7df002d908a9a2ea3c36e92611b5708633c50869e6d922fdf", size = 222093, upload-time = "2026-03-17T10:31:03.642Z" }, + { url = "https://files.pythonhosted.org/packages/29/3d/821a9a5799fac2556bcf0bd37a70d1d11fa9e49784b6d22e92e8b2f85f18/coverage-7.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:d2c87e0c473a10bffe991502eac389220533024c8082ec1ce849f4218dded810", size = 222900, upload-time = "2026-03-17T10:31:05.651Z" }, + { url = "https://files.pythonhosted.org/packages/d4/fa/2238c2ad08e35cf4f020ea721f717e09ec3152aea75d191a7faf3ef009a8/coverage-7.13.5-cp312-cp312-win_arm64.whl", hash = "sha256:bf69236a9a81bdca3bff53796237aab096cdbf8d78a66ad61e992d9dac7eb2de", size = 221515, upload-time = "2026-03-17T10:31:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/74/8c/74fedc9663dcf168b0a059d4ea756ecae4da77a489048f94b5f512a8d0b3/coverage-7.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ec4af212df513e399cf11610cc27063f1586419e814755ab362e50a85ea69c1", size = 219576, upload-time = "2026-03-17T10:31:09.045Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c9/44fb661c55062f0818a6ffd2685c67aa30816200d5f2817543717d4b92eb/coverage-7.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:941617e518602e2d64942c88ec8499f7fbd49d3f6c4327d3a71d43a1973032f3", size = 219942, upload-time = "2026-03-17T10:31:10.708Z" }, + { url = "https://files.pythonhosted.org/packages/5f/13/93419671cee82b780bab7ea96b67c8ef448f5f295f36bf5031154ec9a790/coverage-7.13.5-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:da305e9937617ee95c2e39d8ff9f040e0487cbf1ac174f777ed5eddd7a7c1f26", size = 250935, upload-time = "2026-03-17T10:31:12.392Z" }, + { url = "https://files.pythonhosted.org/packages/ac/68/1666e3a4462f8202d836920114fa7a5ee9275d1fa45366d336c551a162dd/coverage-7.13.5-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:78e696e1cc714e57e8b25760b33a8b1026b7048d270140d25dafe1b0a1ee05a3", size = 253541, upload-time = "2026-03-17T10:31:14.247Z" }, + { url = "https://files.pythonhosted.org/packages/4e/5e/3ee3b835647be646dcf3c65a7c6c18f87c27326a858f72ab22c12730773d/coverage-7.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02ca0eed225b2ff301c474aeeeae27d26e2537942aa0f87491d3e147e784a82b", size = 254780, upload-time = "2026-03-17T10:31:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/44/b3/cb5bd1a04cfcc49ede6cd8409d80bee17661167686741e041abc7ee1b9a9/coverage-7.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04690832cbea4e4663d9149e05dba142546ca05cb1848816760e7f58285c970a", size = 256912, upload-time = "2026-03-17T10:31:17.89Z" }, + { url = "https://files.pythonhosted.org/packages/1b/66/c1dceb7b9714473800b075f5c8a84f4588f887a90eb8645282031676e242/coverage-7.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0590e44dd2745c696a778f7bab6aa95256de2cbc8b8cff4f7db8ff09813d6969", size = 251165, upload-time = "2026-03-17T10:31:19.605Z" }, + { url = "https://files.pythonhosted.org/packages/b7/62/5502b73b97aa2e53ea22a39cf8649ff44827bef76d90bf638777daa27a9d/coverage-7.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d7cfad2d6d81dd298ab6b89fe72c3b7b05ec7544bdda3b707ddaecff8d25c161", size = 252908, upload-time = "2026-03-17T10:31:21.312Z" }, + { url = "https://files.pythonhosted.org/packages/7d/37/7792c2d69854397ca77a55c4646e5897c467928b0e27f2d235d83b5d08c6/coverage-7.13.5-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e092b9499de38ae0fbfbc603a74660eb6ff3e869e507b50d85a13b6db9863e15", size = 250873, upload-time = "2026-03-17T10:31:23.565Z" }, + { url = "https://files.pythonhosted.org/packages/a3/23/bc866fb6163be52a8a9e5d708ba0d3b1283c12158cefca0a8bbb6e247a43/coverage-7.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:48c39bc4a04d983a54a705a6389512883d4a3b9862991b3617d547940e9f52b1", size = 255030, upload-time = "2026-03-17T10:31:25.58Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8b/ef67e1c222ef49860701d346b8bbb70881bef283bd5f6cbba68a39a086c7/coverage-7.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2d3807015f138ffea1ed9afeeb8624fd781703f2858b62a8dd8da5a0994c57b6", size = 250694, upload-time = "2026-03-17T10:31:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/46/0d/866d1f74f0acddbb906db212e096dee77a8e2158ca5e6bb44729f9d93298/coverage-7.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2aa19e03161671ec964004fb74b2257805d9710bf14a5c704558b9d8dbaf17", size = 252469, upload-time = "2026-03-17T10:31:29.472Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f5/be742fec31118f02ce42b21c6af187ad6a344fed546b56ca60caacc6a9a0/coverage-7.13.5-cp313-cp313-win32.whl", hash = "sha256:ce1998c0483007608c8382f4ff50164bfc5bd07a2246dd272aa4043b75e61e85", size = 222112, upload-time = "2026-03-17T10:31:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/66/40/7732d648ab9d069a46e686043241f01206348e2bbf128daea85be4d6414b/coverage-7.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:631efb83f01569670a5e866ceb80fe483e7c159fac6f167e6571522636104a0b", size = 222923, upload-time = "2026-03-17T10:31:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/48/af/fea819c12a095781f6ccd504890aaddaf88b8fab263c4940e82c7b770124/coverage-7.13.5-cp313-cp313-win_arm64.whl", hash = "sha256:f4cd16206ad171cbc2470dbea9103cf9a7607d5fe8c242fdf1edf36174020664", size = 221540, upload-time = "2026-03-17T10:31:35.445Z" }, + { url = "https://files.pythonhosted.org/packages/23/d2/17879af479df7fbbd44bd528a31692a48f6b25055d16482fdf5cdb633805/coverage-7.13.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0428cbef5783ad91fe240f673cc1f76b25e74bbfe1a13115e4aa30d3f538162d", size = 220262, upload-time = "2026-03-17T10:31:37.184Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4c/d20e554f988c8f91d6a02c5118f9abbbf73a8768a3048cb4962230d5743f/coverage-7.13.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e0b216a19534b2427cc201a26c25da4a48633f29a487c61258643e89d28200c0", size = 220617, upload-time = "2026-03-17T10:31:39.245Z" }, + { url = "https://files.pythonhosted.org/packages/29/9c/f9f5277b95184f764b24e7231e166dfdb5780a46d408a2ac665969416d61/coverage-7.13.5-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:972a9cd27894afe4bc2b1480107054e062df08e671df7c2f18c205e805ccd806", size = 261912, upload-time = "2026-03-17T10:31:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f6/7f1ab39393eeb50cfe4747ae8ef0e4fc564b989225aa1152e13a180d74f8/coverage-7.13.5-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4b59148601efcd2bac8c4dbf1f0ad6391693ccf7a74b8205781751637076aee3", size = 263987, upload-time = "2026-03-17T10:31:43.724Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d7/62c084fb489ed9c6fbdf57e006752e7c516ea46fd690e5ed8b8617c7d52e/coverage-7.13.5-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:505d7083c8b0c87a8fa8c07370c285847c1f77739b22e299ad75a6af6c32c5c9", size = 266416, upload-time = "2026-03-17T10:31:45.769Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f6/df63d8660e1a0bff6125947afda112a0502736f470d62ca68b288ea762d8/coverage-7.13.5-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:60365289c3741e4db327e7baff2a4aaacf22f788e80fa4683393891b70a89fbd", size = 267558, upload-time = "2026-03-17T10:31:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/5b/02/353ca81d36779bd108f6d384425f7139ac3c58c750dcfaafe5d0bee6436b/coverage-7.13.5-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1b88c69c8ef5d4b6fe7dea66d6636056a0f6a7527c440e890cf9259011f5e606", size = 261163, upload-time = "2026-03-17T10:31:50.125Z" }, + { url = "https://files.pythonhosted.org/packages/2c/16/2e79106d5749bcaf3aee6d309123548e3276517cd7851faa8da213bc61bf/coverage-7.13.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5b13955d31d1633cf9376908089b7cebe7d15ddad7aeaabcbe969a595a97e95e", size = 263981, upload-time = "2026-03-17T10:31:51.961Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/c29e0c59ffa6942030ae6f50b88ae49988e7e8da06de7ecdbf49c6d4feae/coverage-7.13.5-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:f70c9ab2595c56f81a89620e22899eea8b212a4041bd728ac6f4a28bf5d3ddd0", size = 261604, upload-time = "2026-03-17T10:31:53.872Z" }, + { url = "https://files.pythonhosted.org/packages/40/48/097cdc3db342f34006a308ab41c3a7c11c3f0d84750d340f45d88a782e00/coverage-7.13.5-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:084b84a8c63e8d6fc7e3931b316a9bcafca1458d753c539db82d31ed20091a87", size = 265321, upload-time = "2026-03-17T10:31:55.997Z" }, + { url = "https://files.pythonhosted.org/packages/bb/1f/4994af354689e14fd03a75f8ec85a9a68d94e0188bbdab3fc1516b55e512/coverage-7.13.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad14385487393e386e2ea988b09d62dd42c397662ac2dabc3832d71253eee479", size = 260502, upload-time = "2026-03-17T10:31:58.308Z" }, + { url = "https://files.pythonhosted.org/packages/22/c6/9bb9ef55903e628033560885f5c31aa227e46878118b63ab15dc7ba87797/coverage-7.13.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:7f2c47b36fe7709a6e83bfadf4eefb90bd25fbe4014d715224c4316f808e59a2", size = 262688, upload-time = "2026-03-17T10:32:00.141Z" }, + { url = "https://files.pythonhosted.org/packages/14/4f/f5df9007e50b15e53e01edea486814783a7f019893733d9e4d6caad75557/coverage-7.13.5-cp313-cp313t-win32.whl", hash = "sha256:67e9bc5449801fad0e5dff329499fb090ba4c5800b86805c80617b4e29809b2a", size = 222788, upload-time = "2026-03-17T10:32:02.246Z" }, + { url = "https://files.pythonhosted.org/packages/e1/98/aa7fccaa97d0f3192bec013c4e6fd6d294a6ed44b640e6bb61f479e00ed5/coverage-7.13.5-cp313-cp313t-win_amd64.whl", hash = "sha256:da86cdcf10d2519e10cabb8ac2de03da1bcb6e4853790b7fbd48523332e3a819", size = 223851, upload-time = "2026-03-17T10:32:04.416Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8b/e5c469f7352651e5f013198e9e21f97510b23de957dd06a84071683b4b60/coverage-7.13.5-cp313-cp313t-win_arm64.whl", hash = "sha256:0ecf12ecb326fe2c339d93fc131816f3a7367d223db37817208905c89bded911", size = 222104, upload-time = "2026-03-17T10:32:06.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" }, + { url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" }, + { url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" }, + { url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" }, + { url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" }, + { url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" }, + { url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" }, + { url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" }, + { url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" }, + { url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" }, + { url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" }, + { url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" }, + { url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" }, + { url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" }, + { url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" }, + { url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" }, + { url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" }, + { url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" }, + { url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" }, +] + +[package.optional-dependencies] +toml = [ + { name = "tomli", marker = "python_full_version <= '3.11'" }, +] + +[[package]] +name = "cryptography" +version = "46.0.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/47/93/ac8f3d5ff04d54bc814e961a43ae5b0b146154c89c61b47bb07557679b18/cryptography-46.0.7.tar.gz", hash = "sha256:e4cfd68c5f3e0bfdad0d38e023239b96a2fe84146481852dffbcca442c245aa5", size = 750652, upload-time = "2026-04-08T01:57:54.692Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/5d/4a8f770695d73be252331e60e526291e3df0c9b27556a90a6b47bccca4c2/cryptography-46.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:ea42cbe97209df307fdc3b155f1b6fa2577c0defa8f1f7d3be7d31d189108ad4", size = 7179869, upload-time = "2026-04-08T01:56:17.157Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/6d80dc379b0bbc1f9d1e429f42e4cb9e1d319c7a8201beffd967c516ea01/cryptography-46.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b36a4695e29fe69215d75960b22577197aca3f7a25b9cf9d165dcfe9d80bc325", size = 4275492, upload-time = "2026-04-08T01:56:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/1765afe9f572e239c3469f2cb429f3ba7b31878c893b246b4b2994ffe2fe/cryptography-46.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5ad9ef796328c5e3c4ceed237a183f5d41d21150f972455a9d926593a1dcb308", size = 4426670, upload-time = "2026-04-08T01:56:21.415Z" }, + { url = "https://files.pythonhosted.org/packages/8f/3e/af9246aaf23cd4ee060699adab1e47ced3f5f7e7a8ffdd339f817b446462/cryptography-46.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:73510b83623e080a2c35c62c15298096e2a5dc8d51c3b4e1740211839d0dea77", size = 4280275, upload-time = "2026-04-08T01:56:23.539Z" }, + { url = "https://files.pythonhosted.org/packages/0f/54/6bbbfc5efe86f9d71041827b793c24811a017c6ac0fd12883e4caa86b8ed/cryptography-46.0.7-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cbd5fb06b62bd0721e1170273d3f4d5a277044c47ca27ee257025146c34cbdd1", size = 4928402, upload-time = "2026-04-08T01:56:25.624Z" }, + { url = "https://files.pythonhosted.org/packages/2d/cf/054b9d8220f81509939599c8bdbc0c408dbd2bdd41688616a20731371fe0/cryptography-46.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:420b1e4109cc95f0e5700eed79908cef9268265c773d3a66f7af1eef53d409ef", size = 4459985, upload-time = "2026-04-08T01:56:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/f9/46/4e4e9c6040fb01c7467d47217d2f882daddeb8828f7df800cb806d8a2288/cryptography-46.0.7-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:24402210aa54baae71d99441d15bb5a1919c195398a87b563df84468160a65de", size = 3990652, upload-time = "2026-04-08T01:56:29.095Z" }, + { url = "https://files.pythonhosted.org/packages/36/5f/313586c3be5a2fbe87e4c9a254207b860155a8e1f3cca99f9910008e7d08/cryptography-46.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8a469028a86f12eb7d2fe97162d0634026d92a21f3ae0ac87ed1c4a447886c83", size = 4279805, upload-time = "2026-04-08T01:56:30.928Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/60dfc4595f334a2082749673386a4d05e4f0cf4df8248e63b2c3437585f2/cryptography-46.0.7-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9694078c5d44c157ef3162e3bf3946510b857df5a3955458381d1c7cfc143ddb", size = 4892883, upload-time = "2026-04-08T01:56:32.614Z" }, + { url = "https://files.pythonhosted.org/packages/c7/0b/333ddab4270c4f5b972f980adef4faa66951a4aaf646ca067af597f15563/cryptography-46.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:42a1e5f98abb6391717978baf9f90dc28a743b7d9be7f0751a6f56a75d14065b", size = 4459756, upload-time = "2026-04-08T01:56:34.306Z" }, + { url = "https://files.pythonhosted.org/packages/d2/14/633913398b43b75f1234834170947957c6b623d1701ffc7a9600da907e89/cryptography-46.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:91bbcb08347344f810cbe49065914fe048949648f6bd5c2519f34619142bbe85", size = 4410244, upload-time = "2026-04-08T01:56:35.977Z" }, + { url = "https://files.pythonhosted.org/packages/10/f2/19ceb3b3dc14009373432af0c13f46aa08e3ce334ec6eff13492e1812ccd/cryptography-46.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5d1c02a14ceb9148cc7816249f64f623fbfee39e8c03b3650d842ad3f34d637e", size = 4674868, upload-time = "2026-04-08T01:56:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/1a/bb/a5c213c19ee94b15dfccc48f363738633a493812687f5567addbcbba9f6f/cryptography-46.0.7-cp311-abi3-win32.whl", hash = "sha256:d23c8ca48e44ee015cd0a54aeccdf9f09004eba9fc96f38c911011d9ff1bd457", size = 3026504, upload-time = "2026-04-08T01:56:39.666Z" }, + { url = "https://files.pythonhosted.org/packages/2b/02/7788f9fefa1d060ca68717c3901ae7fffa21ee087a90b7f23c7a603c32ae/cryptography-46.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:397655da831414d165029da9bc483bed2fe0e75dde6a1523ec2fe63f3c46046b", size = 3488363, upload-time = "2026-04-08T01:56:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/7b/56/15619b210e689c5403bb0540e4cb7dbf11a6bf42e483b7644e471a2812b3/cryptography-46.0.7-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:d151173275e1728cf7839aaa80c34fe550c04ddb27b34f48c232193df8db5842", size = 7119671, upload-time = "2026-04-08T01:56:44Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/e3ce040721b0b5599e175ba91ab08884c75928fbeb74597dd10ef13505d2/cryptography-46.0.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:db0f493b9181c7820c8134437eb8b0b4792085d37dbb24da050476ccb664e59c", size = 4268551, upload-time = "2026-04-08T01:56:46.071Z" }, + { url = "https://files.pythonhosted.org/packages/03/11/5e395f961d6868269835dee1bafec6a1ac176505a167f68b7d8818431068/cryptography-46.0.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ebd6daf519b9f189f85c479427bbd6e9c9037862cf8fe89ee35503bd209ed902", size = 4408887, upload-time = "2026-04-08T01:56:47.718Z" }, + { url = "https://files.pythonhosted.org/packages/40/53/8ed1cf4c3b9c8e611e7122fb56f1c32d09e1fff0f1d77e78d9ff7c82653e/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b7b412817be92117ec5ed95f880defe9cf18a832e8cafacf0a22337dc1981b4d", size = 4271354, upload-time = "2026-04-08T01:56:49.312Z" }, + { url = "https://files.pythonhosted.org/packages/50/46/cf71e26025c2e767c5609162c866a78e8a2915bbcfa408b7ca495c6140c4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:fbfd0e5f273877695cb93baf14b185f4878128b250cc9f8e617ea0c025dfb022", size = 4905845, upload-time = "2026-04-08T01:56:50.916Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ea/01276740375bac6249d0a971ebdf6b4dc9ead0ee0a34ef3b5a88c1a9b0d4/cryptography-46.0.7-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:ffca7aa1d00cf7d6469b988c581598f2259e46215e0140af408966a24cf086ce", size = 4444641, upload-time = "2026-04-08T01:56:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/3d/4c/7d258f169ae71230f25d9f3d06caabcff8c3baf0978e2b7d65e0acac3827/cryptography-46.0.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:60627cf07e0d9274338521205899337c5d18249db56865f943cbe753aa96f40f", size = 3967749, upload-time = "2026-04-08T01:56:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b5/2a/2ea0767cad19e71b3530e4cad9605d0b5e338b6a1e72c37c9c1ceb86c333/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:80406c3065e2c55d7f49a9550fe0c49b3f12e5bfff5dedb727e319e1afb9bf99", size = 4270942, upload-time = "2026-04-08T01:56:56.416Z" }, + { url = "https://files.pythonhosted.org/packages/41/3d/fe14df95a83319af25717677e956567a105bb6ab25641acaa093db79975d/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:c5b1ccd1239f48b7151a65bc6dd54bcfcc15e028c8ac126d3fada09db0e07ef1", size = 4871079, upload-time = "2026-04-08T01:56:58.31Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/4a479e0f36f8f378d397f4eab4c850b4ffb79a2f0d58704b8fa0703ddc11/cryptography-46.0.7-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d5f7520159cd9c2154eb61eb67548ca05c5774d39e9c2c4339fd793fe7d097b2", size = 4443999, upload-time = "2026-04-08T01:57:00.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/17/b59a741645822ec6d04732b43c5d35e4ef58be7bfa84a81e5ae6f05a1d33/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fcd8eac50d9138c1d7fc53a653ba60a2bee81a505f9f8850b6b2888555a45d0e", size = 4399191, upload-time = "2026-04-08T01:57:02.654Z" }, + { url = "https://files.pythonhosted.org/packages/59/6a/bb2e166d6d0e0955f1e9ff70f10ec4b2824c9cfcdb4da772c7dd69cc7d80/cryptography-46.0.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:65814c60f8cc400c63131584e3e1fad01235edba2614b61fbfbfa954082db0ee", size = 4655782, upload-time = "2026-04-08T01:57:04.592Z" }, + { url = "https://files.pythonhosted.org/packages/95/b6/3da51d48415bcb63b00dc17c2eff3a651b7c4fed484308d0f19b30e8cb2c/cryptography-46.0.7-cp314-cp314t-win32.whl", hash = "sha256:fdd1736fed309b4300346f88f74cd120c27c56852c3838cab416e7a166f67298", size = 3002227, upload-time = "2026-04-08T01:57:06.91Z" }, + { url = "https://files.pythonhosted.org/packages/32/a8/9f0e4ed57ec9cebe506e58db11ae472972ecb0c659e4d52bbaee80ca340a/cryptography-46.0.7-cp314-cp314t-win_amd64.whl", hash = "sha256:e06acf3c99be55aa3b516397fe42f5855597f430add9c17fa46bf2e0fb34c9bb", size = 3475332, upload-time = "2026-04-08T01:57:08.807Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7f/cd42fc3614386bc0c12f0cb3c4ae1fc2bbca5c9662dfed031514911d513d/cryptography-46.0.7-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:462ad5cb1c148a22b2e3bcc5ad52504dff325d17daf5df8d88c17dda1f75f2a4", size = 7165618, upload-time = "2026-04-08T01:57:10.645Z" }, + { url = "https://files.pythonhosted.org/packages/a5/d0/36a49f0262d2319139d2829f773f1b97ef8aef7f97e6e5bd21455e5a8fb5/cryptography-46.0.7-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:84d4cced91f0f159a7ddacad249cc077e63195c36aac40b4150e7a57e84fffe7", size = 4270628, upload-time = "2026-04-08T01:57:12.885Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6c/1a42450f464dda6ffbe578a911f773e54dd48c10f9895a23a7e88b3e7db5/cryptography-46.0.7-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:128c5edfe5e5938b86b03941e94fac9ee793a94452ad1365c9fc3f4f62216832", size = 4415405, upload-time = "2026-04-08T01:57:14.923Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/4ed714dbe93a066dc1f4b4581a464d2d7dbec9046f7c8b7016f5286329e2/cryptography-46.0.7-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:5e51be372b26ef4ba3de3c167cd3d1022934bc838ae9eaad7e644986d2a3d163", size = 4272715, upload-time = "2026-04-08T01:57:16.638Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e6/a26b84096eddd51494bba19111f8fffe976f6a09f132706f8f1bf03f51f7/cryptography-46.0.7-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:cdf1a610ef82abb396451862739e3fc93b071c844399e15b90726ef7470eeaf2", size = 4918400, upload-time = "2026-04-08T01:57:19.021Z" }, + { url = "https://files.pythonhosted.org/packages/c7/08/ffd537b605568a148543ac3c2b239708ae0bd635064bab41359252ef88ed/cryptography-46.0.7-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1d25aee46d0c6f1a501adcddb2d2fee4b979381346a78558ed13e50aa8a59067", size = 4450634, upload-time = "2026-04-08T01:57:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/16/01/0cd51dd86ab5b9befe0d031e276510491976c3a80e9f6e31810cce46c4ad/cryptography-46.0.7-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:cdfbe22376065ffcf8be74dc9a909f032df19bc58a699456a21712d6e5eabfd0", size = 3985233, upload-time = "2026-04-08T01:57:22.862Z" }, + { url = "https://files.pythonhosted.org/packages/92/49/819d6ed3a7d9349c2939f81b500a738cb733ab62fbecdbc1e38e83d45e12/cryptography-46.0.7-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:abad9dac36cbf55de6eb49badd4016806b3165d396f64925bf2999bcb67837ba", size = 4271955, upload-time = "2026-04-08T01:57:24.814Z" }, + { url = "https://files.pythonhosted.org/packages/80/07/ad9b3c56ebb95ed2473d46df0847357e01583f4c52a85754d1a55e29e4d0/cryptography-46.0.7-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:935ce7e3cfdb53e3536119a542b839bb94ec1ad081013e9ab9b7cfd478b05006", size = 4879888, upload-time = "2026-04-08T01:57:26.88Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c7/201d3d58f30c4c2bdbe9b03844c291feb77c20511cc3586daf7edc12a47b/cryptography-46.0.7-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:35719dc79d4730d30f1c2b6474bd6acda36ae2dfae1e3c16f2051f215df33ce0", size = 4449961, upload-time = "2026-04-08T01:57:29.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ef/649750cbf96f3033c3c976e112265c33906f8e462291a33d77f90356548c/cryptography-46.0.7-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7bbc6ccf49d05ac8f7d7b5e2e2c33830d4fe2061def88210a126d130d7f71a85", size = 4401696, upload-time = "2026-04-08T01:57:31.029Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/a8908dcb1a389a459a29008c29966c1d552588d4ae6d43f3a1a4512e0ebe/cryptography-46.0.7-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a1529d614f44b863a7b480c6d000fe93b59acee9c82ffa027cfadc77521a9f5e", size = 4664256, upload-time = "2026-04-08T01:57:33.144Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/f0ab06238e899cc3fb332623f337a7364f36f4bb3f2534c2bb95a35b132c/cryptography-46.0.7-cp38-abi3-win32.whl", hash = "sha256:f247c8c1a1fb45e12586afbb436ef21ff1e80670b2861a90353d9b025583d246", size = 3013001, upload-time = "2026-04-08T01:57:34.933Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f1/00ce3bde3ca542d1acd8f8cfa38e446840945aa6363f9b74746394b14127/cryptography-46.0.7-cp38-abi3-win_amd64.whl", hash = "sha256:506c4ff91eff4f82bdac7633318a526b1d1309fc07ca76a3ad182cb5b686d6d3", size = 3472985, upload-time = "2026-04-08T01:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/63/0c/dca8abb64e7ca4f6b2978769f6fea5ad06686a190cec381f0a796fdcaaba/cryptography-46.0.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:fc9ab8856ae6cf7c9358430e49b368f3108f050031442eaeb6b9d87e4dcf4e4f", size = 3476879, upload-time = "2026-04-08T01:57:38.664Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ea/075aac6a84b7c271578d81a2f9968acb6e273002408729f2ddff517fed4a/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d3b99c535a9de0adced13d159c5a9cf65c325601aa30f4be08afd680643e9c15", size = 4219700, upload-time = "2026-04-08T01:57:40.625Z" }, + { url = "https://files.pythonhosted.org/packages/6c/7b/1c55db7242b5e5612b29fc7a630e91ee7a6e3c8e7bf5406d22e206875fbd/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d02c738dacda7dc2a74d1b2b3177042009d5cab7c7079db74afc19e56ca1b455", size = 4385982, upload-time = "2026-04-08T01:57:42.725Z" }, + { url = "https://files.pythonhosted.org/packages/cb/da/9870eec4b69c63ef5925bf7d8342b7e13bc2ee3d47791461c4e49ca212f4/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:04959522f938493042d595a736e7dbdff6eb6cc2339c11465b3ff89343b65f65", size = 4219115, upload-time = "2026-04-08T01:57:44.939Z" }, + { url = "https://files.pythonhosted.org/packages/f4/72/05aa5832b82dd341969e9a734d1812a6aadb088d9eb6f0430fc337cc5a8f/cryptography-46.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:3986ac1dee6def53797289999eabe84798ad7817f3e97779b5061a95b0ee4968", size = 4385479, upload-time = "2026-04-08T01:57:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/20/2a/1b016902351a523aa2bd446b50a5bc1175d7a7d1cf90fe2ef904f9b84ebc/cryptography-46.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:258514877e15963bd43b558917bc9f54cf7cf866c38aa576ebf47a77ddbc43a4", size = 3412829, upload-time = "2026-04-08T01:57:48.874Z" }, +] + +[[package]] +name = "dask" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "cloudpickle" }, + { name = "fsspec" }, + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "packaging" }, + { name = "partd" }, + { name = "pyyaml" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/2a/5d8cc1579590af86576dde890254440e478c7174b93a02095ecfc2e6ba38/dask-2026.3.0.tar.gz", hash = "sha256:f7d96c8274e8a900d217c1ff6ea8d1bbf0b4c2c21e74a409644498d925eb8f85", size = 11000710, upload-time = "2026-03-18T07:10:14.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/f3/00bb1e867fba351e2d784170955713bee200c43ea306c59f30bd7e748192/dask-2026.3.0-py3-none-any.whl", hash = "sha256:be614b9242b0b38288060fb2d7696125946469c98a1c30e174883fd199e0428d", size = 1485630, upload-time = "2026-03-18T07:10:12.832Z" }, +] + +[[package]] +name = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/cf/b50ddf667c15276a9ab15a70ef5f257564de271957933ffea49d2cdbcdfb/fsspec-2026.3.0.tar.gz", hash = "sha256:1ee6a0e28677557f8c2f994e3eea77db6392b4de9cd1f5d7a9e87a0ae9d01b41", size = 313547, upload-time = "2026-03-27T19:11:14.892Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d5/1f/5f4a3cd9e4440e9d9bc78ad0a91a1c8d46b4d429d5239ebe6793c9fe5c41/fsspec-2026.3.0-py3-none-any.whl", hash = "sha256:d2ceafaad1b3457968ed14efa28798162f1638dbb5d2a6868a2db002a5ee39a4", size = 202595, upload-time = "2026-03-27T19:11:13.595Z" }, +] + +[[package]] +name = "gemspy" +version = "0.0.6" +source = { editable = "." } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "anytree" }, + { name = "attrs" }, + { name = "highspy" }, + { name = "linopy" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] + +[package.optional-dependencies] +doc = [ + { name = "mkdocs" }, + { name = "mkdocs-material" }, + { name = "mkdocs-material-extensions" }, + { name = "mkdocstrings-python" }, +] + +[package.dev-dependencies] +dev = [ + { name = "antlr4-tools" }, + { name = "black" }, + { name = "isort" }, + { name = "mypy" }, + { name = "pandas-stubs", version = "2.3.3.260113", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "pandas-stubs", version = "3.0.0.260204", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pre-commit" }, + { name = "pyarrow" }, + { name = "pytest" }, + { name = "pytest-cov" }, + { name = "types-pyyaml" }, +] + +[package.metadata] +requires-dist = [ + { name = "antlr4-python3-runtime", specifier = ">=4.13.2" }, + { name = "anytree", specifier = ">=2.13" }, + { name = "attrs", specifier = ">=26.0" }, + { name = "highspy", specifier = ">=1.14" }, + { name = "linopy", specifier = ">=0.6" }, + { name = "mkdocs", marker = "extra == 'doc'" }, + { name = "mkdocs-material", marker = "extra == 'doc'" }, + { name = "mkdocs-material-extensions", marker = "extra == 'doc'" }, + { name = "mkdocstrings-python", marker = "extra == 'doc'" }, + { name = "numpy", specifier = ">=1.26" }, + { name = "pandas", specifier = ">=2.2" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "xarray", specifier = ">=2025.3" }, +] +provides-extras = ["doc"] + +[package.metadata.requires-dev] +dev = [ + { name = "antlr4-tools", specifier = ">=0.2.1" }, + { name = "black", specifier = ">=24.0" }, + { name = "isort", specifier = ">=5.12" }, + { name = "mypy", specifier = ">=1.7" }, + { name = "pandas-stubs" }, + { name = "pre-commit", specifier = ">=3.5" }, + { name = "pyarrow" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-cov" }, + { name = "types-pyyaml", specifier = ">=6.0.12" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "google-api-core" +version = "2.30.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1a/2e/83ca41eb400eb228f9279ec14ed66f6475218b59af4c6daec2d5a509fe83/google_api_core-2.30.2.tar.gz", hash = "sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9", size = 176862, upload-time = "2026-04-02T21:23:44.876Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/e1/ebd5100cbb202e561c0c8b59e485ef3bd63fa9beb610f3fdcaea443f0288/google_api_core-2.30.2-py3-none-any.whl", hash = "sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594", size = 173236, upload-time = "2026-04-02T21:23:06.395Z" }, +] + +[[package]] +name = "google-auth" +version = "2.49.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, + { name = "pyasn1-modules" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/24/6ca08b0a03c7b0c620427503ab00353a4ae806b848b93bcea18b6b76fde6/google_cloud_core-2.5.1.tar.gz", hash = "sha256:3dc94bdec9d05a31d9f355045ed0f369fbc0d8c665076c734f065d729800f811", size = 36078, upload-time = "2026-03-30T22:50:08.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/d9/5bb050cb32826466aa9b25f79e2ca2879fe66cb76782d4ed798dd7506151/google_cloud_core-2.5.1-py3-none-any.whl", hash = "sha256:ea62cdf502c20e3e14be8a32c05ed02113d7bef454e40ff3fab6fe1ec9f1f4e7", size = 29452, upload-time = "2026-03-30T22:48:31.567Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/47/205eb8e9a1739b5345843e5a425775cbdc472cc38e7eda082ba5b8d02450/google_cloud_storage-3.10.1.tar.gz", hash = "sha256:97db9aa4460727982040edd2bd13ff3d5e2260b5331ad22895802da1fc2a5286", size = 17309950, upload-time = "2026-03-23T09:35:23.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/ff/ca9ab2417fa913d75aae38bf40bf856bb2749a604b2e0f701b37cfcd23cc/google_cloud_storage-3.10.1-py3-none-any.whl", hash = "sha256:a72f656759b7b99bda700f901adcb3425a828d4a29f911bc26b3ea79c5b1217f", size = 324453, upload-time = "2026-03-23T09:35:21.368Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/03/41/4b9c02f99e4c5fb477122cd5437403b552873f014616ac1d19ac8221a58d/google_crc32c-1.8.0.tar.gz", hash = "sha256:a428e25fb7691024de47fecfbff7ff957214da51eddded0da0ae0e0f03a2cf79", size = 14192, upload-time = "2025-12-16T00:35:25.142Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/ac/6f7bc93886a823ab545948c2dd48143027b2355ad1944c7cf852b338dc91/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:0470b8c3d73b5f4e3300165498e4cf25221c7eb37f1159e221d1825b6df8a7ff", size = 31296, upload-time = "2025-12-16T00:19:07.261Z" }, + { url = "https://files.pythonhosted.org/packages/f7/97/a5accde175dee985311d949cfcb1249dcbb290f5ec83c994ea733311948f/google_crc32c-1.8.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:119fcd90c57c89f30040b47c211acee231b25a45d225e3225294386f5d258288", size = 30870, upload-time = "2025-12-16T00:29:17.669Z" }, + { url = "https://files.pythonhosted.org/packages/3d/63/bec827e70b7a0d4094e7476f863c0dbd6b5f0f1f91d9c9b32b76dcdfeb4e/google_crc32c-1.8.0-cp310-cp310-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6f35aaffc8ccd81ba3162443fabb920e65b1f20ab1952a31b13173a67811467d", size = 33214, upload-time = "2025-12-16T00:40:19.618Z" }, + { url = "https://files.pythonhosted.org/packages/63/bc/11b70614df04c289128d782efc084b9035ef8466b3d0a8757c1b6f5cf7ac/google_crc32c-1.8.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:864abafe7d6e2c4c66395c1eb0fe12dc891879769b52a3d56499612ca93b6092", size = 33589, upload-time = "2025-12-16T00:40:20.7Z" }, + { url = "https://files.pythonhosted.org/packages/3e/00/a08a4bc24f1261cc5b0f47312d8aebfbe4b53c2e6307f1b595605eed246b/google_crc32c-1.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:db3fe8eaf0612fc8b20fa21a5f25bd785bc3cd5be69f8f3412b0ac2ffd49e733", size = 34437, upload-time = "2025-12-16T00:35:19.437Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/21ccfaab3d5078d41efe8612e0ed0bfc9ce22475de074162a91a25f7980d/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:014a7e68d623e9a4222d663931febc3033c5c7c9730785727de2a81f87d5bab8", size = 31298, upload-time = "2025-12-16T00:20:32.241Z" }, + { url = "https://files.pythonhosted.org/packages/c5/b8/f8413d3f4b676136e965e764ceedec904fe38ae8de0cdc52a12d8eb1096e/google_crc32c-1.8.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:86cfc00fe45a0ac7359e5214a1704e51a99e757d0272554874f419f79838c5f7", size = 30872, upload-time = "2025-12-16T00:33:58.785Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fd/33aa4ec62b290477181c55bb1c9302c9698c58c0ce9a6ab4874abc8b0d60/google_crc32c-1.8.0-cp311-cp311-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:19b40d637a54cb71e0829179f6cb41835f0fbd9e8eb60552152a8b52c36cbe15", size = 33243, upload-time = "2025-12-16T00:40:21.46Z" }, + { url = "https://files.pythonhosted.org/packages/71/03/4820b3bd99c9653d1a5210cb32f9ba4da9681619b4d35b6a052432df4773/google_crc32c-1.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:17446feb05abddc187e5441a45971b8394ea4c1b6efd88ab0af393fd9e0a156a", size = 33608, upload-time = "2025-12-16T00:40:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/7c/43/acf61476a11437bf9733fb2f70599b1ced11ec7ed9ea760fdd9a77d0c619/google_crc32c-1.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:71734788a88f551fbd6a97be9668a0020698e07b2bf5b3aa26a36c10cdfb27b2", size = 34439, upload-time = "2025-12-16T00:35:20.458Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5f/7307325b1198b59324c0fa9807cafb551afb65e831699f2ce211ad5c8240/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:4b8286b659c1335172e39563ab0a768b8015e88e08329fa5321f774275fc3113", size = 31300, upload-time = "2025-12-16T00:21:56.723Z" }, + { url = "https://files.pythonhosted.org/packages/21/8e/58c0d5d86e2220e6a37befe7e6a94dd2f6006044b1a33edf1ff6d9f7e319/google_crc32c-1.8.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:2a3dc3318507de089c5384cc74d54318401410f82aa65b2d9cdde9d297aca7cb", size = 30867, upload-time = "2025-12-16T00:38:31.302Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a9/a780cc66f86335a6019f557a8aaca8fbb970728f0efd2430d15ff1beae0e/google_crc32c-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:14f87e04d613dfa218d6135e81b78272c3b904e2a7053b841481b38a7d901411", size = 33364, upload-time = "2025-12-16T00:40:22.96Z" }, + { url = "https://files.pythonhosted.org/packages/21/3f/3457ea803db0198c9aaca2dd373750972ce28a26f00544b6b85088811939/google_crc32c-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb5c869c2923d56cb0c8e6bcdd73c009c36ae39b652dbe46a05eb4ef0ad01454", size = 33740, upload-time = "2025-12-16T00:40:23.96Z" }, + { url = "https://files.pythonhosted.org/packages/df/c0/87c2073e0c72515bb8733d4eef7b21548e8d189f094b5dad20b0ecaf64f6/google_crc32c-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc0c8912038065eafa603b238abf252e204accab2a704c63b9e14837a854962", size = 34437, upload-time = "2025-12-16T00:35:21.395Z" }, + { url = "https://files.pythonhosted.org/packages/d1/db/000f15b41724589b0e7bc24bc7a8967898d8d3bc8caf64c513d91ef1f6c0/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:3ebb04528e83b2634857f43f9bb8ef5b2bbe7f10f140daeb01b58f972d04736b", size = 31297, upload-time = "2025-12-16T00:23:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/d7/0d/8ebed0c39c53a7e838e2a486da8abb0e52de135f1b376ae2f0b160eb4c1a/google_crc32c-1.8.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:450dc98429d3e33ed2926fc99ee81001928d63460f8538f21a5d6060912a8e27", size = 30867, upload-time = "2025-12-16T00:43:14.628Z" }, + { url = "https://files.pythonhosted.org/packages/ce/42/b468aec74a0354b34c8cbf748db20d6e350a68a2b0912e128cabee49806c/google_crc32c-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3b9776774b24ba76831609ffbabce8cdf6fa2bd5e9df37b594221c7e333a81fa", size = 33344, upload-time = "2025-12-16T00:40:24.742Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e8/b33784d6fc77fb5062a8a7854e43e1e618b87d5ddf610a88025e4de6226e/google_crc32c-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:89c17d53d75562edfff86679244830599ee0a48efc216200691de8b02ab6b2b8", size = 33694, upload-time = "2025-12-16T00:40:25.505Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/d3cbd4d988afb3d8e4db94ca953df429ed6db7282ed0e700d25e6c7bfc8d/google_crc32c-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:57a50a9035b75643996fbf224d6661e386c7162d1dfdab9bc4ca790947d1007f", size = 34435, upload-time = "2025-12-16T00:35:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/21/88/8ecf3c2b864a490b9e7010c84fd203ec8cf3b280651106a3a74dd1b0ca72/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:e6584b12cb06796d285d09e33f63309a09368b9d806a551d8036a4207ea43697", size = 31301, upload-time = "2025-12-16T00:24:48.527Z" }, + { url = "https://files.pythonhosted.org/packages/36/c6/f7ff6c11f5ca215d9f43d3629163727a272eabc356e5c9b2853df2bfe965/google_crc32c-1.8.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:f4b51844ef67d6cf2e9425983274da75f18b1597bb2c998e1c0a0e8d46f8f651", size = 30868, upload-time = "2025-12-16T00:48:12.163Z" }, + { url = "https://files.pythonhosted.org/packages/56/15/c25671c7aad70f8179d858c55a6ae8404902abe0cdcf32a29d581792b491/google_crc32c-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b0d1a7afc6e8e4635564ba8aa5c0548e3173e41b6384d7711a9123165f582de2", size = 33381, upload-time = "2025-12-16T00:40:26.268Z" }, + { url = "https://files.pythonhosted.org/packages/42/fa/f50f51260d7b0ef5d4898af122d8a7ec5a84e2984f676f746445f783705f/google_crc32c-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8b3f68782f3cbd1bce027e48768293072813469af6a61a86f6bb4977a4380f21", size = 33734, upload-time = "2025-12-16T00:40:27.028Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/7b059810934a09fb3ccb657e0843813c1fee1183d3bc2c8041800374aa2c/google_crc32c-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:d511b3153e7011a27ab6ee6bb3a5404a55b994dc1a7322c0b87b29606d9790e2", size = 34878, upload-time = "2025-12-16T00:35:23.142Z" }, + { url = "https://files.pythonhosted.org/packages/52/c5/c171e4d8c44fec1422d801a6d2e5d7ddabd733eeda505c79730ee9607f07/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:87fa445064e7db928226b2e6f0d5304ab4cd0339e664a4e9a25029f384d9bb93", size = 28615, upload-time = "2025-12-16T00:40:29.298Z" }, + { url = "https://files.pythonhosted.org/packages/9c/97/7d75fe37a7a6ed171a2cf17117177e7aab7e6e0d115858741b41e9dd4254/google_crc32c-1.8.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f639065ea2042d5c034bf258a9f085eaa7af0cd250667c0635a3118e8f92c69c", size = 28800, upload-time = "2025-12-16T00:40:30.322Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/d1/b1ea14b93b6b78f57fc580125de44e9f593ab88dd2460f1a8a8d18f74754/google_resumable_media-2.8.2.tar.gz", hash = "sha256:f3354a182ebd193ae3f42e3ef95e6c9b10f128320de23ac7637236713b1acd70", size = 2164510, upload-time = "2026-03-30T23:34:25.369Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/f8/50bfaf4658431ff9de45c5c3935af7ab01157a4903c603cd0eee6e78e087/google_resumable_media-2.8.2-py3-none-any.whl", hash = "sha256:82b6d8ccd11765268cdd2a2123f417ec806b8eef3000a9a38dfe3033da5fb220", size = 81511, upload-time = "2026-03-30T23:34:09.671Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/20/18/a746c8344152d368a5aac738d4c857012f2c5d1fd2eac7e17b647a7861bd/googleapis_common_protos-1.74.0.tar.gz", hash = "sha256:57971e4eeeba6aad1163c1f0fc88543f965bb49129b8bb55b2b7b26ecab084f1", size = 151254, upload-time = "2026-04-02T21:23:26.679Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b6/b0/be5d3329badb9230b765de6eea66b73abd5944bdeb5afb3562ddcd80ae84/googleapis_common_protos-1.74.0-py3-none-any.whl", hash = "sha256:702216f78610bb510e3f12ac3cafd281b7ac45cc5d86e90ad87e4d301a3426b5", size = 300743, upload-time = "2026-04-02T21:22:49.108Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9d/82/74f4a3310cdabfbb10da554c3a672847f1ed33c6f61dd472681ce7f1fe67/griffelib-2.0.2.tar.gz", hash = "sha256:3cf20b3bc470e83763ffbf236e0076b1211bac1bc67de13daf494640f2de707e", size = 166461, upload-time = "2026-03-27T11:34:51.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/8c/c9138d881c79aa0ea9ed83cbd58d5ca75624378b38cee225dcf5c42cc91f/griffelib-2.0.2-py3-none-any.whl", hash = "sha256:925c857658fb1ba40c0772c37acbc2ab650bd794d9c1b9726922e36ea4117ea1", size = 142357, upload-time = "2026-03-27T11:34:46.275Z" }, +] + +[[package]] +name = "highspy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/66/e74b1a805f65c52666e3b54cfc1ba783e745c2c8a7abaae9e7ef2d9e7270/highspy-1.14.0.tar.gz", hash = "sha256:b09cb5e3179a25fc615b8b0941130b0f71e19372c119f3dd620d63b54cd3ca4c", size = 1654913, upload-time = "2026-04-06T15:53:31.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/14/bc6ee11233ad23426274007d89c0a623ccf960639a891dfff9431300cb5b/highspy-1.14.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3647f57e0edebbd0a51cf45d3f07f1c62a1f76f9cbb0e72324938ec52fd048da", size = 2310142, upload-time = "2026-04-06T15:51:33.956Z" }, + { url = "https://files.pythonhosted.org/packages/e8/82/3c8a0545c024c8cf6f3c7ad133f41eddd934b301404df5d577b923031349/highspy-1.14.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ea9edf1b15dc93a8c129a0d08bad17597ef709d16026e76c2c2da1e5ec486ded", size = 2115436, upload-time = "2026-04-06T15:51:35.751Z" }, + { url = "https://files.pythonhosted.org/packages/b0/bd/d48267a781e2ee2ab978207dfe2533f3f35cf59f23a3d7b1582c9dae1b33/highspy-1.14.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4f929802b418e0746aa6d7a0c1ab05abae300f483a8593a07c6cea653b062de", size = 2404088, upload-time = "2026-04-06T15:51:37.412Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d0/a13c8d9bc7d45759c0d68837a5939a93dff9d33fad0ad40b3fe8a9767dd6/highspy-1.14.0-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee3dd40da51f1f7f07d629044228ffd2f25d7aa561a3f08b64e2696bdf927355", size = 2625158, upload-time = "2026-04-06T15:51:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/8d/89/d37f21b9e632e9008a0f49a56ff1b10114341aa8465aa6a0d6a85e3e4909/highspy-1.14.0-cp310-cp310-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:39e78e5a08d29382fae67b8dd144dc9947b022ad295a0fc679f93c096603efb0", size = 2788617, upload-time = "2026-04-06T15:51:41.094Z" }, + { url = "https://files.pythonhosted.org/packages/16/e0/43f49fce9bdac40d53510701c092f33a4c8f4606d7e3aebfe455329a8b06/highspy-1.14.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b66d48939177301b9ebee4b6e13ef97b49a0a389cc0a7360dcbb1cdd57cb31f4", size = 3460987, upload-time = "2026-04-06T15:51:42.955Z" }, + { url = "https://files.pythonhosted.org/packages/14/dd/eb62bbf2dfed24d766f23541e79706bde93154a22d91e69e787ff68eb238/highspy-1.14.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2d4d8c405615bae110306531da4c0b04715aca39b34c853c1598edac5c0ca1fa", size = 4043807, upload-time = "2026-04-06T15:51:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/1d/d1/403e78390f2e707318effe15b410e16f6fe25266e4e8c9c35c0c271b6520/highspy-1.14.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d417857853e440b91733418acb55b260f3c96a0d7c454c1c7a0ac5fd236c77e4", size = 3705933, upload-time = "2026-04-06T15:51:47.109Z" }, + { url = "https://files.pythonhosted.org/packages/18/93/d2a2bcaf524921e1dbb96e0eb3cc818eb3c1ad8d110361dc95bfb820a892/highspy-1.14.0-cp310-cp310-win32.whl", hash = "sha256:c0e27e6d2309f2a1a6ba03c54399580c9af89f736911d00a4a58569475da9d2c", size = 1953893, upload-time = "2026-04-06T15:51:48.638Z" }, + { url = "https://files.pythonhosted.org/packages/b9/c9/f2dfa17423afcf2b17ad28452dbff75870614594121a10f8424cd948547e/highspy-1.14.0-cp310-cp310-win_amd64.whl", hash = "sha256:41c2bcca7ab720db5cfb7f89fa949a3336b476aac9c68cf0e501a3ca5beb18fc", size = 2319788, upload-time = "2026-04-06T15:51:50.079Z" }, + { url = "https://files.pythonhosted.org/packages/73/4e/c813156aa513eb3344b333c1424373cebf1f5843868b2ba5c49c64beecde/highspy-1.14.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:69558127aabad8b5718a58009dc3d36618c3aa5aa5e733206c32ce396189a132", size = 2310870, upload-time = "2026-04-06T15:51:51.586Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6c/d4baa83e8745d729764bf960b51828e9c99c90b4f5cd99e65b59fbf2b6f9/highspy-1.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3699fc8072a70664d5bbaf1c9239f8e2e8700c5090f57486f2ce4567f9b2b6aa", size = 2116701, upload-time = "2026-04-06T15:51:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/cd/64/9dbafa1f3f9ec9293c4038d64b4a49a7a577e1ffcc5c48cf861849d2cff0/highspy-1.14.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4dd237b94494b14693edbebd05a0068fa02ae36b2629e6353be42bc2b491c1f0", size = 2404995, upload-time = "2026-04-06T15:51:54.664Z" }, + { url = "https://files.pythonhosted.org/packages/94/d6/d73cfcce4d3863d9839174a42fd976d84bb7781c132bf0bccdc74e83d9a7/highspy-1.14.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:526c54ff6e0384abcc495cda08fb2f98156544031afb9ac8bb02f20968d743ba", size = 2629054, upload-time = "2026-04-06T15:51:56.157Z" }, + { url = "https://files.pythonhosted.org/packages/71/c2/5ec46d5381815b849f25f4327af187d70325aa693bccdad960228c98ebb1/highspy-1.14.0-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:60265c69fb9d199526b8190f3bd42bc241896a335c299570d1dfc9d6e97f8895", size = 2787400, upload-time = "2026-04-06T15:51:57.682Z" }, + { url = "https://files.pythonhosted.org/packages/25/f0/1e89d849701388886d39ffda26f64bdc835d63f26aa4b5d865067d56fae1/highspy-1.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:895b3450ece50c1cdcd348aa5c6efd6ee823598f3a21c23677d57a06e3c6f28a", size = 3465733, upload-time = "2026-04-06T15:51:59.9Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/b3477f7ca17526167e5eff9d194ba8ea3eca0f04e98a248b26cc6612aa8f/highspy-1.14.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b50202f02c7db95163b0ad92160167541dad263b19327164dcf0f827cf9f69b3", size = 4044165, upload-time = "2026-04-06T15:52:01.482Z" }, + { url = "https://files.pythonhosted.org/packages/5d/85/67bead3e385de8f433572809b3cab6d7694c1a606edac2fbf94309b746d4/highspy-1.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e12591c1d495ebb5f6d8dc50e99ed48f338dfb59741929cd68c2c2b40d97a58f", size = 3707245, upload-time = "2026-04-06T15:52:03.05Z" }, + { url = "https://files.pythonhosted.org/packages/08/1c/0f61f66855a39e22f1f8e7d1ab3632b2a27cc1f42ee628fd9c317a5b4616/highspy-1.14.0-cp311-cp311-win32.whl", hash = "sha256:9ba82456280ef72cde8e45ecf6bdb2a244c56d80f7e44bb2e5ef7a9abf21f4d9", size = 1953948, upload-time = "2026-04-06T15:52:04.549Z" }, + { url = "https://files.pythonhosted.org/packages/f8/bd/4eaa775022d55519101a51a2b1ad5c46cfa8c725a400033943a79138001f/highspy-1.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:e726092e35237dccdd8093f8c91be195a5826e48aab349d6e7856e32c0e87b41", size = 2320210, upload-time = "2026-04-06T15:52:06.417Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3d/83ee11de10ff6499efdb6edbb4586b472a4e9f982c0f6c5d3faa670bf1c3/highspy-1.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a8d0339887d65f5ef20c59be529af33115197d47f775ee21fca911a87d30f92", size = 2311831, upload-time = "2026-04-06T15:52:08.042Z" }, + { url = "https://files.pythonhosted.org/packages/b6/74/51cfca0c382886e302c4e6b9a50f9b160d214a88b4bc5937f5f8e2452dd9/highspy-1.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f61406a287b19680ece9a8c0bc3926e31d26ad2b4df8ba49f22666ce762fb7d", size = 2122237, upload-time = "2026-04-06T15:52:09.587Z" }, + { url = "https://files.pythonhosted.org/packages/78/69/a60f9dc033712f564089700441fb08c7f89db32bccab7926ef95db6b2306/highspy-1.14.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74ee022cc8cc0e3a576f9b2309287974ea80db68adf8c9f1c5698dc725ec0497", size = 2409165, upload-time = "2026-04-06T15:52:11.067Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c5/efa6d74704aa0bc5ffce9975553f6d13f4527e6fad8f79e7cacadfedd3d3/highspy-1.14.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb7c564ee426355671edf6ad17b2ece5aaa74f21b326ed5e7a8ba5bdc880f207", size = 2632243, upload-time = "2026-04-06T15:52:12.529Z" }, + { url = "https://files.pythonhosted.org/packages/f2/1f/a701fee9ca318e6d175d719f8916d090dd7c8100c28bc591adac9fc2db35/highspy-1.14.0-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:44d80756dde11941336933d777ae85b913c303fe40c3b7eb55fae0fe62bbea08", size = 2792226, upload-time = "2026-04-06T15:52:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/b1db0018292e46d75d7aa7889f220d0fa0f2243a628e57790de80f1fc22f/highspy-1.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6ba00a35b6c4b96eb2d20d75238c6046ecf82ac85a603706e5f588d28c3b3abf", size = 3465843, upload-time = "2026-04-06T15:52:15.692Z" }, + { url = "https://files.pythonhosted.org/packages/6c/41/64c4b290a5237e14fbc7e4812d110aecf457273bbade8b27ca9b9ea86c5b/highspy-1.14.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e991b5ac8af64d686f2a56fb59391b4bb81a50d647fe1e31a1b0ff34d9d2bb51", size = 4042641, upload-time = "2026-04-06T15:52:17.573Z" }, + { url = "https://files.pythonhosted.org/packages/30/3a/cff37994f2fd313467749bc9939e5baab4aef0210c79547471d6bbda5e81/highspy-1.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c10554bd0a37d4be126f24cbb7f5aca5a4252dd6e572b51b2433ad6d94ce7a9d", size = 3712946, upload-time = "2026-04-06T15:52:19.466Z" }, + { url = "https://files.pythonhosted.org/packages/6d/60/f328af00a9f05838e766aa7808dc733bb74a4fa79adcf5fdd665cfb8d7ed/highspy-1.14.0-cp312-cp312-win32.whl", hash = "sha256:7290540f0352192e43bdc790a59a82cee1f8029bd8d6b9ca20b54b651256bab4", size = 1955124, upload-time = "2026-04-06T15:52:20.965Z" }, + { url = "https://files.pythonhosted.org/packages/69/ea/0b47c49b6df4474c603b6a232278d5d6e6afddcb9da5044e06dfec579222/highspy-1.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0568d0fb514dc82776c3be1041988fe428c9df2be0ce98c1bff6382c0d2a5ba", size = 2323321, upload-time = "2026-04-06T15:52:22.418Z" }, + { url = "https://files.pythonhosted.org/packages/2d/2e/43b5d51852a8b06a079cc324ee877a91d2e87128ecd99905b45840b0fd3e/highspy-1.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:777ce930bc29a984826bc4e65ca7fec0407284a30877bc0d179dacc6bb7ee972", size = 2311865, upload-time = "2026-04-06T15:52:24.112Z" }, + { url = "https://files.pythonhosted.org/packages/1d/49/7e1a308163954faa3e91cbc0f73282a24d99b9cb6e7314c6f4266fee88d5/highspy-1.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:479be627bbdf7646dccf5621059b9416e6480c3ffa16998e6cc4de89c40a3716", size = 2122334, upload-time = "2026-04-06T15:52:25.938Z" }, + { url = "https://files.pythonhosted.org/packages/87/8c/8ae1a6f3f645deeaaf5522ebd47c61f7d856e8883dd5e7f51318e00ca287/highspy-1.14.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:863a9363b624d0ef0a5a5bfcbe437bcd3891676c8b7c4d801462320b1387ef37", size = 2409256, upload-time = "2026-04-06T15:52:27.306Z" }, + { url = "https://files.pythonhosted.org/packages/7c/40/501586f760677501f3b2f19f0b7236515d06c08db29ccaae838a6f20c05d/highspy-1.14.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba290153919da6368e9144189060a496077a738522555e89403504d379e8d63f", size = 2632208, upload-time = "2026-04-06T15:52:29.122Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/a4137e3fd564fe2615093771463d66fa74cc4a2a94e6b68c15a0f8572218/highspy-1.14.0-cp313-cp313-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:bdc317bc2572a591cf9f0f2415d4b0879f9f0a47bdff3cda44f85a943941e64b", size = 2792091, upload-time = "2026-04-06T15:52:30.586Z" }, + { url = "https://files.pythonhosted.org/packages/af/91/aa3d6758212f4bb14c4c424053932a030ead226f8138da396a0bb6216d66/highspy-1.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:02a987e0020bf28757df9efbdfd6abde6d501b64294f8201301958095dae7979", size = 3466025, upload-time = "2026-04-06T15:52:32.209Z" }, + { url = "https://files.pythonhosted.org/packages/64/10/25cd0fe6b0dfbae39bee2b7a6dfb91e29b1c78a338e537eb5fc8475ea03d/highspy-1.14.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:03c9b39a9ca8fe973eb89093ac386a8606ac37beb0e637b1a9255e31bfe87ef3", size = 4043109, upload-time = "2026-04-06T15:52:34.203Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/4dbcba90d2c8b2ddcd3157d62f5531c2bfa75a3e360b8f05ac52996b489b/highspy-1.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a92b50e7508b90c6d65b315b7dd3b37c6d0db331e698052f7054da523e446eb4", size = 3712794, upload-time = "2026-04-06T15:52:36.163Z" }, + { url = "https://files.pythonhosted.org/packages/de/5a/72d9840b26b0b26916bd7624d606f31a0ce83f14709fcfaf52df31e29898/highspy-1.14.0-cp313-cp313-win32.whl", hash = "sha256:e8eb72be8766717ec856cf5fa3fcadd8ddd59bee0f4d71d4f45ae0a2b861795f", size = 1955044, upload-time = "2026-04-06T15:52:37.532Z" }, + { url = "https://files.pythonhosted.org/packages/22/7e/89f07a03ff9f5460043ba4815583fd3fda22aa6d088be3fb730346ccae63/highspy-1.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:3d15faaab62b408320373540bfd5a7b9a48e9a01072b847f2270b8d5d881647c", size = 2323514, upload-time = "2026-04-06T15:52:38.939Z" }, + { url = "https://files.pythonhosted.org/packages/27/d4/2658ccfef1c31e25a29e337c9ede3107394fad0a9535820a096cd50ad055/highspy-1.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfde9ad829f3463627dab84152ceb4c30c08b89b19226bf8f69d47a7fed5d", size = 2317451, upload-time = "2026-04-06T15:52:40.659Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f8/7d9be61c80a6daa782bf51b4324bf8425896bf120809ca804ad08f69d12e/highspy-1.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fc8ad7a6fc0a44c99b9aa3a3d0c3a917b50dfa8c61e332cb2ee1f7ea4c234e4f", size = 2121732, upload-time = "2026-04-06T15:52:42.189Z" }, + { url = "https://files.pythonhosted.org/packages/01/eb/47f960ccb56986c2c9ef4ce9f293bae95de7c22a662f6c45b844c316d7ff/highspy-1.14.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39339a7a000998ab26eb87add814222fcdc304d38cfba2f6e098189b53ef0a79", size = 2410647, upload-time = "2026-04-06T15:52:43.674Z" }, + { url = "https://files.pythonhosted.org/packages/2d/38/3b37047686105955e2d54ec753c3b9e9d135bdd8e5ee1df310a87be25472/highspy-1.14.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c8f31f2635517c7d370be612a1e9102481483c749b9db786760dd489fad22519", size = 2632928, upload-time = "2026-04-06T15:52:45.373Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/2a6673db80a5388f364d0f7218af08aebbfc05c866d1b4cc2ec79bc4d822/highspy-1.14.0-cp314-cp314-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:68b944014ce307e24921c3bf904253d8849799ded819bda39a4b09359074ea0a", size = 2791407, upload-time = "2026-04-06T15:52:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/75f862b96420dd77a5f88d6401212534833b99aa0ef971d4eddfd227f913/highspy-1.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:03538f68dff2038582d8aa7f5d05690ce459435f498ea0022869fe962d70d8ae", size = 3471297, upload-time = "2026-04-06T15:52:48.892Z" }, + { url = "https://files.pythonhosted.org/packages/ed/17/564c24dcc05d6f11876485654956dfb6b2f4ba17f21ecafe84b059a17ab4/highspy-1.14.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a54d687522347348639a62df270f45546a3cbd84a6cd230dc41732e9559c766f", size = 4045124, upload-time = "2026-04-06T15:52:50.383Z" }, + { url = "https://files.pythonhosted.org/packages/b1/34/a611fe3271be165fb13a7b85970820579515d652bd59f69aed001ed2ff98/highspy-1.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:88db4d1ebefb119991ff16adb624c682ebc20fc586c86156843cb4be7c965d8a", size = 3713436, upload-time = "2026-04-06T15:52:51.849Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0d/001726678facdd7ca435d430bf039732cc50d77fdbd6231fd3bac428893b/highspy-1.14.0-cp314-cp314-win32.whl", hash = "sha256:7a85730676ffc88eadca1721252bec168f6ffc0423f6141f6ad41f79bb441327", size = 1999958, upload-time = "2026-04-06T15:52:54.154Z" }, + { url = "https://files.pythonhosted.org/packages/a4/4d/c7f2b5c23c4b7103095a9959add5119c5653c17c6bc7817fd003bd3ba8c6/highspy-1.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:90b7074d4bc34a4390636aaf9e4232ae15d4536098f1f1f39f04329c08751148", size = 2410786, upload-time = "2026-04-06T15:52:56.161Z" }, +] + +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + +[[package]] +name = "idna" +version = "3.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "9.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/01/15bb152d77b21318514a96f43af312635eb2500c96b55398d020c93d86ea/importlib_metadata-9.0.0.tar.gz", hash = "sha256:a4f57ab599e6a2e3016d7595cfd72eb4661a5106e787a95bcc90c7105b831efc", size = 56405, upload-time = "2026-03-20T06:42:56.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/3d/2d244233ac4f76e38533cfcb2991c9eb4c7bf688ae0a036d30725b8faafe/importlib_metadata-9.0.0-py3-none-any.whl", hash = "sha256:2d21d1cc5a017bd0559e36150c21c830ab1dc304dedd1b7ea85d20f45ef3edd7", size = 27789, upload-time = "2026-03-20T06:42:55.665Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "install-jdk" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/67/502a753533e9b4deb691f3f7ba6303682494f2d8ee651d6253cd78045b66/install_jdk-1.1.0.tar.gz", hash = "sha256:2bfd53caf660e4916df0215a5715519dcb9547fa2a5f07421fd97a8046851eaa", size = 15181, upload-time = "2023-07-21T01:18:36.591Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/5e/af84054b0ff9f9fbe49a7079d46ba8b4ee7ab6192a0310d4bd2c91254626/install_jdk-1.1.0-py3-none-any.whl", hash = "sha256:b63f0fcd63f7abab3443d4120ba92716397753b8a8ea3c85762a629925a9936e", size = 15648, upload-time = "2023-07-21T01:18:35.501Z" }, +] + +[[package]] +name = "isort" +version = "8.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/4a/c64265d71b84030174ff3ac2cd16d8b664072afab8c41fccd8e2ee5a6f8d/librt-0.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f8e12706dcb8ff6b3ed57514a19e45c49ad00bcd423e87b2b2e4b5f64578443", size = 67529, upload-time = "2026-04-09T16:04:27.373Z" }, + { url = "https://files.pythonhosted.org/packages/23/b1/30ca0b3a8bdac209a00145c66cf42e5e7da2cc056ffc6ebc5c7b430ddd34/librt-0.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e3dda8345307fd7306db0ed0cb109a63a2c85ba780eb9dc2d09b2049a931f9c", size = 70248, upload-time = "2026-04-09T16:04:28.758Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fc/c6018dc181478d6ac5aa24a5846b8185101eb90894346db239eb3ea53209/librt-0.9.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:de7dac64e3eb832ffc7b840eb8f52f76420cde1b845be51b2a0f6b870890645e", size = 202184, upload-time = "2026-04-09T16:04:29.893Z" }, + { url = "https://files.pythonhosted.org/packages/bf/58/d69629f002203370ef41ea69ff71c49a2c618aec39b226ff49986ecd8623/librt-0.9.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22a904cbdb678f7cb348c90d543d3c52f581663d687992fee47fd566dcbf5285", size = 212926, upload-time = "2026-04-09T16:04:31.126Z" }, + { url = "https://files.pythonhosted.org/packages/cc/55/01d859f57824e42bd02465c77bec31fa5ef9d8c2bcee702ccf8ef1b9f508/librt-0.9.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:224b9727eb8bc188bc3bcf29d969dba0cd61b01d9bac80c41575520cc4baabb2", size = 225664, upload-time = "2026-04-09T16:04:32.352Z" }, + { url = "https://files.pythonhosted.org/packages/9b/02/32f63ad0ef085a94a70315291efe1151a48b9947af12261882f8445b2a30/librt-0.9.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e94cbc6ad9a6aeea46d775cbb11f361022f778a9cc8cc90af653d3a594b057ce", size = 219534, upload-time = "2026-04-09T16:04:33.667Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5a/9d77111a183c885acf3b3b6e4c00f5b5b07b5817028226499a55f1fedc59/librt-0.9.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7bc30ad339f4e1a01d4917d645e522a0bc0030644d8973f6346397c93ba1503f", size = 227322, upload-time = "2026-04-09T16:04:34.945Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/05d700c93063753e12ab230b972002a3f8f3b9c95d8a980c2f646c8b6963/librt-0.9.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:56d65b583cf43b8cf4c8fbe1e1da20fa3076cc32a1149a141507af1062718236", size = 223407, upload-time = "2026-04-09T16:04:36.22Z" }, + { url = "https://files.pythonhosted.org/packages/c0/26/26c3124823c67c987456977c683da9a27cc874befc194ddcead5f9988425/librt-0.9.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0a1be03168b2691ba61927e299b352a6315189199ca18a57b733f86cb3cc8d38", size = 221302, upload-time = "2026-04-09T16:04:37.62Z" }, + { url = "https://files.pythonhosted.org/packages/50/2b/c7cc2be5cf4ff7b017d948a789256288cb33a517687ff1995e72a7eea79f/librt-0.9.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:63c12efcd160e1d14da11af0c46c0217473e1e0d2ae1acbccc83f561ea4c2a7b", size = 243893, upload-time = "2026-04-09T16:04:38.909Z" }, + { url = "https://files.pythonhosted.org/packages/62/d3/da553d37417a337d12660450535d5fd51373caffbedf6962173c87867246/librt-0.9.0-cp310-cp310-win32.whl", hash = "sha256:e9002e98dcb1c0a66723592520decd86238ddcef168b37ff6cfb559200b4b774", size = 55375, upload-time = "2026-04-09T16:04:40.148Z" }, + { url = "https://files.pythonhosted.org/packages/9b/5a/46fa357bab8311b6442a83471591f2f9e5b15ecc1d2121a43725e0c529b8/librt-0.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:9fcb461fbf70654a52a7cc670e606f04449e2374c199b1825f754e16dacfedd8", size = 62581, upload-time = "2026-04-09T16:04:41.452Z" }, + { url = "https://files.pythonhosted.org/packages/e2/1e/2ec7afcebcf3efea593d13aee18bbcfdd3a243043d848ebf385055e9f636/librt-0.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:90904fac73c478f4b83f4ed96c99c8208b75e6f9a8a1910548f69a00f1eaa671", size = 67155, upload-time = "2026-04-09T16:04:42.933Z" }, + { url = "https://files.pythonhosted.org/packages/18/77/72b85afd4435268338ad4ec6231b3da8c77363f212a0227c1ff3b45e4d35/librt-0.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:789fff71757facc0738e8d89e3b84e4f0251c1c975e85e81b152cdaca927cc2d", size = 69916, upload-time = "2026-04-09T16:04:44.042Z" }, + { url = "https://files.pythonhosted.org/packages/27/fb/948ea0204fbe2e78add6d46b48330e58d39897e425560674aee302dca81c/librt-0.9.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1bf465d1e5b0a27713862441f6467b5ab76385f4ecf8f1f3a44f8aa3c695b4b6", size = 199635, upload-time = "2026-04-09T16:04:45.5Z" }, + { url = "https://files.pythonhosted.org/packages/ac/cd/894a29e251b296a27957856804cfd21e93c194aa131de8bb8032021be07e/librt-0.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f819e0c6413e259a17a7c0d49f97f405abadd3c2a316a3b46c6440b7dbbedbb1", size = 211051, upload-time = "2026-04-09T16:04:47.016Z" }, + { url = "https://files.pythonhosted.org/packages/18/8f/dcaed0bc084a35f3721ff2d081158db569d2c57ea07d35623ddaca5cfc8e/librt-0.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0785c2fb4a81e1aece366aa3e2e039f4a4d7d21aaaded5227d7f3c703427882", size = 224031, upload-time = "2026-04-09T16:04:48.207Z" }, + { url = "https://files.pythonhosted.org/packages/03/44/88f6c1ed1132cd418601cc041fbd92fed28b3a09f39de81978e0822d13ff/librt-0.9.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:80b25c7b570a86c03b5da69e665809deb39265476e8e21d96a9328f9762f9990", size = 218069, upload-time = "2026-04-09T16:04:50.025Z" }, + { url = "https://files.pythonhosted.org/packages/a3/90/7d02e981c2db12188d82b4410ff3e35bfdb844b26aecd02233626f46af2b/librt-0.9.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d4d16b608a1c43d7e33142099a75cd93af482dadce0bf82421e91cad077157f4", size = 224857, upload-time = "2026-04-09T16:04:51.684Z" }, + { url = "https://files.pythonhosted.org/packages/ef/c3/c77e706b7215ca32e928d47535cf13dbc3d25f096f84ddf8fbc06693e229/librt-0.9.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:194fc1a32e1e21fe809d38b5faea66cc65eaa00217c8901fbdb99866938adbdb", size = 219865, upload-time = "2026-04-09T16:04:52.949Z" }, + { url = "https://files.pythonhosted.org/packages/52/d1/32b0c1a0eb8461c70c11656c46a29f760b7c7edf3c36d6f102470c17170f/librt-0.9.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:8c6bc1384d9738781cfd41d09ad7f6e8af13cfea2c75ece6bd6d2566cdea2076", size = 218451, upload-time = "2026-04-09T16:04:54.174Z" }, + { url = "https://files.pythonhosted.org/packages/74/d1/adfd0f9c44761b1d49b1bec66173389834c33ee2bd3c7fd2e2367f1942d4/librt-0.9.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15cb151e52a044f06e54ac7f7b47adbfc89b5c8e2b63e1175a9d587c43e8942a", size = 241300, upload-time = "2026-04-09T16:04:55.452Z" }, + { url = "https://files.pythonhosted.org/packages/09/b0/9074b64407712f0003c27f5b1d7655d1438979155f049720e8a1abd9b1a1/librt-0.9.0-cp311-cp311-win32.whl", hash = "sha256:f100bfe2acf8a3689af9d0cc660d89f17286c9c795f9f18f7b62dd1a6b247ae6", size = 55668, upload-time = "2026-04-09T16:04:56.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/19/40b77b77ce80b9389fb03971431b09b6b913911c38d412059e0b3e2a9ef2/librt-0.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:0b73e4266307e51c95e09c0750b7ec383c561d2e97d58e473f6f6a209952fbb8", size = 62976, upload-time = "2026-04-09T16:04:57.733Z" }, + { url = "https://files.pythonhosted.org/packages/70/9d/9fa7a64041e29035cb8c575af5f0e3840be1b97b4c4d9061e0713f171849/librt-0.9.0-cp311-cp311-win_arm64.whl", hash = "sha256:bc5518873822d2faa8ebdd2c1a4d7c8ef47b01a058495ab7924cb65bdbf5fc9a", size = 53502, upload-time = "2026-04-09T16:04:58.806Z" }, + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, +] + +[[package]] +name = "linopy" +version = "0.6.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "bottleneck" }, + { name = "dask" }, + { name = "deprecation" }, + { name = "google-cloud-storage" }, + { name = "numexpr" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging" }, + { name = "polars" }, + { name = "requests" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "toolz" }, + { name = "tqdm" }, + { name = "xarray", version = "2025.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "xarray", version = "2026.2.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/76/34b4fa488fc6ad3dbb2004e15b016adba4472dc281afbc6a9f1ba7f8b3f0/linopy-0.6.6.tar.gz", hash = "sha256:600a87898ceb8f11cb1fac830a626872657ff897c938c183b7791e047248567b", size = 1268670, upload-time = "2026-03-26T10:01:55.714Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/4d/a1cf38c61467b8a1ca5ea4e3d9ba46d1fcb98a7e7ad64898d9b8dcec1a93/linopy-0.6.6-py3-none-any.whl", hash = "sha256:2f3b776e52a15ec4c092e80aec6691dc2fdd830aaafb8b6d9e303a55e2423d87", size = 116791, upload-time = "2026-03-26T10:01:53.641Z" }, +] + +[[package]] +name = "locket" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/83/97b29fe05cb6ae28d2dbd30b81e2e402a3eed5f460c26e9eaa5895ceacf5/locket-1.0.0.tar.gz", hash = "sha256:5c0d4c052a8bbbf750e056a8e65ccd309086f4f0f18a2eac306a8dfa4112a632", size = 4350, upload-time = "2022-04-20T22:04:44.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/bc/83e112abc66cd466c6b83f99118035867cecd41802f8d044638aa78a106e/locket-1.0.0-py2.py3-none-any.whl", hash = "sha256:b6c819a722f7b6bd955b80781788e4a66a55628b858d347536b7e81325a3a5e3", size = 4398, upload-time = "2022-04-20T22:04:42.23Z" }, +] + +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/4b/3541d44f3937ba468b75da9eebcae497dcf67adb65caa16760b0a6807ebb/markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559", size = 11631, upload-time = "2025-09-27T18:36:05.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/1b/fbd8eed11021cabd9226c37342fa6ca4e8a98d8188a8d9b66740494960e4/markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419", size = 12057, upload-time = "2025-09-27T18:36:07.165Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/e560d658dc0bb8ab762670ece35281dec7b6c1b33f5fbc09ebb57a185519/markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695", size = 22050, upload-time = "2025-09-27T18:36:08.005Z" }, + { url = "https://files.pythonhosted.org/packages/af/cd/ce6e848bbf2c32314c9b237839119c5a564a59725b53157c856e90937b7a/markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591", size = 20681, upload-time = "2025-09-27T18:36:08.881Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2a/b5c12c809f1c3045c4d580b035a743d12fcde53cf685dbc44660826308da/markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c", size = 20705, upload-time = "2025-09-27T18:36:10.131Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e3/9427a68c82728d0a88c50f890d0fc072a1484de2f3ac1ad0bfc1a7214fd5/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f", size = 21524, upload-time = "2025-09-27T18:36:11.324Z" }, + { url = "https://files.pythonhosted.org/packages/bc/36/23578f29e9e582a4d0278e009b38081dbe363c5e7165113fad546918a232/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6", size = 20282, upload-time = "2025-09-27T18:36:12.573Z" }, + { url = "https://files.pythonhosted.org/packages/56/21/dca11354e756ebd03e036bd8ad58d6d7168c80ce1fe5e75218e4945cbab7/markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1", size = 20745, upload-time = "2025-09-27T18:36:13.504Z" }, + { url = "https://files.pythonhosted.org/packages/87/99/faba9369a7ad6e4d10b6a5fbf71fa2a188fe4a593b15f0963b73859a1bbd/markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa", size = 14571, upload-time = "2025-09-27T18:36:14.779Z" }, + { url = "https://files.pythonhosted.org/packages/d6/25/55dc3ab959917602c96985cb1253efaa4ff42f71194bddeb61eb7278b8be/markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8", size = 15056, upload-time = "2025-09-27T18:36:16.125Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9e/0a02226640c255d1da0b8d12e24ac2aa6734da68bff14c05dd53b94a0fc3/markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1", size = 13932, upload-time = "2025-09-27T18:36:17.311Z" }, + { url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" }, + { url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" }, + { url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" }, + { url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" }, + { url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" }, + { url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" }, + { url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" }, + { url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" }, + { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" }, + { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" }, + { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" }, + { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" }, + { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" }, + { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/c0/f641843de3f612a6b48253f39244165acff36657a91cc903633d456ae1ac/mkdocs_autorefs-1.4.4.tar.gz", hash = "sha256:d54a284f27a7346b9c38f1f852177940c222da508e66edc816a0fa55fc6da197", size = 56588, upload-time = "2026-02-10T15:23:55.105Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/de/a3e710469772c6a89595fc52816da05c1e164b4c866a89e3cb82fb1b67c5/mkdocs_autorefs-1.4.4-py3-none-any.whl", hash = "sha256:834ef5408d827071ad1bc69e0f39704fa34c7fc05bc8e1c72b227dfdc5c76089", size = 25530, upload-time = "2026-02-10T15:23:53.817Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/5d/f888d4d3eb31359b327bc9b17a212d6ef03fe0b0682fbb3fc2cb849fb12b/mkdocstrings-1.0.4.tar.gz", hash = "sha256:3969a6515b77db65fd097b53c1b7aa4ae840bd71a2ee62a6a3e89503446d7172", size = 100088, upload-time = "2026-04-15T09:16:53.376Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/94/be70f8ee9c45f2f62b39a1f0e9303bc20e138a8f3b8e50ffd89498e177e1/mkdocstrings-1.0.4-py3-none-any.whl", hash = "sha256:63464b4b29053514f32a1dbbf604e52876d5e638111b0c295ab7ed3cac73ca9b", size = 35560, upload-time = "2026-04-15T09:16:51.436Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/97/ce2502df2cecf2ef997b6c6527c4a223b92feb9e7b790cdc8dcd683f3a8a/mypy-1.20.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:cf5a4db6dca263010e2c7bff081c89383c72d187ba2cf4c44759aac970e2f0c4", size = 14457059, upload-time = "2026-04-21T17:06:14.935Z" }, + { url = "https://files.pythonhosted.org/packages/c9/34/417ee60b822cc80c0f3dc9f495ad7fd8dbb8d8b2cf4baf22d4046d25d01d/mypy-1.20.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7b0e817b518bff7facd7f85ea05b643ad8bdcce684cf29784987b0a7c8e1f997", size = 13346816, upload-time = "2026-04-21T17:10:41.433Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/e20951978702df58379d0bcc2e8f7ccdca4e78cd7dc66dd3ddbf9b29d517/mypy-1.20.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97d7b9a485b40f8ca425460e89bf1da2814625b2da627c0dcc6aa46c92631d14", size = 13772593, upload-time = "2026-04-21T17:08:11.24Z" }, + { url = "https://files.pythonhosted.org/packages/63/a5/5441a13259ec516c56fd5de0fd96a69a9590ae6c5e5d3e5174aa84b97973/mypy-1.20.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e1c12f6d2db3d78b909b5f77513c11eb7f2dd2782b96a3ab6dffc7d44575c99", size = 14656635, upload-time = "2026-04-21T17:09:54.042Z" }, + { url = "https://files.pythonhosted.org/packages/3b/51/b89c69157c5e1f19fd125a65d991166a26906e7902f026f00feebbcfa2b9/mypy-1.20.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89dce27e142d25ffbc154c1819383b69f2e9234dc4ed4766f42e0e8cb264ab5c", size = 14943278, upload-time = "2026-04-21T17:09:15.599Z" }, + { url = "https://files.pythonhosted.org/packages/e9/44/6b0eeecfe96d7cce1d71c66b8e03cb304aa70ec11f1955dc1d6b46aca3c3/mypy-1.20.2-cp310-cp310-win_amd64.whl", hash = "sha256:f376e37f9bf2a946872fc5fd1199c99310748e3c26c7a26683f13f8bdb756cbd", size = 10851915, upload-time = "2026-04-21T17:06:03.5Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/6593dc88545d75fb96416184be5392da5e2a8e8c2802a8597913e16ae25c/mypy-1.20.2-cp310-cp310-win_arm64.whl", hash = "sha256:6e2b469efd811707bc530fd1effef0f5d6eebcb7fe376affae69025da4b979a2", size = 9786676, upload-time = "2026-04-21T17:07:02.035Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4d/9ebeae211caccbdaddde7ed5e31dfcf57faac66be9b11deb1dc6526c8078/mypy-1.20.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4077797a273e56e8843d001e9dfe4ba10e33323d6ade647ff260e5cd97d9758c", size = 14371307, upload-time = "2026-04-21T17:08:56.442Z" }, + { url = "https://files.pythonhosted.org/packages/95/d7/93473d34b61f04fac1aecc01368485c89c5c4af7a4b9a0cab5d77d04b63f/mypy-1.20.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cdecf62abcc4292500d7858aeae87a1f8f1150f4c4dd08fb0b336ee79b2a6df3", size = 13258917, upload-time = "2026-04-21T17:05:50.978Z" }, + { url = "https://files.pythonhosted.org/packages/e2/30/3dd903e8bafb7b5f7bf87fcd58f8382086dea2aa19f0a7b357f21f63071b/mypy-1.20.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c566c3a88b6ece59b3d70f65bedef17304f48eb52ff040a6a18214e1917b3254", size = 13700516, upload-time = "2026-04-21T17:11:33.161Z" }, + { url = "https://files.pythonhosted.org/packages/07/05/c61a140aba4c729ac7bc99ae26fc627c78a6e08f5b9dd319244ea71a3d7e/mypy-1.20.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0deb80d062b2479f2c87ae568f89845afc71d11bc41b04179e58165fd9f31e98", size = 14562889, upload-time = "2026-04-21T17:05:27.674Z" }, + { url = "https://files.pythonhosted.org/packages/fd/87/da78243742ffa8a36d98c3010f0d829f93d5da4e6786f1a1a6f2ad616502/mypy-1.20.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bba9ad231e92a3e424b3e56b65aa17704993425bba97e302c832f9466bb85bac", size = 14803844, upload-time = "2026-04-21T17:10:06.2Z" }, + { url = "https://files.pythonhosted.org/packages/37/52/10a1ddf91b40f843943a3c6db51e2df59c9e237f29d355e95eaab427461f/mypy-1.20.2-cp311-cp311-win_amd64.whl", hash = "sha256:baf593f2765fa3a6b1ef95807dbaa3d25b594f6a52adcc506a6b9cb115e1be67", size = 10846300, upload-time = "2026-04-21T17:12:23.886Z" }, + { url = "https://files.pythonhosted.org/packages/20/02/f9a4415b664c53bd34d6709be59da303abcae986dc4ac847b402edb6fa1e/mypy-1.20.2-cp311-cp311-win_arm64.whl", hash = "sha256:20175a1c0f49863946ec20b7f63255768058ac4f07d2b9ded6a6b46cfb5a9100", size = 9779498, upload-time = "2026-04-21T17:09:23.695Z" }, + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + +[[package]] +name = "numexpr" +version = "2.14.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cb/2f/fdba158c9dbe5caca9c3eca3eaffffb251f2fb8674bf8e2d0aed5f38d319/numexpr-2.14.1.tar.gz", hash = "sha256:4be00b1086c7b7a5c32e31558122b7b80243fe098579b170967da83f3152b48b", size = 119400, upload-time = "2025-10-13T16:17:27.351Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/91/ccd504cbe5b88d06987c77f42ba37a13ef05065fdab4afe6dcfeb2961faf/numexpr-2.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d0fab3fd06a04f6b86102552b26aa5d85e20ac7d8296c15764c726eeabae6cc8", size = 163200, upload-time = "2025-10-13T16:16:25.47Z" }, + { url = "https://files.pythonhosted.org/packages/f3/89/6b07977baf2af75fb6692f9e7a1fb612a15f600fc921f3f565366de01f4a/numexpr-2.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:64ae5dfd62d74a3ef82fe0b37f80527247f3626171ad82025900f46ffca4b39a", size = 152085, upload-time = "2025-10-13T16:16:29.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/c2/c5775541256c4bf16b4d88fa1cffa74a0126703e513093c8774d911b0bb7/numexpr-2.14.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:955c92b064f9074d2970cf3138f5e3b965be673b82024962ed526f39bc25a920", size = 449435, upload-time = "2025-10-13T16:13:16.257Z" }, + { url = "https://files.pythonhosted.org/packages/34/d4/d1a410901c620f7a6a3c5c2b1fc9dab22170be05a89d2c02ae699e27bd3f/numexpr-2.14.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:75440c54fc01e130396650fdf307aa9d41a67dc06ddbfb288971b591c13a395b", size = 440197, upload-time = "2025-10-13T16:14:44.109Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c8/fa85f0cc5c39db587ba4927b862a92477c017ee8476e415e8120a100457b/numexpr-2.14.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:dde9fa47ed319e1e1728940a539df3cb78326b7754bc7c6ab3152afc91808f9b", size = 1414125, upload-time = "2025-10-13T16:13:19.882Z" }, + { url = "https://files.pythonhosted.org/packages/08/72/a58ddc05e0eabb3fa8d3fcd319f3d97870e6b41520832acfd04a6734c2c0/numexpr-2.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76db0bc6267e591ab9c4df405ffb533598e4c88239db7338d11ae9e4b368a85a", size = 1463041, upload-time = "2025-10-13T16:14:47.502Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c5/bdd1862302bb71a78dba941eaf7060e1274f1cf6af2d1b0f1880bfcb289b/numexpr-2.14.1-cp310-cp310-win32.whl", hash = "sha256:0d1dcbdc4d0374c0d523cee2f94f06b001623cbc1fd163612841017a3495427c", size = 166833, upload-time = "2025-10-13T16:17:03.543Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/26773a246716922794388786529e5640676399efabb0ee217ce034df9d27/numexpr-2.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:823cd82c8e7937981339f634e7a9c6a92cb2d0b9d0a5cf627a5e394fffc05377", size = 160068, upload-time = "2025-10-13T16:17:05.191Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/67999bdd1ed1f938d38f3fedd4969632f2f197b090e50505f7cc1fa82510/numexpr-2.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2d03fcb4644a12f70a14d74006f72662824da5b6128bf1bcd10cc3ed80e64c34", size = 163195, upload-time = "2025-10-13T16:16:31.212Z" }, + { url = "https://files.pythonhosted.org/packages/25/95/d64f680ea1fc56d165457287e0851d6708800f9fcea346fc1b9957942ee6/numexpr-2.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2773ee1133f77009a1fc2f34fe236f3d9823779f5f75450e183137d49f00499f", size = 152088, upload-time = "2025-10-13T16:16:33.186Z" }, + { url = "https://files.pythonhosted.org/packages/0e/7f/3bae417cb13ae08afd86d08bb0301c32440fe0cae4e6262b530e0819aeda/numexpr-2.14.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebe4980f9494b9f94d10d2e526edc29e72516698d3bf95670ba79415492212a4", size = 451126, upload-time = "2025-10-13T16:13:22.248Z" }, + { url = "https://files.pythonhosted.org/packages/4c/1a/edbe839109518364ac0bd9e918cf874c755bb2c128040e920f198c494263/numexpr-2.14.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a381e5e919a745c9503bcefffc1c7f98c972c04ec58fc8e999ed1a929e01ba6", size = 442012, upload-time = "2025-10-13T16:14:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/b1/be4ce99bff769a5003baddac103f34681997b31d4640d5a75c0e8ed59c78/numexpr-2.14.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d08856cfc1b440eb1caaa60515235369654321995dd68eb9377577392020f6cb", size = 1415975, upload-time = "2025-10-13T16:13:26.088Z" }, + { url = "https://files.pythonhosted.org/packages/e7/33/b33b8fdc032a05d9ebb44a51bfcd4b92c178a2572cd3e6c1b03d8a4b45b2/numexpr-2.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03130afa04edf83a7b590d207444f05a00363c9b9ea5d81c0f53b1ea13fad55a", size = 1464683, upload-time = "2025-10-13T16:14:58.87Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b2/ddcf0ac6cf0a1d605e5aecd4281507fd79a9628a67896795ab2e975de5df/numexpr-2.14.1-cp311-cp311-win32.whl", hash = "sha256:db78fa0c9fcbaded3ae7453faf060bd7a18b0dc10299d7fcd02d9362be1213ed", size = 166838, upload-time = "2025-10-13T16:17:06.765Z" }, + { url = "https://files.pythonhosted.org/packages/64/72/4ca9bd97b2eb6dce9f5e70a3b6acec1a93e1fb9b079cb4cba2cdfbbf295d/numexpr-2.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9b2f957798c67a2428be96b04bce85439bed05efe78eb78e4c2ca43737578e7", size = 160069, upload-time = "2025-10-13T16:17:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/9d/20/c473fc04a371f5e2f8c5749e04505c13e7a8ede27c09e9f099b2ad6f43d6/numexpr-2.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:91ebae0ab18c799b0e6b8c5a8d11e1fa3848eb4011271d99848b297468a39430", size = 162790, upload-time = "2025-10-13T16:16:34.903Z" }, + { url = "https://files.pythonhosted.org/packages/45/93/b6760dd1904c2a498e5f43d1bb436f59383c3ddea3815f1461dfaa259373/numexpr-2.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47041f2f7b9e69498fb311af672ba914a60e6e6d804011caacb17d66f639e659", size = 152196, upload-time = "2025-10-13T16:16:36.593Z" }, + { url = "https://files.pythonhosted.org/packages/72/94/cc921e35593b820521e464cbbeaf8212bbdb07f16dc79fe283168df38195/numexpr-2.14.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d686dfb2c1382d9e6e0ee0b7647f943c1886dba3adbf606c625479f35f1956c1", size = 452468, upload-time = "2025-10-13T16:13:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/d9/43/560e9ba23c02c904b5934496486d061bcb14cd3ebba2e3cf0e2dccb6c22b/numexpr-2.14.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee6d4fbbbc368e6cdd0772734d6249128d957b3b8ad47a100789009f4de7083", size = 443631, upload-time = "2025-10-13T16:15:02.473Z" }, + { url = "https://files.pythonhosted.org/packages/7b/6c/78f83b6219f61c2c22d71ab6e6c2d4e5d7381334c6c29b77204e59edb039/numexpr-2.14.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3a2839efa25f3c8d4133252ea7342d8f81226c7c4dda81f97a57e090b9d87a48", size = 1417670, upload-time = "2025-10-13T16:13:33.464Z" }, + { url = "https://files.pythonhosted.org/packages/0e/bb/1ccc9dcaf46281568ce769888bf16294c40e98a5158e4b16c241de31d0d3/numexpr-2.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9f9137f1351b310436662b5dc6f4082a245efa8950c3b0d9008028df92fefb9b", size = 1466212, upload-time = "2025-10-13T16:15:12.828Z" }, + { url = "https://files.pythonhosted.org/packages/31/9f/203d82b9e39dadd91d64bca55b3c8ca432e981b822468dcef41a4418626b/numexpr-2.14.1-cp312-cp312-win32.whl", hash = "sha256:36f8d5c1bd1355df93b43d766790f9046cccfc1e32b7c6163f75bcde682cda07", size = 166996, upload-time = "2025-10-13T16:17:10.369Z" }, + { url = "https://files.pythonhosted.org/packages/1f/67/ffe750b5452eb66de788c34e7d21ec6d886abb4d7c43ad1dc88ceb3d998f/numexpr-2.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:fdd886f4b7dbaf167633ee396478f0d0aa58ea2f9e7ccc3c6431019623e8d68f", size = 160187, upload-time = "2025-10-13T16:17:11.974Z" }, + { url = "https://files.pythonhosted.org/packages/73/b4/9f6d637fd79df42be1be29ee7ba1f050fab63b7182cb922a0e08adc12320/numexpr-2.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09078ba73cffe94745abfbcc2d81ab8b4b4e9d7bfbbde6cac2ee5dbf38eee222", size = 162794, upload-time = "2025-10-13T16:16:38.291Z" }, + { url = "https://files.pythonhosted.org/packages/35/ae/d58558d8043de0c49f385ea2fa789e3cfe4d436c96be80200c5292f45f15/numexpr-2.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dce0b5a0447baa7b44bc218ec2d7dcd175b8eee6083605293349c0c1d9b82fb6", size = 152203, upload-time = "2025-10-13T16:16:39.907Z" }, + { url = "https://files.pythonhosted.org/packages/13/65/72b065f9c75baf8f474fd5d2b768350935989d4917db1c6c75b866d4067c/numexpr-2.14.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06855053de7a3a8425429bd996e8ae3c50b57637ad3e757e0fa0602a7874be30", size = 455860, upload-time = "2025-10-13T16:13:35.811Z" }, + { url = "https://files.pythonhosted.org/packages/fc/f9/c9457652dfe28e2eb898372da2fe786c6db81af9540c0f853ee04a0699cc/numexpr-2.14.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f9366d23a2e991fd5a8b5e61a17558f028ba86158a4552f8f239b005cdf83c", size = 446574, upload-time = "2025-10-13T16:15:17.367Z" }, + { url = "https://files.pythonhosted.org/packages/b6/99/8d3879c4d67d3db5560cf2de65ce1778b80b75f6fa415eb5c3e7bd37ba27/numexpr-2.14.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c5f1b1605695778896534dfc6e130d54a65cd52be7ed2cd0cfee3981fd676bf5", size = 1417306, upload-time = "2025-10-13T16:13:42.813Z" }, + { url = "https://files.pythonhosted.org/packages/ea/05/6bddac9f18598ba94281e27a6943093f7d0976544b0cb5d92272c64719bd/numexpr-2.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4ba71db47ea99c659d88ee6233fa77b6dc83392f1d324e0c90ddf617ae3f421", size = 1466145, upload-time = "2025-10-13T16:15:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/24/5d/cbeb67aca0c5a76ead13df7e8bd8dd5e0d49145f90da697ba1d9f07005b0/numexpr-2.14.1-cp313-cp313-win32.whl", hash = "sha256:638dce8320f4a1483d5ca4fda69f60a70ed7e66be6e68bc23fb9f1a6b78a9e3b", size = 166996, upload-time = "2025-10-13T16:17:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/cc/23/9281bceaeb282cead95f0aa5f7f222ffc895670ea689cc1398355f6e3001/numexpr-2.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:9fdcd4735121658a313f878fd31136d1bfc6a5b913219e7274e9fca9f8dac3bb", size = 160189, upload-time = "2025-10-13T16:17:15.417Z" }, + { url = "https://files.pythonhosted.org/packages/f3/76/7aac965fd93a56803cbe502aee2adcad667253ae34b0badf6c5af7908b6c/numexpr-2.14.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:557887ad7f5d3c2a40fd7310e50597045a68e66b20a77b3f44d7bc7608523b4b", size = 163524, upload-time = "2025-10-13T16:16:42.213Z" }, + { url = "https://files.pythonhosted.org/packages/58/65/79d592d5e63fbfab3b59a60c386853d9186a44a3fa3c87ba26bdc25b6195/numexpr-2.14.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:af111c8fe6fc55d15e4c7cab11920fc50740d913636d486545b080192cd0ad73", size = 152919, upload-time = "2025-10-13T16:16:44.229Z" }, + { url = "https://files.pythonhosted.org/packages/84/78/3c8335f713d4aeb99fa758d7c62f0be1482d4947ce5b508e2052bb7aeee9/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33265294376e7e2ae4d264d75b798a915d2acf37b9dd2b9405e8b04f84d05cfc", size = 465972, upload-time = "2025-10-13T16:13:45.061Z" }, + { url = "https://files.pythonhosted.org/packages/35/81/9ee5f69b811e8f18746c12d6f71848617684edd3161927f95eee7a305631/numexpr-2.14.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83647d846d3eeeb9a9255311236135286728b398d0d41d35dedb532dca807fe9", size = 456953, upload-time = "2025-10-13T16:15:31.186Z" }, + { url = "https://files.pythonhosted.org/packages/6d/39/9b8bc6e294d85cbb54a634e47b833e9f3276a8bdf7ce92aa808718a0212d/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6e575fd3ad41ddf3355d0c7ef6bd0168619dc1779a98fe46693cad5e95d25e6e", size = 1426199, upload-time = "2025-10-13T16:13:48.231Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ce/0d4fcd31ab49319740d934fba1734d7dad13aa485532ca754e555ca16c8b/numexpr-2.14.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:67ea4771029ce818573b1998f5ca416bd255156feea017841b86176a938f7d19", size = 1474214, upload-time = "2025-10-13T16:15:38.893Z" }, + { url = "https://files.pythonhosted.org/packages/b7/47/b2a93cbdb3ba4e009728ad1b9ef1550e2655ea2c86958ebaf03b9615f275/numexpr-2.14.1-cp313-cp313t-win32.whl", hash = "sha256:15015d47d3d1487072d58c0e7682ef2eb608321e14099c39d52e2dd689483611", size = 167676, upload-time = "2025-10-13T16:17:17.351Z" }, + { url = "https://files.pythonhosted.org/packages/86/99/ee3accc589ed032eea68e12172515ed96a5568534c213ad109e1f4411df1/numexpr-2.14.1-cp313-cp313t-win_amd64.whl", hash = "sha256:94c711f6d8f17dfb4606842b403699603aa591ab9f6bf23038b488ea9cfb0f09", size = 161096, upload-time = "2025-10-13T16:17:19.174Z" }, + { url = "https://files.pythonhosted.org/packages/ac/36/9db78dfbfdfa1f8bf0872993f1a334cdd8fca5a5b6567e47dcb128bcb7c2/numexpr-2.14.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ede79f7ff06629f599081de644546ce7324f1581c09b0ac174da88a470d39c21", size = 162848, upload-time = "2025-10-13T16:16:46.216Z" }, + { url = "https://files.pythonhosted.org/packages/13/c1/a5c78ae637402c5550e2e0ba175275d2515d432ec28af0cdc23c9b476e65/numexpr-2.14.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2eac7a5a2f70b3768c67056445d1ceb4ecd9b853c8eda9563823b551aeaa5082", size = 152270, upload-time = "2025-10-13T16:16:47.92Z" }, + { url = "https://files.pythonhosted.org/packages/9a/ed/aabd8678077848dd9a751c5558c2057839f5a09e2a176d8dfcd0850ee00e/numexpr-2.14.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aedf38d4c0c19d3cecfe0334c3f4099fb496f54c146223d30fa930084bc8574", size = 455918, upload-time = "2025-10-13T16:13:50.338Z" }, + { url = "https://files.pythonhosted.org/packages/88/e1/3db65117f02cdefb0e5e4c440daf1c30beb45051b7f47aded25b7f4f2f34/numexpr-2.14.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:439ec4d57b853792ebe5456e3160312281c3a7071ecac5532ded3278ede614de", size = 446512, upload-time = "2025-10-13T16:15:42.313Z" }, + { url = "https://files.pythonhosted.org/packages/9a/fb/7ceb9ee55b5f67e4a3e4d73d5af4c7e37e3c9f37f54bee90361b64b17e3f/numexpr-2.14.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e23b87f744e04e302d82ac5e2189ae20a533566aec76a46885376e20b0645bf8", size = 1417845, upload-time = "2025-10-13T16:13:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/9b5764d0eafbbb2889288f80de773791358acf6fad1a55767538d8b79599/numexpr-2.14.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:44f84e0e5af219dbb62a081606156420815890e041b87252fbcea5df55214c4c", size = 1466211, upload-time = "2025-10-13T16:15:48.985Z" }, + { url = "https://files.pythonhosted.org/packages/5d/21/204db708eccd71aa8bc55bcad55bc0fc6c5a4e01ad78e14ee5714a749386/numexpr-2.14.1-cp314-cp314-win32.whl", hash = "sha256:1f1a5e817c534539351aa75d26088e9e1e0ef1b3a6ab484047618a652ccc4fc3", size = 168835, upload-time = "2025-10-13T16:17:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/4f/3e/d83e9401a1c3449a124f7d4b3fb44084798e0d30f7c11e60712d9b94cf11/numexpr-2.14.1-cp314-cp314-win_amd64.whl", hash = "sha256:587c41509bc373dfb1fe6086ba55a73147297247bedb6d588cda69169fc412f2", size = 162608, upload-time = "2025-10-13T16:17:22.228Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d6/ec947806bb57836d6379a8c8a253c2aeaa602b12fef2336bfd2462bb4ed5/numexpr-2.14.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec368819502b64f190c3f71be14a304780b5935c42aae5bf22c27cc2cbba70b5", size = 163525, upload-time = "2025-10-13T16:16:50.133Z" }, + { url = "https://files.pythonhosted.org/packages/0d/77/048f30dcf661a3d52963a88c29b52b6d5ce996d38e9313a56a922451c1e0/numexpr-2.14.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e87f6d203ac57239de32261c941e9748f9309cbc0da6295eabd0c438b920d3a", size = 152917, upload-time = "2025-10-13T16:16:52.055Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/956a13e628d722d649fbf2fded615134a308c082e122a48bad0e90a99ce9/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dd72d8c2a165fe45ea7650b16eb8cc1792a94a722022006bb97c86fe51fd2091", size = 466242, upload-time = "2025-10-13T16:13:55.795Z" }, + { url = "https://files.pythonhosted.org/packages/d6/dd/abe848678d82486940892f2cacf39e82eec790e8930d4d713d3f9191063b/numexpr-2.14.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70d80fcb418a54ca208e9a38e58ddc425c07f66485176b261d9a67c7f2864f73", size = 457149, upload-time = "2025-10-13T16:15:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/fd/bb/797b583b5fb9da5700a5708ca6eb4f889c94d81abb28de4d642c0f4b3258/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:edea2f20c2040df8b54ee8ca8ebda63de9545b2112872466118e9df4d0ae99f3", size = 1426493, upload-time = "2025-10-13T16:13:59.244Z" }, + { url = "https://files.pythonhosted.org/packages/77/c4/0519ab028fdc35e3e7ee700def7f2b4631b175cd9e1202bd7966c1695c33/numexpr-2.14.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:790447be6879a6c51b9545f79612d24c9ea0a41d537a84e15e6a8ddef0b6268e", size = 1474413, upload-time = "2025-10-13T16:15:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/d4/4a/33044878c8f4a75213cfe9c11d4c02058bb710a7a063fe14f362e8de1077/numexpr-2.14.1-cp314-cp314t-win32.whl", hash = "sha256:538961096c2300ea44240209181e31fae82759d26b51713b589332b9f2a4117e", size = 169502, upload-time = "2025-10-13T16:17:23.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/a2/5a1a2c72528b429337f49911b18c302ecd36eeab00f409147e1aa4ae4519/numexpr-2.14.1-cp314-cp314t-win_amd64.whl", hash = "sha256:a40b350cd45b4446076fa11843fa32bbe07024747aeddf6d467290bf9011b392", size = 163589, upload-time = "2025-10-13T16:17:25.696Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468, upload-time = "2024-02-05T23:48:01.194Z" }, + { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411, upload-time = "2024-02-05T23:48:29.038Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016, upload-time = "2024-02-05T23:48:54.098Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889, upload-time = "2024-02-05T23:49:25.361Z" }, + { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746, upload-time = "2024-02-05T23:49:51.983Z" }, + { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620, upload-time = "2024-02-05T23:50:22.515Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659, upload-time = "2024-02-05T23:50:35.834Z" }, + { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905, upload-time = "2024-02-05T23:51:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554, upload-time = "2024-02-05T23:51:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127, upload-time = "2024-02-05T23:52:15.314Z" }, + { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994, upload-time = "2024-02-05T23:52:47.569Z" }, + { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005, upload-time = "2024-02-05T23:53:15.637Z" }, + { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297, upload-time = "2024-02-05T23:53:42.16Z" }, + { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" }, + { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" }, + { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" }, + { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" }, + { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" }, + { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" }, + { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" }, + { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" }, + { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" }, + { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" }, + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, + { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" }, + { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" }, + { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" }, +] + +[[package]] +name = "packaging" +version = "26.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pandas" +version = "2.3.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "python-dateutil", marker = "python_full_version < '3.11'" }, + { name = "pytz", marker = "python_full_version < '3.11'" }, + { name = "tzdata", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" }, + { url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" }, + { url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" }, + { url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" }, + { url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" }, + { url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" }, + { url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" }, + { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" }, + { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" }, + { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" }, + { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" }, + { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" }, + { url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" }, + { url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" }, + { url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" }, + { url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" }, + { url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" }, + { url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" }, + { url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" }, + { url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" }, + { url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" }, + { url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" }, + { url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" }, + { url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" }, + { url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" }, + { url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" }, + { url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" }, + { url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" }, + { url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" }, + { url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" }, + { url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" }, + { url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" }, +] + +[[package]] +name = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "python-dateutil", marker = "python_full_version >= '3.11'" }, + { name = "tzdata", marker = "(python_full_version >= '3.11' and sys_platform == 'emscripten') or (python_full_version >= '3.11' and sys_platform == 'win32')" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/97/35/6411db530c618e0e0005187e35aa02ce60ae4c4c4d206964a2f978217c27/pandas-3.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a727a73cbdba2f7458dc82449e2315899d5140b449015d822f515749a46cbbe0", size = 10326926, upload-time = "2026-03-31T06:46:08.29Z" }, + { url = "https://files.pythonhosted.org/packages/c4/d3/b7da1d5d7dbdc5ef52ed7debd2b484313b832982266905315dad5a0bf0b1/pandas-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dbbd4aa20ca51e63b53bbde6a0fa4254b1aaabb74d2f542df7a7959feb1d760c", size = 9926987, upload-time = "2026-03-31T06:46:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/52/77/9b1c2d6070b5dbe239a7bc889e21bfa58720793fb902d1e070695d87c6d0/pandas-3.0.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:339dda302bd8369dedeae979cb750e484d549b563c3f54f3922cb8ff4978c5eb", size = 10757067, upload-time = "2026-03-31T06:46:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/20/17/ec40d981705654853726e7ac9aea9ddbb4a5d9cf54d8472222f4f3de06c2/pandas-3.0.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:61c2fd96d72b983a9891b2598f286befd4ad262161a609c92dc1652544b46b76", size = 11258787, upload-time = "2026-03-31T06:46:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/90/e3/3f1126d43d3702ca8773871a81c9f15122a1f412342cc56284ffda5b1f70/pandas-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c934008c733b8bbea273ea308b73b3156f0181e5b72960790b09c18a2794fe1e", size = 11771616, upload-time = "2026-03-31T06:46:20.532Z" }, + { url = "https://files.pythonhosted.org/packages/2e/cf/0f4e268e1f5062e44a6bda9f925806721cd4c95c2b808a4c82ebe914f96b/pandas-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:60a80bb4feacbef5e1447a3f82c33209c8b7e07f28d805cfd1fb951e5cb443aa", size = 12337623, upload-time = "2026-03-31T06:46:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/44/a0/97a6339859d4acb2536efb24feb6708e82f7d33b2ed7e036f2983fcced82/pandas-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:ed72cb3f45190874eb579c64fa92d9df74e98fd63e2be7f62bce5ace0ade61df", size = 9897372, upload-time = "2026-03-31T06:46:26.703Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/781516b808a99ddf288143cec46b342b3016c3414d137da1fdc3290d8860/pandas-3.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:f12b1a9e332c01e09510586f8ca9b108fd631fd656af82e452d7315ef6df5f9f", size = 9154922, upload-time = "2026-03-31T06:46:30.284Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "2.3.3.260113" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "types-pytz", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/92/5d/be23854a73fda69f1dbdda7bc10fbd6f930bd1fa87aaec389f00c901c1e8/pandas_stubs-2.3.3.260113.tar.gz", hash = "sha256:076e3724bcaa73de78932b012ec64b3010463d377fa63116f4e6850643d93800", size = 116131, upload-time = "2026-01-13T22:30:16.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/c6/df1fe324248424f77b89371116dab5243db7f052c32cc9fe7442ad9c5f75/pandas_stubs-2.3.3.260113-py3-none-any.whl", hash = "sha256:ec070b5c576e1badf12544ae50385872f0631fc35d99d00dc598c2954ec564d3", size = 168246, upload-time = "2026-01-13T22:30:15.244Z" }, +] + +[[package]] +name = "pandas-stubs" +version = "3.0.0.260204" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/1d/297ff2c7ea50a768a2247621d6451abb2a07c0e9be7ca6d36ebe371658e5/pandas_stubs-3.0.0.260204.tar.gz", hash = "sha256:bf9294b76352effcffa9cb85edf0bed1339a7ec0c30b8e1ac3d66b4228f1fbc3", size = 109383, upload-time = "2026-02-04T15:17:17.247Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/2f/f91e4eee21585ff548e83358332d5632ee49f6b2dcd96cb5dca4e0468951/pandas_stubs-3.0.0.260204-py3-none-any.whl", hash = "sha256:5ab9e4d55a6e2752e9720828564af40d48c4f709e6a2c69b743014a6fcb6c241", size = 168540, upload-time = "2026-02-04T15:17:15.615Z" }, +] + +[[package]] +name = "partd" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "locket" }, + { name = "toolz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/3a/3f06f34820a31257ddcabdfafc2672c5816be79c7e353b02c1f318daa7d4/partd-1.4.2.tar.gz", hash = "sha256:d022c33afbdc8405c226621b015e8067888173d85f7f5ecebb3cafed9a20f02c", size = 21029, upload-time = "2024-05-06T19:51:41.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/e7/40fb618334dcdf7c5a316c0e7343c5cd82d3d866edc100d98e29bc945ecd/partd-1.4.2-py3-none-any.whl", hash = "sha256:978e4ac767ec4ba5b86c6eaa52e5a2a3bc748a2ca839e8cc798f1cc6ce6efb0f", size = 18905, upload-time = "2024-05-06T19:51:39.271Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/17/9c3094b822982b9f1ea666d8580ce59000f61f87c1663556fb72031ad9ec/pathspec-1.1.0.tar.gz", hash = "sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080", size = 133918, upload-time = "2026-04-23T01:46:22.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/c9/8eed0486f074e9f1ca7f8ce5ad663e65f12fdab344028d658fa1b03d35e0/pathspec-1.1.0-py3-none-any.whl", hash = "sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42", size = 56264, upload-time = "2026-04-23T01:46:20.606Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polars" +version = "1.39.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/93/ab/f19e592fce9e000da49c96bf35e77cef67f9cb4b040bfa538a2764c0263e/polars-1.39.3.tar.gz", hash = "sha256:2e016c7f3e8d14fa777ef86fe0477cec6c67023a20ba4c94d6e8431eefe4a63c", size = 728987, upload-time = "2026-03-20T11:16:24.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/db/08f4ca10c5018813e7e0b59e4472302328b3d2ab1512f5a2157a814540e0/polars-1.39.3-py3-none-any.whl", hash = "sha256:c2b955ccc0a08a2bc9259785decf3d5c007b489b523bf2390cf21cec2bb82a56", size = 823985, upload-time = "2026-03-20T11:14:23.619Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.39.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/17/39/c8688696bc22b6c501e3b82ef3be10e543c07a785af5660f30997cd22dd2/polars_runtime_32-1.39.3.tar.gz", hash = "sha256:c728e4f469cafab501947585f36311b8fb222d3e934c6209e83791e0df20b29d", size = 2872335, upload-time = "2026-03-20T11:16:26.581Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/74/1b41205f7368c9375ab1dea91178eaa20435fe3eff036390a53a7660b416/polars_runtime_32-1.39.3-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:425c0b220b573fa097b4042edff73114cc6d23432a21dfd2dc41adf329d7d2e9", size = 45273243, upload-time = "2026-03-20T11:14:26.691Z" }, + { url = "https://files.pythonhosted.org/packages/90/bf/297716b3095fe719be20fcf7af1d2b6ab069c38199bbace2469608a69b3a/polars_runtime_32-1.39.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:ef5884711e3c617d7dc93519a7d038e242f5741cfe5fe9afd32d58845d86c562", size = 40842924, upload-time = "2026-03-20T11:14:31.154Z" }, + { url = "https://files.pythonhosted.org/packages/3d/3e/e65236d9d0d9babfa0ecba593413c06530fca60a8feb8f66243aa5dba92e/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06b47f535eb1f97a9a1e5b0053ef50db3a4276e241178e37bbb1a38b1fa53b14", size = 43220650, upload-time = "2026-03-20T11:14:35.458Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/fc3e43f3fdf3f20b7dfb5abe871ab6162cf8fb4aeabf4cfad822d5dc4c79/polars_runtime_32-1.39.3-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bc9e13dc1d2e828331f2fe8ccbc9757554dc4933a8d3e85e906b988178f95ed", size = 46877498, upload-time = "2026-03-20T11:14:40.14Z" }, + { url = "https://files.pythonhosted.org/packages/3c/81/bd5f895919e32c6ab0a7786cd0c0ca961cb03152c47c3645808b54383f31/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:363d49e3a3e638fc943e2b9887940300a7d06789930855a178a4727949259dc2", size = 43380176, upload-time = "2026-03-20T11:14:45.566Z" }, + { url = "https://files.pythonhosted.org/packages/7a/3e/c86433c3b5ec0315bdfc7640d0c15d41f1216c0103a0eab9a9b5147d6c4c/polars_runtime_32-1.39.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7c206bdcc7bc62ea038d6adea8e44b02f0e675e0191a54c810703b4895208ea4", size = 46485933, upload-time = "2026-03-20T11:14:51.155Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/200b310cf91f98e652eb6ea09fdb3a9718aa0293ebf113dce325797c8572/polars_runtime_32-1.39.3-cp310-abi3-win_amd64.whl", hash = "sha256:d66ca522517554a883446957539c40dc7b75eb0c2220357fb28bc8940d305339", size = 46995458, upload-time = "2026-03-20T11:14:56.074Z" }, + { url = "https://files.pythonhosted.org/packages/da/76/2d48927e0aa2abbdde08cbf4a2536883b73277d47fbeca95e952de86df34/polars_runtime_32-1.39.3-cp310-abi3-win_arm64.whl", hash = "sha256:f49f51461de63f13e5dd4eb080421c8f23f856945f3f8bd5b2b1f59da52c2860", size = 41857648, upload-time = "2026-03-20T11:15:01.142Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + +[[package]] +name = "proto-plus" +version = "1.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/81/0d/94dfe80193e79d55258345901acd2917523d56e8381bc4dee7fd38e3868a/proto_plus-1.27.2.tar.gz", hash = "sha256:b2adde53adadf75737c44d3dcb0104fde65250dfc83ad59168b4aa3e574b6a24", size = 57204, upload-time = "2026-03-26T22:18:57.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/f3/1fba73eeffafc998a25d59703b63f8be4fe8a5cb12eaff7386a0ba0f7125/proto_plus-1.27.2-py3-none-any.whl", hash = "sha256:6432f75893d3b9e70b9c412f1d2f03f65b11fb164b793d14ae2ca01821d22718", size = 50450, upload-time = "2026-03-26T22:13:42.927Z" }, +] + +[[package]] +name = "protobuf" +version = "7.34.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6b/6b/a0e95cad1ad7cc3f2c6821fcab91671bd5b78bd42afb357bb4765f29bc41/protobuf-7.34.1.tar.gz", hash = "sha256:9ce42245e704cc5027be797c1db1eb93184d44d1cdd71811fb2d9b25ad541280", size = 454708, upload-time = "2026-03-20T17:34:47.036Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/11/3325d41e6ee15bf1125654301211247b042563bcc898784351252549a8ad/protobuf-7.34.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:d8b2cc79c4d8f62b293ad9b11ec3aebce9af481fa73e64556969f7345ebf9fc7", size = 429247, upload-time = "2026-03-20T17:34:37.024Z" }, + { url = "https://files.pythonhosted.org/packages/eb/9d/aa69df2724ff63efa6f72307b483ce0827f4347cc6d6df24b59e26659fef/protobuf-7.34.1-cp310-abi3-manylinux2014_aarch64.whl", hash = "sha256:5185e0e948d07abe94bb76ec9b8416b604cfe5da6f871d67aad30cbf24c3110b", size = 325753, upload-time = "2026-03-20T17:34:38.751Z" }, + { url = "https://files.pythonhosted.org/packages/92/e8/d174c91fd48e50101943f042b09af9029064810b734e4160bbe282fa1caa/protobuf-7.34.1-cp310-abi3-manylinux2014_s390x.whl", hash = "sha256:403b093a6e28a960372b44e5eb081775c9b056e816a8029c61231743d63f881a", size = 340198, upload-time = "2026-03-20T17:34:39.871Z" }, + { url = "https://files.pythonhosted.org/packages/53/1b/3b431694a4dc6d37b9f653f0c64b0a0d9ec074ee810710c0c3da21d67ba7/protobuf-7.34.1-cp310-abi3-manylinux2014_x86_64.whl", hash = "sha256:8ff40ce8cd688f7265326b38d5a1bed9bfdf5e6723d49961432f83e21d5713e4", size = 324267, upload-time = "2026-03-20T17:34:41.1Z" }, + { url = "https://files.pythonhosted.org/packages/85/29/64de04a0ac142fb685fd09999bc3d337943fb386f3a0ec57f92fd8203f97/protobuf-7.34.1-cp310-abi3-win32.whl", hash = "sha256:34b84ce27680df7cca9f231043ada0daa55d0c44a2ddfaa58ec1d0d89d8bf60a", size = 426628, upload-time = "2026-03-20T17:34:42.536Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/cb5e585192a22b8bd457df5a2c16a75ea0db9674c3a0a39fc9347d84e075/protobuf-7.34.1-cp310-abi3-win_amd64.whl", hash = "sha256:e97b55646e6ce5cbb0954a8c28cd39a5869b59090dfaa7df4598a7fba869468c", size = 437901, upload-time = "2026-03-20T17:34:44.112Z" }, + { url = "https://files.pythonhosted.org/packages/88/95/608f665226bca68b736b79e457fded9a2a38c4f4379a4a7614303d9db3bc/protobuf-7.34.1-py3-none-any.whl", hash = "sha256:bb3812cd53aefea2b028ef42bd780f5b96407247f20c6ef7c679807e9d188f11", size = 170715, upload-time = "2026-03-20T17:34:45.384Z" }, +] + +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/bf/a34fee1d624152124fa8355c42f34195ad5fe5233ce5bb87946432047d52/pyarrow-24.0.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:7c2b98645d576a0b9616892ead22b64a83a5f043c5e2ca15ebcefcb5b70c80cb", size = 35076681, upload-time = "2026-04-21T08:51:46.845Z" }, + { url = "https://files.pythonhosted.org/packages/1d/41/64180033d7027afce12dc96d0fe1f504c6fa112190582b458acea2399530/pyarrow-24.0.0-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:644a246325b8c69c595ad1dd4b463eba4b0cdb731370e4a86137d433208d6147", size = 36684260, upload-time = "2026-04-21T08:51:53.642Z" }, + { url = "https://files.pythonhosted.org/packages/57/02/9b9320e673dd8a99411fac78690f3df92f6dd6f59754c750110bca66d64e/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:3a577bd840ca83f646f0a625dbc571dba7044c43c2d1503afc378b570954345c", size = 45698566, upload-time = "2026-04-21T10:46:02.133Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/f75e91b9a64c3f33c787e263c93b871ad91b8a4a68c1d5cebddd9840e835/pyarrow-24.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e3268e43984d0b1a185c89b4cfff282a7ead12fc93f56cfd7088bdbcbe727041", size = 48835562, upload-time = "2026-04-21T10:46:10.278Z" }, + { url = "https://files.pythonhosted.org/packages/a5/63/097510448e47e4091faa41c43ba92f97cecaab8f4535b56a3d149578f634/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2392d954fcb920f42d230284b677605e4e2fbb11f2821e823e642abd67fbb491", size = 49394997, upload-time = "2026-04-21T10:46:18.08Z" }, + { url = "https://files.pythonhosted.org/packages/60/6b/c047d6222ab279024a062742d1807e2fbaf27bba88a98637299ff47b9236/pyarrow-24.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bec9373df11544592b0ba7ec2af0e35059e5f0e7647c6183a854dedd193298f1", size = 51911424, upload-time = "2026-04-21T10:46:25.347Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ba/464cc70761c2a525d97ebd84e21c31ebd47f3ef4bdcee117009f51c46f24/pyarrow-24.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:c42ab9439498270139cc63e18847a02afe5c8b3ed9c931266533cfe378bd3591", size = 27251730, upload-time = "2026-04-21T10:46:30.913Z" }, + { url = "https://files.pythonhosted.org/packages/62/c9/a47ab7ece0d86cbe6678418a0fbd1ac4bb493b9184a3891dfa0e7f287ae0/pyarrow-24.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:b0e131f880cda8d04e076cee175a46fc0e8bc8b65c99c6c09dff6669335fde74", size = 35068898, upload-time = "2026-04-21T10:46:36.599Z" }, + { url = "https://files.pythonhosted.org/packages/d1/bc/8db86617a9a58008acf8913d6fed68ea2a46acb6de928db28d724c891a68/pyarrow-24.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:1b2fe7f9a5566401a0ef2571f197eb92358925c1f0c8dba305d6e43ea0871bb3", size = 36679915, upload-time = "2026-04-21T10:46:42.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/8e/fb178720400ef69db251eb4a9c3ccf4af269bc1feb5055529b8fc87170d1/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:0b3537c00fb8d384f15ac1e79b6eb6db04a16514c8c1d22e59a9b95c8ba42868", size = 45697931, upload-time = "2026-04-21T10:46:48.403Z" }, + { url = "https://files.pythonhosted.org/packages/f3/27/99c42abe8e21b44f4917f62631f3aa31404882a2c41d8a4cd5c110e13d52/pyarrow-24.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:14e31a3c9e35f1ab6356c6378f6f72830e6d2d5f1791df3774a7b097d18a6a1e", size = 48837449, upload-time = "2026-04-21T10:46:55.329Z" }, + { url = "https://files.pythonhosted.org/packages/36/b6/333749e2666e9032891125bf9c691146e92901bece62030ac1430e2e7c88/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b7d9a514e73bc42711e6a35aaccf3587c520024fe0a25d830a1a8a27c15f4f57", size = 49395949, upload-time = "2026-04-21T10:47:01.869Z" }, + { url = "https://files.pythonhosted.org/packages/17/25/c5201706a2dd374e8ba6ee3fd7a8c89fb7ffc16eed5217a91fd2bd7f7626/pyarrow-24.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b196eb3f931862af3fa84c2a253514d859c08e0d8fe020e07be12e75a5a9780c", size = 51912986, upload-time = "2026-04-21T10:47:09.872Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d2/4d1bbba65320b21a49678d6fbdc6ff7c649251359fdcfc03568c4136231d/pyarrow-24.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:35405aecb474e683fb36af650618fd5340ee5471fc65a21b36076a18bbc6c981", size = 27255371, upload-time = "2026-04-21T10:47:15.943Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + +[[package]] +name = "pyasn1" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.21.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/08/f1c908c581fd11913da4711ea7ba32c0eee40b0190000996bb863b0c9349/pymdown_extensions-10.21.2.tar.gz", hash = "sha256:c3f55a5b8a1d0edf6699e35dcbea71d978d34ff3fa79f3d807b8a5b3fa90fbdc", size = 853922, upload-time = "2026-03-29T15:01:55.233Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/27/a2fc51a4a122dfd1015e921ae9d22fee3d20b0b8080d9a704578bf9deece/pymdown_extensions-10.21.2-py3-none-any.whl", hash = "sha256:5c0fd2a2bea14eb39af8ff284f1066d898ab2187d81b889b75d46d4348c01638", size = 268901, upload-time = "2026-03-29T15:01:53.244Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-cov" +version = "7.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage", extra = ["toml"] }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, +] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + +[[package]] +name = "pytokens" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" }, + { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" }, + { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" }, + { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" }, + { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, + { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, + { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, + { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, + { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, + { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, + { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, + { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, + { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, + { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, + { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, + { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, + { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, + { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, + { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, + { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, + { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, + { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, + { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, +] + +[[package]] +name = "pytz" +version = "2026.1.post1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/56/db/b8721d71d945e6a8ac63c0fc900b2067181dbb50805958d4d4661cf7d277/pytz-2026.1.post1.tar.gz", hash = "sha256:3378dde6a0c3d26719182142c56e60c7f9af7e968076f31aae569d72a0358ee1", size = 321088, upload-time = "2026-03-03T07:47:50.683Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/99/781fe0c827be2742bcc775efefccb3b048a3a9c6ce9aec0cbf4a101677e5/pytz-2026.1.post1-py2.py3-none-any.whl", hash = "sha256:f2fd16142fda348286a75e1a524be810bb05d444e5a081f37f7affc635035f7a", size = 510489, upload-time = "2026-03-03T07:47:49.167Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, + { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, + { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, + { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, + { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, + { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, + { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, + { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, + { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, + { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, + { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, + { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, + { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + +[[package]] +name = "requests" +version = "2.33.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5f/a4/98b9c7c6428a668bf7e42ebb7c79d576a1c3c1e3ae2d47e674b468388871/requests-2.33.1.tar.gz", hash = "sha256:18817f8c57c6263968bc123d237e3b8b08ac046f5456bd1e307ee8f4250d3517", size = 134120, upload-time = "2026-03-30T16:09:15.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + +[[package]] +name = "toolz" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/d6/114b492226588d6ff54579d95847662fc69196bdeec318eb45393b24c192/toolz-1.1.0.tar.gz", hash = "sha256:27a5c770d068c110d9ed9323f24f1543e83b2f300a687b7891c1a6d56b697b5b", size = 52613, upload-time = "2025-10-17T04:03:21.661Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl", hash = "sha256:15ccc861ac51c53696de0a5d6d4607f99c210739caf987b5d2054f3efed429d8", size = 58093, upload-time = "2025-10-17T04:03:20.435Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "types-pytz" +version = "2026.1.1.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f7/b7/33f5a4f29b1f285b99ff79a607751a7996194cbb98705e331dab7a2daa28/types_pytz-2026.1.1.20260408.tar.gz", hash = "sha256:89b6a34b9198ea2a4b98a9d15cbca987053f52a105fd44f7ce3789cae4349408", size = 10788, upload-time = "2026-04-08T04:28:14.54Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/90/12c059e6bb330a22d9cc97daf027ac7fb7f50fbf518e4d88185b4d39120e/types_pytz-2026.1.1.20260408-py3-none-any.whl", hash = "sha256:c7e4dec76221fb7d0c97b91ad8561d689bebe39b6bcb7b728387e7ffd8cde788", size = 10124, upload-time = "2026-04-08T04:28:13.353Z" }, +] + +[[package]] +name = "types-pyyaml" +version = "6.0.12.20260408" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/73/b759b1e413c31034cc01ecdfb96b38115d0ab4db55a752a3929f0cd449fd/types_pyyaml-6.0.12.20260408.tar.gz", hash = "sha256:92a73f2b8d7f39ef392a38131f76b970f8c66e4c42b3125ae872b7c93b556307", size = 17735, upload-time = "2026-04-08T04:30:50.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/f0/c391068b86abb708882c6d75a08cd7d25b2c7227dab527b3a3685a3c635b/types_pyyaml-6.0.12.20260408-py3-none-any.whl", hash = "sha256:fbc42037d12159d9c801ebfcc79ebd28335a7c13b08a4cfbc6916df78fee9384", size = 20339, upload-time = "2026-04-08T04:30:50.113Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/f5/cd531b2d15a671a40c0f66cf06bc3570a12cd56eef98960068ebbad1bf5a/tzdata-2026.1.tar.gz", hash = "sha256:67658a1903c75917309e753fdc349ac0efd8c27db7a0cb406a25be4840f87f98", size = 197639, upload-time = "2026-04-03T11:25:22.002Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b0/70/d460bd685a170790ec89317e9bd33047988e4bce507b831f5db771e142de/tzdata-2026.1-py2.py3-none-any.whl", hash = "sha256:4b1d2be7ac37ceafd7327b961aa3a54e467efbdb563a23655fbfe0d39cfc42a9", size = 348952, upload-time = "2026-04-03T11:25:20.313Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "xarray" +version = "2025.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] +dependencies = [ + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "packaging", marker = "python_full_version < '3.11'" }, + { name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/19/ec/e50d833518f10b0c24feb184b209bb6856f25b919ba8c1f89678b930b1cd/xarray-2025.6.1.tar.gz", hash = "sha256:a84f3f07544634a130d7dc615ae44175419f4c77957a7255161ed99c69c7c8b0", size = 3003185, upload-time = "2025-06-12T03:04:09.099Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/8a/6b50c1dd2260d407c1a499d47cf829f59f07007e0dcebafdabb24d1d26a5/xarray-2025.6.1-py3-none-any.whl", hash = "sha256:8b988b47f67a383bdc3b04c5db475cd165e580134c1f1943d52aee4a9c97651b", size = 1314739, upload-time = "2025-06-12T03:04:06.708Z" }, +] + +[[package]] +name = "xarray" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform == 'emscripten'", + "python_full_version == '3.14.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.15' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] +dependencies = [ + { name = "numpy", version = "2.4.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "packaging", marker = "python_full_version >= '3.11'" }, + { name = "pandas", version = "3.0.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/03/e3353b72e518574b32993989d8f696277bf878e9d508c7dd22e86c0dab5b/xarray-2026.2.0.tar.gz", hash = "sha256:978b6acb018770554f8fd964af4eb02f9bcc165d4085dbb7326190d92aa74bcf", size = 3111388, upload-time = "2026-02-13T22:20:50.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/92/545eb2ca17fc0e05456728d7e4378bfee48d66433ae3b7e71948e46826fb/xarray-2026.2.0-py3-none-any.whl", hash = "sha256:e927d7d716ea71dea78a13417970850a640447d8dd2ceeb65c5687f6373837c9", size = 1405358, upload-time = "2026-02-13T22:20:47.847Z" }, +] + +[[package]] +name = "zipp" +version = "3.23.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e3/02/0f2892c661036d50ede074e376733dca2ae7c6eb617489437771209d4180/zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166", size = 25547, upload-time = "2025-06-08T17:06:39.4Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2e/54/647ade08bf0db230bfea292f893923872fd20be6ac6f53b2b936ba839d75/zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e", size = 10276, upload-time = "2025-06-08T17:06:38.034Z" }, +]