Skip to content

Commit 90fac96

Browse files
Merge remote-tracking branch 'origin/claude/expand-variable-bounds-PkM2W' into test/variadic-max-min-operators-test
2 parents 83448bf + 56ab77b commit 90fac96

File tree

7 files changed

+210
-4
lines changed

7 files changed

+210
-4
lines changed

.github/copilot-instructions.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [AGENTS.md](../AGENTS.md) for project overview, architecture, commands, conventions, and agent guardrails.

AGENTS.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
# AGENTS.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
**GemsPy** is a Python interpreter for the GEMS (Generic Energy Modeling System) framework — a high-level modeling language for simulating energy systems under uncertainty. It allows users to define energy system models via YAML without writing solver code directly.
8+
9+
## Commands
10+
11+
**Install:**
12+
```bash
13+
pip install -r requirements.txt -r requirements-dev.txt
14+
```
15+
16+
**Test:**
17+
```bash
18+
pytest # run all tests
19+
pytest tests/path/to/test_file.py::test_name # run a single test
20+
pytest --cov gems --cov-report xml # with coverage
21+
```
22+
23+
**Lint & Format:**
24+
```bash
25+
black --config pyproject.toml src/ tests/
26+
isort --profile black --filter-files src/ tests/
27+
mypy
28+
```
29+
30+
**Running:**
31+
```bash
32+
# CLI entry point
33+
gemspy \
34+
--model-libs path/to/lib1.yml path/to/lib2.yml \
35+
--components path/to/components.yml \
36+
--timeseries path/to/timeseries/ \
37+
--duration 8760 \
38+
--scenarios 1
39+
40+
# Python API (minimal example)
41+
from gems.main.main import build_problem
42+
```
43+
44+
## Architecture
45+
46+
The pipeline flows: **YAML input → parsing → model resolution → network building → optimization problem → OR-Tools solver → results**
47+
48+
### Core Modules (`src/gems/`)
49+
50+
**`model/`** — Immutable model templates.
51+
- `Model`: defines component behavior (parameters, variables, constraints, ports)
52+
- `Library`: a collection of models, loaded from YAML
53+
- Models are never instantiated directly — they are referenced by components
54+
55+
**`expression/`** — Mathematical expression language and AST.
56+
- `ExpressionNode`: base frozen dataclass for all expression tree nodes
57+
- Node types cover: arithmetic (`+`, `-`, `*`, `/`), comparisons (`<=`, `>=`, `==`), time/scenario operators (`time_sum()`), and functions (`max()`, `min()`, `ceil()`, `floor()`)
58+
- Grammar is defined in `/grammar/` and parsed via ANTLR4 (generated files live in `expression/parsing/antlr/`)
59+
- `ExpressionVisitor` is the dominant pattern for traversing and transforming expression trees (evaluation, linearization, printing, degree analysis)
60+
- Expressions support operator overloading: `var('x') + 5 * param('p')`
61+
62+
**`study/`** — Study definition and network instantiation.
63+
- `System`: top-level structure parsed from YAML (before instantiation)
64+
- `Network`: instantiated graph of `Node`s, `Component`s, and connections
65+
- `Component`: an instance of a `Model` with concrete parameter values
66+
- `DataBase`: manages time series and scenario data
67+
68+
**`simulation/`** — Optimization problem construction and solving.
69+
- `OptimizationProblem`: main interface; translates network + database into OR-Tools constraints
70+
- `LinearExpression`: the linearized form of model constraints used by the solver
71+
- `BendersDecomposedProblem`: temporal decomposition strategy for large problems
72+
- `TimeBlock`: structure for defining temporal decomposition
73+
- `OutputValues`: result extraction and formatting
74+
75+
### Key Design Patterns
76+
77+
- **Frozen dataclasses** throughout for immutability (models, expressions, constraints)
78+
- **Visitor pattern** for all expression tree operations (`ExpressionVisitor` subclasses)
79+
- **Indexing dimensions**: parameters and variables carry time and scenario indices explicitly; expressions track these automatically
80+
- **`ValueType`** enum (`INTEGER`, `CONTINUOUS`, `BOOLEAN`) for variable typing
81+
82+
### Type Checking
83+
84+
Strict mypy is enforced (`disallow_untyped_defs`, `disallow_untyped_calls`). All new code must be fully typed. Configuration is in `mypy.ini`.
85+
86+
## Further Reading
87+
88+
- [Python Convention](docs/agents/python-convention.md) — Code style, conventions, and agent guardrails
89+
- [Testing](docs/agents/testing.md) — Testing strategy and layer overview
90+
- [docs/getting-started.md](docs/getting-started.md) — Installation and first study walkthrough
91+
- [docs/user-guide.md](docs/user-guide.md) — Full user documentation
92+
- [docs/developer-guide.md](docs/developer-guide.md) — Contributor guide
93+
- [grammar/](grammar/) — ANTLR4 grammar source (`Expr.g4`)

docs/agents/python-convention.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Python conventions
2+
3+
## Code Style & Conventions
4+
5+
- **Formatter**: Black, line-length 88. Never adjust line breaks manually—let Black decide.
6+
- **Import order**: isort with `profile = "black"`. One `import` block, no manual blank lines between
7+
groups.
8+
- **Type annotations**: All functions and methods **must** have full type annotations. mypy is run in
9+
strict mode (`disallow_untyped_defs`, `disallow_untyped_calls`).
10+
- **Dataclasses**: Prefer `@dataclass(frozen=True)` for value objects (expression nodes, model
11+
definitions). Mutability must be justified explicitly.
12+
- **Pydantic**: Use `ConfigDict(alias_generator=to_camel)` or kebab-case alias generation for YAML
13+
round-tripping; use Pydantic v2 APIs only.
14+
- **Naming**:
15+
- Classes: `PascalCase`
16+
- Functions / variables: `snake_case`
17+
- Constants / type-level aliases: `UPPER_SNAKE_CASE`
18+
- YAML keys: `kebab-case`
19+
- **Commit messages**: Follow Conventional Commits — `<type>(<scope>): <summary>`, e.g.
20+
`feat(expression): add floor operator`. Types used in this repo: `feat`, `fix`, `docs`, `refactor`,
21+
`test`, `chore`, `release`.
22+
23+
---
24+
25+
## Agent Guardrails
26+
27+
Rules for automated agents (CI bots, AI coding assistants, Dependabot, etc.):
28+
29+
1. **Never auto-merge** to `main`; all changes require at least one human review.
30+
2. **Do not edit generated files** under `src/gems/expression/parsing/antlr/`; regenerate them
31+
from `grammar/Expr.g4` instead.
32+
3. **Do not modify `pyproject.toml` version** manually; version bumps are handled via the
33+
`feat(release):` commit workflow.
34+
4. **Keep pre-commit hooks passing**: any commit that breaks `black`, `isort`, or `mypy` must not
35+
be auto-pushed.
36+
5. **Test coverage must not decrease** on `main`; PRs that drop coverage without justification
37+
should be flagged.

docs/agents/testing.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Testing Strategy
2+
3+
## Philosophy
4+
5+
Tests live alongside the module they exercise. Fixtures (YAML snippets, small networks) are kept
6+
in `tests/` sub-directories. No mocking of the solver—tests use real OR-Tools calls.
7+
8+
## Layers
9+
10+
| Layer | Location | Description |
11+
|---|---|---|
12+
| Unit | `tests/unittests/` | One file per module area; covers AST visitors, parsing, model loading, linearisation |
13+
| Integration | `tests/unittests/simulation/` | Full problem build + solve on tiny networks |
14+
| End-to-end | `tests/e2e/` | CLI-level tests reading real YAML fixtures |
15+
| Converter | `tests/input_converter/`, `tests/pypsa_converter/`, `tests/antares_historic/` | Format-specific conversion tests |

src/gems/expression/copy.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from typing import List, cast
1515

1616
from .expression import (
17+
AdditionNode,
1718
AllTimeSumNode,
1819
CeilNode,
1920
ComparisonNode,
@@ -44,6 +45,9 @@ class CopyVisitor(ExpressionVisitorOperations[ExpressionNode]):
4445
Simply copies the whole AST.
4546
"""
4647

48+
def addition(self, node: AdditionNode) -> ExpressionNode:
49+
return AdditionNode([visit(o, self) for o in node.operands])
50+
4751
def literal(self, node: LiteralNode) -> ExpressionNode:
4852
return LiteralNode(node.value)
4953

src/gems/simulation/optimization.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -680,10 +680,16 @@ def _create_variables(self) -> None:
680680
instantiated_lb_expr = _instantiate_model_expression(
681681
model_var.lower_bound, component.id, self.context
682682
)
683+
instantiated_lb_expr = self.context.expand_operators(
684+
instantiated_lb_expr
685+
)
683686
if model_var.upper_bound:
684687
instantiated_ub_expr = _instantiate_model_expression(
685688
model_var.upper_bound, component.id, self.context
686689
)
690+
instantiated_ub_expr = self.context.expand_operators(
691+
instantiated_ub_expr
692+
)
687693

688694
time_indices: Iterable[Optional[int]] = [None]
689695
if var_indexing.is_time_varying():
@@ -697,13 +703,23 @@ def _create_variables(self) -> None:
697703
lower_bound = -self.solver.infinity()
698704
upper_bound = self.solver.infinity()
699705
if instantiated_lb_expr:
700-
lower_bound = _compute_expression_value(
701-
instantiated_lb_expr, self.context, t, s
706+
lb_linear = self.context.linearize_expression(
707+
instantiated_lb_expr, t, s
702708
)
709+
if lb_linear.terms:
710+
raise ValueError(
711+
f"Lower bound of variable '{model_var.name}' must be a constant expression (no decision variables)."
712+
)
713+
lower_bound = lb_linear.constant
703714
if instantiated_ub_expr:
704-
upper_bound = _compute_expression_value(
705-
instantiated_ub_expr, self.context, t, s
715+
ub_linear = self.context.linearize_expression(
716+
instantiated_ub_expr, t, s
706717
)
718+
if ub_linear.terms:
719+
raise ValueError(
720+
f"Upper bound of variable '{model_var.name}' must be a constant expression (no decision variables)."
721+
)
722+
upper_bound = ub_linear.constant
707723

708724
solver_var_name = self._solver_variable_name(
709725
component.id, model_var.name, t, s

tests/unittests/expressions/visitor/test_copy.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
# This file is part of the Antares project.
1212

1313

14+
import time
15+
1416
from gems.expression import (
1517
AdditionNode,
1618
DivisionNode,
@@ -29,6 +31,44 @@
2931
)
3032

3133

34+
def test_copy_large_addition_is_linear() -> None:
35+
"""
36+
Copying an AdditionNode with T operands must be O(T), not O(T²).
37+
38+
Before the fix, CopyVisitor inherited the default addition() from
39+
ExpressionVisitorOperations which accumulated results with `res = res + o`.
40+
Each call to __add__ flattens the AdditionNode by copying the accumulated
41+
operand list, giving 1+2+...+(T-1) = O(T²) list copies in total.
42+
43+
The fix overrides addition() in CopyVisitor to build AdditionNode directly
44+
from a list comprehension, reducing the cost to O(T).
45+
46+
We verify linearity by checking that the time ratio between T=10_000 and
47+
T=1_000 stays below 20 (linear ≈ 10, quadratic ≈ 100).
48+
"""
49+
small_n = 1_000
50+
large_n = 10_000
51+
52+
small_node = AdditionNode([VariableNode(f"x{i}") for i in range(small_n)])
53+
large_node = AdditionNode([VariableNode(f"x{i}") for i in range(large_n)])
54+
55+
t0 = time.perf_counter()
56+
copy_expression(small_node)
57+
small_time = time.perf_counter() - t0
58+
59+
t0 = time.perf_counter()
60+
copy_expression(large_node)
61+
large_time = time.perf_counter() - t0
62+
63+
ratio = large_time / small_time
64+
assert ratio < 20, (
65+
f"copy_expression scaling looks super-linear: "
66+
f"T={small_n} took {small_time:.4f}s, "
67+
f"T={large_n} took {large_time:.4f}s, "
68+
f"ratio={ratio:.1f} (expected <20 for O(T))"
69+
)
70+
71+
3272
def test_copy_ast() -> None:
3373
ast = AllTimeSumNode(
3474
DivisionNode(

0 commit comments

Comments
 (0)