linopy
Advanced tools
| name: Claude Code Review | ||
| on: | ||
| pull_request: | ||
| types: [opened] | ||
| # Optional: Only run on specific file changes | ||
| # paths: | ||
| # - "src/**/*.ts" | ||
| # - "src/**/*.tsx" | ||
| # - "src/**/*.js" | ||
| # - "src/**/*.jsx" | ||
| jobs: | ||
| claude-review: | ||
| # Skip review for draft PRs, WIP, or explicitly skipped PRs | ||
| if: | | ||
| !contains(github.event.pull_request.title, '[skip-review]') && | ||
| !contains(github.event.pull_request.title, '[WIP]') && | ||
| !github.event.pull_request.draft | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| issues: read | ||
| id-token: write | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Validate required secrets | ||
| run: | | ||
| if [ -z "${{ secrets.ANTHROPIC_API_KEY }}" ]; then | ||
| echo "Error: Missing required secret ANTHROPIC_API_KEY" | ||
| echo "Please add the ANTHROPIC_API_KEY secret to your repository settings" | ||
| exit 1 | ||
| fi | ||
| - name: Run Claude Code Review | ||
| id: claude-review | ||
| uses: anthropics/claude-code-action@beta | ||
| with: | ||
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) | ||
| # model: "claude-opus-4-20250514" | ||
| # Direct prompt for automated review (no @claude mention needed) | ||
| direct_prompt: | | ||
| Please review this pull request for the linopy optimization library. Focus on: | ||
| - Python best practices and type safety (we use mypy for type checking) | ||
| - Proper xarray integration patterns and dimension handling | ||
| - Performance implications for large-scale optimization problems | ||
| - Mathematical correctness in solver interfaces and constraint formulations | ||
| - Memory efficiency considerations for handling large arrays | ||
| - Test coverage and edge cases | ||
| - Consistency with the existing codebase patterns, avoiding redundant code | ||
| Linopy is built on xarray and provides N-dimensional labeled arrays for variables and constraints. | ||
| Be constructive and specific in your feedback. | ||
| # Optional: Customize review based on file types | ||
| # direct_prompt: | | ||
| # Review this PR focusing on: | ||
| # - For TypeScript files: Type safety and proper interface usage | ||
| # - For API endpoints: Security, input validation, and error handling | ||
| # - For React components: Performance, accessibility, and best practices | ||
| # - For tests: Coverage, edge cases, and test quality | ||
| # Optional: Different prompts for different authors | ||
| # direct_prompt: | | ||
| # ${{ github.event.pull_request.author_association == 'FIRST_TIME_CONTRIBUTOR' && | ||
| # 'Welcome! Please review this PR from a first-time contributor. Be encouraging and provide detailed explanations for any suggestions.' || | ||
| # 'Please provide a thorough code review focusing on our coding standards and best practices.' }} | ||
| # Project-specific tools for Python development | ||
| allowed_tools: "Bash(pytest),Bash(ruff check .),Bash(ruff format .),Bash(mypy .),Bash(uv pip install -e .[dev,solvers])" | ||
| # Optional: Skip review for certain conditions | ||
| # if: | | ||
| # !contains(github.event.pull_request.title, '[skip-review]') && | ||
| # !contains(github.event.pull_request.title, '[WIP]') |
| name: Claude Code | ||
| on: | ||
| issue_comment: | ||
| types: [created] | ||
| pull_request_review_comment: | ||
| types: [created] | ||
| issues: | ||
| types: [opened, assigned] | ||
| pull_request_review: | ||
| types: [submitted] | ||
| jobs: | ||
| claude: | ||
| # This workflow triggers when @claude is mentioned in: | ||
| # - Issue comments (on issues or PRs) | ||
| # - Pull request review comments (inline code comments) | ||
| # - Pull request reviews (general review comments) | ||
| # - New issues (in title or body) | ||
| if: | | ||
| (github.event_name == 'issue_comment' && contains(github.event.comment.body, '@claude')) || | ||
| (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude')) || | ||
| (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude')) || | ||
| (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude'))) | ||
| runs-on: ubuntu-latest | ||
| timeout-minutes: 30 | ||
| permissions: | ||
| contents: read | ||
| pull-requests: read | ||
| issues: read | ||
| id-token: write | ||
| steps: | ||
| - name: Checkout repository | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 1 | ||
| - name: Validate required secrets | ||
| run: | | ||
| if [ -z "${{ secrets.ANTHROPIC_API_KEY }}" ]; then | ||
| echo "Error: Missing required secret ANTHROPIC_API_KEY" | ||
| echo "Please add the ANTHROPIC_API_KEY secret to your repository settings" | ||
| exit 1 | ||
| fi | ||
| - name: Run Claude Code | ||
| id: claude | ||
| uses: anthropics/claude-code-action@beta | ||
| with: | ||
| anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} | ||
| # Optional: Specify model (defaults to Claude Sonnet 4, uncomment for Claude Opus 4) | ||
| # model: "claude-opus-4-20250514" | ||
| # Optional: Customize the trigger phrase (default: @claude) | ||
| # trigger_phrase: "/claude" | ||
| # Optional: Trigger when specific user is assigned to an issue | ||
| # assignee_trigger: "claude-bot" | ||
| # Project-specific tools for Python development | ||
| allowed_tools: "Bash(pytest),Bash(ruff check .),Bash(ruff format .),Bash(mypy .),Bash(uv pip install -e .[dev,solvers])" | ||
| # Custom instructions for linopy project | ||
| custom_instructions: | | ||
| You are working on linopy, a optimization library built on xarray. | ||
| Follow these guidelines: | ||
| - Use type hints and ensure mypy compliance | ||
| - Follow xarray patterns for dimension handling | ||
| - Write tests for new features or bug fixes | ||
| - Use ruff for linting and formatting (run ruff check --fix .) | ||
| - Place tests in the test directory with test_*.py naming | ||
| - Maintain consistency with existing codebase patterns, avoiding redundant code | ||
| - Consider memory efficiency for large-scale optimization problems | ||
| # Optional: Custom environment variables for Claude | ||
| # claude_env: | | ||
| # NODE_ENV: test |
+134
| # CLAUDE.md | ||
| This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. | ||
| ## Common Development Commands | ||
| ### Running Tests | ||
| ```bash | ||
| # Run all tests | ||
| pytest | ||
| # Run tests with coverage | ||
| pytest --cov=./ --cov-report=xml linopy --doctest-modules test | ||
| # Run a specific test file | ||
| pytest test/test_model.py | ||
| # Run a specific test function | ||
| pytest test/test_model.py::test_model_creation | ||
| ``` | ||
| ### Linting and Type Checking | ||
| ```bash | ||
| # Run linter (ruff) | ||
| ruff check . | ||
| ruff check --fix . # Auto-fix issues | ||
| # Run formatter | ||
| ruff format . | ||
| # Run type checker | ||
| mypy . | ||
| # Run all pre-commit hooks | ||
| pre-commit run --all-files | ||
| ``` | ||
| ### Development Setup | ||
| ```bash | ||
| # Create virtual environment and install development dependencies | ||
| python -m venv venv | ||
| source venv/bin/activate # On Windows: venv\Scripts\activate | ||
| pip install uv | ||
| uv pip install -e .[dev,solvers] | ||
| ``` | ||
| ## High-Level Architecture | ||
| linopy is a linear optimization library built on top of xarray, providing N-dimensional labeled arrays for variables and constraints. The architecture follows these key principles: | ||
| ### Core Components | ||
| 1. **Model** (`model.py`): Central container for optimization problems | ||
| - Manages variables, constraints, and objective | ||
| - Handles solver integration through abstract interfaces | ||
| - Supports chunked operations for memory efficiency | ||
| - Provides matrix representations for solver APIs | ||
| 2. **Variables** (`variables.py`): Multi-dimensional decision variables | ||
| - Built on xarray.Dataset with labels, lower, and upper bounds | ||
| - Arithmetic operations automatically create LinearExpressions | ||
| - Support for continuous and binary variables | ||
| - Container class (Variables) manages collections with dict-like access | ||
| 3. **Constraints** (`constraints.py`): Linear inequality/equality constraints | ||
| - Store coefficients, variable references, signs, and RHS values | ||
| - Support ≤, ≥, and = constraints | ||
| - Container class (Constraints) provides organized access | ||
| 4. **Expressions** (`expressions.py`): Linear combinations of variables | ||
| - LinearExpression: coeffs × vars + const | ||
| - QuadraticExpression: for non-linear optimization | ||
| - Support full arithmetic operations with automatic broadcasting | ||
| - Special `_term` dimension for handling multiple terms | ||
| 5. **Solvers** (`solvers.py`): Abstract interface with multiple implementations | ||
| - File-based solvers: Write LP/MPS files, call solver, parse results | ||
| - Direct API solvers: Use Python bindings (e.g., gurobipy) | ||
| - Automatic solver detection based on installed packages | ||
| ### Data Flow Pattern | ||
| 1. User creates Model and adds Variables with coordinates (dimensions) | ||
| 2. Variables combined into LinearExpressions through arithmetic | ||
| 3. Expressions used to create Constraints and Objective | ||
| 4. Model.solve() converts to solver format and retrieves solution | ||
| 5. Solution stored back in xarray format with original dimensions | ||
| ### Key Design Patterns | ||
| - **xarray Integration**: All data structures use xarray for dimension handling | ||
| - **Lazy Evaluation**: Expressions built symbolically before solving | ||
| - **Broadcasting**: Operations automatically align dimensions | ||
| - **Solver Abstraction**: Clean separation between model and solver specifics | ||
| - **Memory Efficiency**: Support for dask arrays and chunked operations | ||
| When modifying the codebase, maintain consistency with these patterns and ensure new features integrate naturally with the xarray-based architecture. | ||
| ## Working with the Github Repository | ||
| * The main branch is `master`. | ||
| * Always create a feature branch for new features or bug fixes. | ||
| * Use the github cli (gh) to interact with the Github repository. | ||
| ### GitHub Claude Code Integration | ||
| This repository includes Claude Code GitHub Actions for automated assistance: | ||
| 1. **Automated PR Reviews** (`claude-code-review.yml`): | ||
| - Automatically reviews PRs only when first created (opened) | ||
| - Subsequent reviews require manual `@claude` mention | ||
| - Focuses on Python best practices, xarray patterns, and optimization correctness | ||
| - Can run tests and linting as part of the review | ||
| - **Skip initial review by**: Adding `[skip-review]` or `[WIP]` to PR title, or using draft PRs | ||
| 2. **Manual Claude Assistance** (`claude.yml`): | ||
| - Trigger by mentioning `@claude` in any: | ||
| - Issue comments | ||
| - Pull request comments | ||
| - Pull request reviews | ||
| - New issue body or title | ||
| - Claude can help with bug fixes, feature implementation, code explanations, etc. | ||
| **Note**: Both workflows require the `ANTHROPIC_API_KEY` secret to be configured in the repository settings. | ||
| ## Development Guidelines | ||
| 1. Always write tests for new features or bug fixes. | ||
| 2. Always run the tests after making changes and ensure they pass. | ||
| 3. Always use ruff for linting and formatting, run `ruff check --fix .` to auto-fix issues. | ||
| 4. Use type hints and mypy for type checking. | ||
| 5. Always write tests into the `test` directory, following the naming convention `test_*.py`. | ||
| 6. Always write temporary and non git-tracked code in the `dev-scripts` directory. |
| #!/usr/bin/env python3 | ||
| """ | ||
| Test infeasibility detection for different solvers. | ||
| """ | ||
| import pandas as pd | ||
| import pytest | ||
| from linopy import Model, available_solvers | ||
| class TestInfeasibility: | ||
| """Test class for infeasibility detection functionality.""" | ||
| @pytest.fixture | ||
| def simple_infeasible_model(self) -> Model: | ||
| """Create a simple infeasible model.""" | ||
| m = Model() | ||
| time = pd.RangeIndex(10, name="time") | ||
| x = m.add_variables(lower=0, coords=[time], name="x") | ||
| y = m.add_variables(lower=0, coords=[time], name="y") | ||
| # Create infeasible constraints | ||
| m.add_constraints(x <= 5, name="con_x_upper") | ||
| m.add_constraints(y <= 5, name="con_y_upper") | ||
| m.add_constraints(x + y >= 12, name="con_sum_lower") | ||
| # Add objective to avoid multi-objective issue with xpress | ||
| m.add_objective(x.sum() + y.sum()) | ||
| return m | ||
| @pytest.fixture | ||
| def complex_infeasible_model(self) -> Model: | ||
| """Create a more complex infeasible model.""" | ||
| m = Model() | ||
| # Create variables | ||
| x = m.add_variables(lower=0, upper=10, name="x") | ||
| y = m.add_variables(lower=0, upper=10, name="y") | ||
| z = m.add_variables(lower=0, upper=10, name="z") | ||
| # Add conflicting constraints | ||
| m.add_constraints(x + y >= 15, name="con1") | ||
| m.add_constraints(x <= 5, name="con2") | ||
| m.add_constraints(y <= 5, name="con3") | ||
| m.add_constraints(z >= x + y, name="con4") | ||
| m.add_constraints(z <= 8, name="con5") | ||
| # Add objective | ||
| m.add_objective(x + y + z) | ||
| return m | ||
| @pytest.fixture | ||
| def multi_dimensional_infeasible_model(self) -> Model: | ||
| """Create a multi-dimensional infeasible model.""" | ||
| m = Model() | ||
| # Create multi-dimensional variables | ||
| i = pd.RangeIndex(5, name="i") | ||
| j = pd.RangeIndex(3, name="j") | ||
| x = m.add_variables(lower=0, upper=1, coords=[i, j], name="x") | ||
| # Add constraints that make it infeasible | ||
| m.add_constraints(x.sum("j") >= 2.5, name="row_sum") # Each row sum >= 2.5 | ||
| m.add_constraints(x.sum("i") <= 1, name="col_sum") # Each column sum <= 1 | ||
| # Add objective | ||
| m.add_objective(x.sum()) | ||
| return m | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_simple_infeasibility_detection( | ||
| self, simple_infeasible_model: Model, solver: str | ||
| ) -> None: | ||
| """Test basic infeasibility detection.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = simple_infeasible_model | ||
| status, condition = m.solve(solver_name=solver) | ||
| assert status == "warning" | ||
| assert "infeasible" in condition | ||
| # Test compute_infeasibilities | ||
| labels = m.compute_infeasibilities() | ||
| assert isinstance(labels, list) | ||
| assert len(labels) > 0 # Should find at least one infeasible constraint | ||
| # Test print_infeasibilities (just check it doesn't raise an error) | ||
| m.print_infeasibilities() | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_complex_infeasibility_detection( | ||
| self, complex_infeasible_model: Model, solver: str | ||
| ) -> None: | ||
| """Test infeasibility detection on more complex model.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = complex_infeasible_model | ||
| status, condition = m.solve(solver_name=solver) | ||
| assert status == "warning" | ||
| assert "infeasible" in condition | ||
| labels = m.compute_infeasibilities() | ||
| assert isinstance(labels, list) | ||
| assert len(labels) > 0 | ||
| # The infeasible set should include constraints that conflict | ||
| # Different solvers might find different minimal IIS | ||
| # We expect at least 2 constraints to be involved | ||
| assert len(labels) >= 2 | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_multi_dimensional_infeasibility( | ||
| self, multi_dimensional_infeasible_model: Model, solver: str | ||
| ) -> None: | ||
| """Test infeasibility detection on multi-dimensional model.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = multi_dimensional_infeasible_model | ||
| status, condition = m.solve(solver_name=solver) | ||
| assert status == "warning" | ||
| assert "infeasible" in condition | ||
| labels = m.compute_infeasibilities() | ||
| assert isinstance(labels, list) | ||
| assert len(labels) > 0 | ||
| def test_unsolved_model_error(self) -> None: | ||
| """Test error when model hasn't been solved yet.""" | ||
| m = Model() | ||
| x = m.add_variables(name="x") | ||
| m.add_constraints(x >= 0) | ||
| m.add_objective(1 * x) # Convert to LinearExpression | ||
| # Don't solve the model - should raise NotImplementedError for unsolved models | ||
| with pytest.raises( | ||
| NotImplementedError, match="Computing infeasibilities is not supported" | ||
| ): | ||
| m.compute_infeasibilities() | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_no_solver_model_error(self, solver: str) -> None: | ||
| """Test error when solver model is not available after solving.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = Model() | ||
| x = m.add_variables(name="x") | ||
| m.add_constraints(x >= 0) | ||
| m.add_objective(1 * x) # Convert to LinearExpression | ||
| # Solve the model first | ||
| m.solve(solver_name=solver) | ||
| # Manually remove the solver_model to simulate cleanup | ||
| m.solver_model = None | ||
| m.solver_name = solver # But keep the solver name | ||
| # Should raise ValueError since we know it was solved with supported solver | ||
| with pytest.raises(ValueError, match="No solver model available"): | ||
| m.compute_infeasibilities() | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_feasible_model_iis(self, solver: str) -> None: | ||
| """Test IIS computation on a feasible model.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = Model() | ||
| x = m.add_variables(lower=0, name="x") | ||
| y = m.add_variables(lower=0, name="y") | ||
| m.add_constraints(x + y >= 1) | ||
| m.add_constraints(x <= 10) | ||
| m.add_constraints(y <= 10) | ||
| m.add_objective(x + y) | ||
| status, condition = m.solve(solver_name=solver) | ||
| assert status == "ok" | ||
| assert condition == "optimal" | ||
| # Calling compute_infeasibilities on a feasible model | ||
| # Different solvers might handle this differently | ||
| # Gurobi might raise an error, Xpress might return empty list | ||
| try: | ||
| labels = m.compute_infeasibilities() | ||
| # If it doesn't raise an error, it should return empty list | ||
| assert labels == [] | ||
| except Exception: | ||
| # Some solvers might raise an error when computing IIS on feasible model | ||
| pass | ||
| def test_unsupported_solver_error(self) -> None: | ||
| """Test error for unsupported solvers.""" | ||
| m = Model() | ||
| x = m.add_variables(name="x") | ||
| m.add_constraints(x >= 0) | ||
| m.add_constraints(x <= -1) # Make it infeasible | ||
| # Use a solver that doesn't support IIS | ||
| if "cbc" in available_solvers: | ||
| status, condition = m.solve(solver_name="cbc") | ||
| assert "infeasible" in condition | ||
| with pytest.raises(NotImplementedError): | ||
| m.compute_infeasibilities() | ||
| @pytest.mark.parametrize("solver", ["gurobi", "xpress"]) | ||
| def test_deprecated_method( | ||
| self, simple_infeasible_model: Model, solver: str | ||
| ) -> None: | ||
| """Test that deprecated method still works.""" | ||
| if solver not in available_solvers: | ||
| pytest.skip(f"{solver} not available") | ||
| m = simple_infeasible_model | ||
| status, condition = m.solve(solver_name=solver) | ||
| assert status == "warning" | ||
| assert "infeasible" in condition | ||
| # Test deprecated method | ||
| with pytest.warns(DeprecationWarning): | ||
| subset = m.compute_set_of_infeasible_constraints() | ||
| # Check that it returns a Dataset | ||
| from xarray import Dataset | ||
| assert isinstance(subset, Dataset) | ||
| # Check that it contains constraint labels | ||
| assert len(subset) > 0 |
| import xarray as xr | ||
| import linopy | ||
| def test_operations_with_data_arrays_are_typed_correctly() -> None: | ||
| m = linopy.Model() | ||
| a: xr.DataArray = xr.DataArray([1, 2, 3]) | ||
| v: linopy.Variable = m.add_variables(lower=0.0, name="v") | ||
| e: linopy.LinearExpression = v * 1.0 | ||
| q = v * v | ||
| _ = a * v | ||
| _ = v * a | ||
| _ = v + a | ||
| _ = a * e | ||
| _ = e * a | ||
| _ = e + a | ||
| _ = a * q | ||
| _ = q * a | ||
| _ = q + a |
+7
-0
| .coverage | ||
| .eggs | ||
| .DS_Store | ||
| .mypy_cache | ||
| linopy/__pycache__ | ||
@@ -39,1 +40,7 @@ test/__pycache__ | ||
| benchmark/scripts/leftovers/ | ||
| # IDE | ||
| .idea/ | ||
| # Claude | ||
| .claude/settings.local.json |
@@ -12,3 +12,3 @@ ci: | ||
| - repo: https://github.com/astral-sh/ruff-pre-commit | ||
| rev: v0.11.8 | ||
| rev: v0.12.2 | ||
| hooks: | ||
@@ -25,3 +25,3 @@ - id: ruff | ||
| - repo: https://github.com/keewis/blackdoc | ||
| rev: v0.3.9 | ||
| rev: v0.4.1 | ||
| hooks: | ||
@@ -28,0 +28,0 @@ - id: blackdoc |
@@ -7,2 +7,12 @@ Release Notes | ||
| Version 0.5.6 | ||
| -------------- | ||
| * Improved variable/expression arithmetic methods so that they correctly handle types | ||
| * Gurobi: Pass dictionary as env argument `env={...}` through to gurobi env creation | ||
| **Breaking Changes** | ||
| * With this release, the package support for Python 3.9 was dropped and support for Python 3.10 was officially added. | ||
| Version 0.5.5 | ||
@@ -13,3 +23,2 @@ -------------- | ||
| Version 0.5.4 | ||
@@ -16,0 +25,0 @@ -------------- |
| Metadata-Version: 2.4 | ||
| Name: linopy | ||
| Version: 0.5.5 | ||
| Version: 0.5.6 | ||
| Summary: Linear optimization with N-D labeled arrays in Python | ||
@@ -8,6 +8,6 @@ Author-email: Fabian Hofmann <fabianmarikhofmann@gmail.com> | ||
| Project-URL: Source, https://github.com/PyPSA/linopy | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Development Status :: 3 - Alpha | ||
@@ -20,3 +20,3 @@ Classifier: Environment :: Console | ||
| Classifier: Operating System :: OS Independent | ||
| Requires-Python: >=3.9 | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/markdown | ||
@@ -135,5 +135,5 @@ License-File: LICENSE.txt | ||
| >>> days = pd.Index(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], name='day') | ||
| >>> apples = m.add_variables(lower=0, name='apples', coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name='bananas', coords=[days]) | ||
| >>> days = pd.Index(["Mon", "Tue", "Wed", "Thu", "Fri"], name="day") | ||
| >>> apples = m.add_variables(lower=0, name="apples", coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name="bananas", coords=[days]) | ||
| >>> apples | ||
@@ -154,3 +154,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name='daily_vitamins') | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name="daily_vitamins") | ||
| ``` | ||
@@ -170,3 +170,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name='weekly_vitamins') | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name="weekly_vitamins") | ||
| ``` | ||
@@ -173,0 +173,0 @@ ``` |
@@ -5,2 +5,3 @@ .git-blame-ignore-revs | ||
| .readthedocs.yaml | ||
| CLAUDE.md | ||
| CONTRIBUTING.md | ||
@@ -16,2 +17,4 @@ LICENSE.txt | ||
| .github/ISSUE_TEMPLATE/feature_request.md | ||
| .github/workflows/claude-code-review.yml | ||
| .github/workflows/claude.yml | ||
| .github/workflows/codeql.yml | ||
@@ -146,2 +149,3 @@ .github/workflows/release.yml | ||
| test/test_inconsistency_checks.py | ||
| test/test_infeasibility.py | ||
| test/test_io.py | ||
@@ -159,4 +163,5 @@ test/test_linear_expression.py | ||
| test/test_solvers.py | ||
| test/test_typing.py | ||
| test/test_variable.py | ||
| test/test_variable_assignment.py | ||
| test/test_variables.py |
+24
-19
@@ -12,6 +12,6 @@ #!/usr/bin/env python3 | ||
| import os | ||
| from collections.abc import Generator, Hashable, Iterable, Sequence | ||
| from collections.abc import Callable, Generator, Hashable, Iterable, Sequence | ||
| from functools import partial, reduce, wraps | ||
| from pathlib import Path | ||
| from typing import TYPE_CHECKING, Any, Callable, Generic, TypeVar, overload | ||
| from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload | ||
| from warnings import warn | ||
@@ -41,3 +41,3 @@ | ||
| from linopy.constraints import Constraint | ||
| from linopy.expressions import LinearExpression | ||
| from linopy.expressions import LinearExpression, QuadraticExpression | ||
| from linopy.variables import Variable | ||
@@ -122,3 +122,3 @@ | ||
| return None | ||
| if isinstance(lst, (Sequence, Iterable)): | ||
| if isinstance(lst, Sequence | Iterable): | ||
| lst = list(lst) | ||
@@ -216,3 +216,3 @@ else: | ||
| ndim = max(arr.ndim, 0 if coords is None else len(coords)) | ||
| if isinstance(dims, (Iterable, Sequence)): | ||
| if isinstance(dims, Iterable | Sequence): | ||
| dims = list(dims) | ||
@@ -257,7 +257,7 @@ elif dims is not None: | ||
| """ | ||
| if isinstance(arr, (pd.Series, pd.DataFrame)): | ||
| if isinstance(arr, pd.Series | pd.DataFrame): | ||
| arr = pandas_to_dataarray(arr, coords=coords, dims=dims, **kwargs) | ||
| elif isinstance(arr, np.ndarray): | ||
| arr = numpy_to_dataarray(arr, coords=coords, dims=dims, **kwargs) | ||
| elif isinstance(arr, (np.number, int, float, str, bool, list)): | ||
| elif isinstance(arr, np.number | int | float | str | bool | list): | ||
| arr = DataArray(arr, coords=coords, dims=dims, **kwargs) | ||
@@ -501,3 +501,3 @@ | ||
| ds = ds.copy() | ||
| if not isinstance(ds, (Dataset, DataArray)): | ||
| if not isinstance(ds, Dataset | DataArray): | ||
| raise TypeError(f"Expected xarray.DataArray or xarray.Dataset, got {type(ds)}.") | ||
@@ -816,3 +816,3 @@ | ||
| for value in values: | ||
| if isinstance(value, (list, tuple)): | ||
| if isinstance(value, list | tuple): | ||
| formatted.append(f"({', '.join(str(x) for x in value)})") | ||
@@ -956,7 +956,5 @@ else: | ||
| arg, | ||
| ( | ||
| variables.Variable, | ||
| variables.ScalarVariable, | ||
| expressions.LinearExpression, | ||
| ), | ||
| variables.Variable | ||
| | variables.ScalarVariable | ||
| | expressions.LinearExpression, | ||
| ): | ||
@@ -1005,3 +1003,3 @@ raise TypeError(f"Assigned rhs must be a constant, got {type(arg)}).") | ||
| def align( | ||
| *objects: LinearExpression | Variable | T_Alignable, | ||
| *objects: LinearExpression | QuadraticExpression | Variable | T_Alignable, | ||
| join: JoinOptions = "inner", | ||
@@ -1012,3 +1010,3 @@ copy: bool = True, | ||
| fill_value: Any = dtypes.NA, | ||
| ) -> tuple[LinearExpression | Variable | T_Alignable, ...]: | ||
| ) -> tuple[LinearExpression | QuadraticExpression | Variable | T_Alignable, ...]: | ||
| """ | ||
@@ -1068,3 +1066,3 @@ Given any number of Variables, Expressions, Dataset and/or DataArray objects, | ||
| """ | ||
| from linopy.expressions import LinearExpression | ||
| from linopy.expressions import LinearExpression, QuadraticExpression | ||
| from linopy.variables import Variable | ||
@@ -1075,3 +1073,3 @@ | ||
| for obj in objects: | ||
| if isinstance(obj, LinearExpression): | ||
| if isinstance(obj, LinearExpression | QuadraticExpression): | ||
| finisher.append(partial(obj.__class__, model=obj.model)) | ||
@@ -1105,3 +1103,10 @@ das.append(obj.data) | ||
| LocT = TypeVar("LocT", "Dataset", "Variable", "LinearExpression", "Constraint") | ||
| LocT = TypeVar( | ||
| "LocT", | ||
| "Dataset", | ||
| "Variable", | ||
| "LinearExpression", | ||
| "QuadraticExpression", | ||
| "Constraint", | ||
| ) | ||
@@ -1108,0 +1113,0 @@ |
@@ -109,2 +109,3 @@ #!/usr/bin/env python3 | ||
| suboptimal = "suboptimal" | ||
| imprecise = "imprecise" | ||
@@ -145,2 +146,3 @@ # WARNING | ||
| TerminationCondition.suboptimal, | ||
| TerminationCondition.imprecise, | ||
| ], | ||
@@ -174,3 +176,3 @@ SolverStatus.warning: [ | ||
| termination_condition: TerminationCondition | ||
| legacy_status: Union[tuple[str, str], str] = "" | ||
| legacy_status: tuple[str, str] | str = "" | ||
@@ -220,3 +222,3 @@ @classmethod | ||
| status: Status | ||
| solution: Union[Solution, None] = None | ||
| solution: Solution | None = None | ||
| solver_model: Any = None | ||
@@ -223,0 +225,0 @@ |
@@ -943,3 +943,3 @@ """ | ||
| """ | ||
| if not isinstance(label, (float, int)) or label < 0: | ||
| if not isinstance(label, float | int) or label < 0: | ||
| raise ValueError("Label must be a positive number.") | ||
@@ -1088,3 +1088,3 @@ for name, ds in self.items(): | ||
| """ | ||
| if not isinstance(rhs, (int, float, np.floating, np.integer)): | ||
| if not isinstance(rhs, int | float | np.floating | np.integer): | ||
| raise TypeError(f"Assigned rhs must be a constant, got {type(rhs)}).") | ||
@@ -1091,0 +1091,0 @@ self._lhs = lhs |
+2
-2
@@ -11,7 +11,7 @@ #!/usr/bin/env python3 | ||
| import time | ||
| from collections.abc import Iterable | ||
| from collections.abc import Callable, Iterable | ||
| from io import BufferedWriter, TextIOWrapper | ||
| from pathlib import Path | ||
| from tempfile import TemporaryDirectory | ||
| from typing import TYPE_CHECKING, Any, Callable | ||
| from typing import TYPE_CHECKING, Any | ||
@@ -18,0 +18,0 @@ import numpy as np |
@@ -35,3 +35,3 @@ #!/usr/bin/env python3 | ||
| max_value = indices.max() | ||
| if not isinstance(max_value, (np.integer, int)): | ||
| if not isinstance(max_value, np.integer | int): | ||
| raise ValueError("Indices must be integers.") | ||
@@ -38,0 +38,0 @@ shape = max_value + 1 |
+160
-23
@@ -15,3 +15,3 @@ """ | ||
| from tempfile import NamedTemporaryFile, gettempdir | ||
| from typing import Any | ||
| from typing import Any, overload | ||
@@ -321,3 +321,3 @@ import numpy as np | ||
| def solver_dir(self, value: str | Path) -> None: | ||
| if not isinstance(value, (str, Path)): | ||
| if not isinstance(value, str | Path): | ||
| raise TypeError("'solver_dir' must path-like.") | ||
@@ -619,3 +619,3 @@ self._solver_dir = Path(value) | ||
| data = lhs.to_constraint(sign, rhs).data | ||
| elif isinstance(lhs, (list, tuple)): | ||
| elif isinstance(lhs, list | tuple): | ||
| if sign is None or rhs is None: | ||
@@ -639,3 +639,3 @@ raise ValueError(msg_sign_rhs_none) | ||
| data = lhs.data | ||
| elif isinstance(lhs, (Variable, ScalarVariable, ScalarLinearExpression)): | ||
| elif isinstance(lhs, Variable | ScalarVariable | ScalarLinearExpression): | ||
| if sign is None or rhs is None: | ||
@@ -717,3 +717,3 @@ raise ValueError(msg_sign_rhs_not_none) | ||
| ) | ||
| self.objective.expression = expr # type: ignore[assignment] | ||
| self.objective.expression = expr | ||
| self.objective.sense = sense | ||
@@ -889,4 +889,19 @@ | ||
| @overload | ||
| def linexpr( | ||
| self, *args: tuple[ConstantLike, str | Variable | ScalarVariable] | Callable | ||
| self, *args: Sequence[Sequence | pd.Index | DataArray] | Mapping | ||
| ) -> LinearExpression: ... | ||
| @overload | ||
| def linexpr( | ||
| self, *args: tuple[ConstantLike, str | Variable | ScalarVariable] | ConstantLike | ||
| ) -> LinearExpression: ... | ||
| def linexpr( | ||
| self, | ||
| *args: tuple[ConstantLike, str | Variable | ScalarVariable] | ||
| | ConstantLike | ||
| | Callable | ||
| | Sequence[Sequence | pd.Index | DataArray] | ||
| | Mapping, | ||
| ) -> LinearExpression: | ||
@@ -898,5 +913,6 @@ """ | ||
| ---------- | ||
| args : tuples of (coefficients, variables) or tuples of | ||
| coordinates and a function | ||
| If args is a collection of coefficients-variables-tuples, the resulting | ||
| args : A mixture of tuples of (coefficients, variables) and constants | ||
| or a function and tuples of coordinates | ||
| If args is a collection of coefficients-variables-tuples and constants, the resulting | ||
| linear expression is built with the function LinearExpression.from_tuples. | ||
@@ -910,2 +926,4 @@ * coefficients : int/float/array_like | ||
| by name. | ||
| * constant: int/float/array_like | ||
| The constant value to add to the expression | ||
@@ -921,3 +939,3 @@ If args is a collection of coordinates with an appended function at the | ||
| the variables. The function has to return a | ||
| `ScalarLinearExpression`. Therefore use the direct getter when | ||
| `ScalarLinearExpression`. Therefore, use the direct getter when | ||
| indexing variables. | ||
@@ -969,5 +987,10 @@ * coords : coordinate-like | ||
| raise TypeError(f"Not supported type {args}.") | ||
| tuples = [ # type: ignore | ||
| (c, self.variables[v]) if isinstance(v, str) else (c, v) for (c, v) in args | ||
| ] | ||
| tuples: list[tuple[ConstantLike, VariableLike] | ConstantLike] = [] | ||
| for arg in args: | ||
| if isinstance(arg, tuple): | ||
| c, v = arg | ||
| tuples.append((c, self.variables[v]) if isinstance(v, str) else (c, v)) | ||
| else: | ||
| tuples.append(arg) | ||
| return LinearExpression.from_tuples(*tuples, model=self) | ||
@@ -1273,4 +1296,5 @@ | ||
| This function requires that the model was solved with `gurobi` and the | ||
| termination condition was infeasible. | ||
| This function requires that the model was solved with `gurobi` or `xpress` | ||
| and the termination condition was infeasible. The solver must have detected | ||
| the infeasibility during the solve process. | ||
@@ -1282,12 +1306,52 @@ Returns | ||
| """ | ||
| if "gurobi" not in available_solvers: | ||
| raise ImportError("Gurobi is required for this method.") | ||
| solver_model = getattr(self, "solver_model", None) | ||
| import gurobipy | ||
| # Check for Gurobi | ||
| if "gurobi" in available_solvers: | ||
| try: | ||
| import gurobipy | ||
| solver_model = getattr(self, "solver_model") | ||
| if solver_model is not None and isinstance( | ||
| solver_model, gurobipy.Model | ||
| ): | ||
| return self._compute_infeasibilities_gurobi(solver_model) | ||
| except ImportError: | ||
| pass | ||
| if not isinstance(solver_model, gurobipy.Model): | ||
| raise NotImplementedError("Solver model must be a Gurobi Model.") | ||
| # Check for Xpress | ||
| if "xpress" in available_solvers: | ||
| try: | ||
| import xpress | ||
| if solver_model is not None and isinstance( | ||
| solver_model, xpress.problem | ||
| ): | ||
| return self._compute_infeasibilities_xpress(solver_model) | ||
| except ImportError: | ||
| pass | ||
| # If we get here, either the solver doesn't support IIS or no solver model is available | ||
| if solver_model is None: | ||
| # Check if this is a supported solver without a stored model | ||
| solver_name = getattr(self, "solver_name", "unknown") | ||
| if solver_name in ["gurobi", "xpress"]: | ||
| raise ValueError( | ||
| "No solver model available. The model must be solved first with " | ||
| "'gurobi' or 'xpress' solver and the result must be infeasible." | ||
| ) | ||
| else: | ||
| # This is an unsupported solver | ||
| raise NotImplementedError( | ||
| f"Computing infeasibilities is not supported for '{solver_name}' solver. " | ||
| "Only Gurobi and Xpress solvers support IIS computation." | ||
| ) | ||
| else: | ||
| # We have a solver model but it's not a supported type | ||
| raise NotImplementedError( | ||
| "Computing infeasibilities is only supported for Gurobi and Xpress solvers. " | ||
| f"Current solver model type: {type(solver_model).__name__}" | ||
| ) | ||
| def _compute_infeasibilities_gurobi(self, solver_model: Any) -> list[int]: | ||
| """Compute infeasibilities for Gurobi solver.""" | ||
| solver_model.computeIIS() | ||
@@ -1307,4 +1371,77 @@ f = NamedTemporaryFile(suffix=".ilp", prefix="linopy-iis-", delete=False) | ||
| labels.append(int(match.group(1))) | ||
| f.close() | ||
| return labels | ||
| def _compute_infeasibilities_xpress(self, solver_model: Any) -> list[int]: | ||
| """Compute infeasibilities for Xpress solver.""" | ||
| # Compute all IIS | ||
| solver_model.iisall() | ||
| # Get the number of IIS found | ||
| num_iis = solver_model.attributes.numiis | ||
| if num_iis == 0: | ||
| return [] | ||
| labels = set() | ||
| # Create constraint mapping for efficient lookups | ||
| constraint_to_index = { | ||
| constraint: idx | ||
| for idx, constraint in enumerate(solver_model.getConstraint()) | ||
| } | ||
| # Retrieve each IIS | ||
| for iis_num in range(1, num_iis + 1): | ||
| iis_constraints = self._extract_iis_constraints(solver_model, iis_num) | ||
| # Convert constraint objects to indices | ||
| for constraint_obj in iis_constraints: | ||
| if constraint_obj in constraint_to_index: | ||
| labels.add(constraint_to_index[constraint_obj]) | ||
| # Note: Silently skip constraints not found in mapping | ||
| # This can happen if the model structure changed after solving | ||
| return sorted(list(labels)) | ||
| def _extract_iis_constraints(self, solver_model: Any, iis_num: int) -> list[Any]: | ||
| """ | ||
| Extract constraint objects from a specific IIS. | ||
| Parameters | ||
| ---------- | ||
| solver_model : xpress.problem | ||
| The Xpress solver model | ||
| iis_num : int | ||
| IIS number (1-indexed) | ||
| Returns | ||
| ------- | ||
| list[Any] | ||
| List of xpress.constraint objects in the IIS | ||
| """ | ||
| # Prepare lists to receive IIS data | ||
| miisrow: list[Any] = [] # xpress.constraint objects in the IIS | ||
| miiscol: list[Any] = [] # xpress.variable objects in the IIS | ||
| constrainttype: list[str] = [] # Constraint types ('L', 'G', 'E') | ||
| colbndtype: list[str] = [] # Column bound types | ||
| duals: list[float] = [] # Dual values | ||
| rdcs: list[float] = [] # Reduced costs | ||
| isolationrows: list[str] = [] # Row isolation info | ||
| isolationcols: list[str] = [] # Column isolation info | ||
| # Get IIS data from Xpress | ||
| solver_model.getiisdata( | ||
| iis_num, | ||
| miisrow, | ||
| miiscol, | ||
| constrainttype, | ||
| colbndtype, | ||
| duals, | ||
| rdcs, | ||
| isolationrows, | ||
| isolationcols, | ||
| ) | ||
| return miisrow | ||
| def print_infeasibilities(self, display_max_terms: int | None = None) -> None: | ||
@@ -1314,3 +1451,3 @@ """ | ||
| This function requires that the model was solved using `gurobi` | ||
| This function requires that the model was solved using `gurobi` or `xpress` | ||
| and the termination condition was infeasible. | ||
@@ -1340,3 +1477,3 @@ | ||
| This function requires that the model was solved with `gurobi` and the | ||
| This function requires that the model was solved with `gurobi` or `xpress` and the | ||
| termination condition was infeasible. | ||
@@ -1343,0 +1480,0 @@ |
| from __future__ import annotations | ||
| from collections.abc import Callable | ||
| from functools import partialmethod, update_wrapper | ||
| from typing import Any, Callable | ||
| from types import NotImplementedType | ||
| from typing import Any | ||
@@ -9,3 +11,2 @@ from xarray import DataArray | ||
| from linopy import expressions, variables | ||
| from linopy.types import NotImplementedType | ||
@@ -30,4 +31,9 @@ | ||
| ) -> DataArray | NotImplementedType: | ||
| if isinstance(other, (variables.Variable, expressions.LinearExpression)): | ||
| if isinstance( | ||
| other, | ||
| variables.Variable | ||
| | expressions.LinearExpression | ||
| | expressions.QuadraticExpression, | ||
| ): | ||
| return NotImplemented | ||
| return unpatched_method(da, other) |
@@ -11,4 +11,4 @@ #!/usr/bin/env python3 | ||
| import functools | ||
| from collections.abc import Sequence | ||
| from typing import TYPE_CHECKING, Any, Callable | ||
| from collections.abc import Callable, Sequence | ||
| from typing import TYPE_CHECKING, Any | ||
@@ -179,7 +179,7 @@ import numpy as np | ||
| """ | ||
| if isinstance(expr, (list, tuple)): | ||
| if isinstance(expr, list | tuple): | ||
| expr = self.model.linexpr(*expr) | ||
| if not isinstance( | ||
| expr, (expressions.LinearExpression, expressions.QuadraticExpression) | ||
| expr, expressions.LinearExpression | expressions.QuadraticExpression | ||
| ): | ||
@@ -268,3 +268,3 @@ raise ValueError( | ||
| # only allow scalar multiplication | ||
| if not isinstance(expr, (int, float, np.floating, np.integer)): | ||
| if not isinstance(expr, int | float | np.floating | np.integer): | ||
| raise ValueError("Invalid type for multiplication.") | ||
@@ -278,4 +278,4 @@ return Objective(self.expression * expr, self.model, self.sense) | ||
| # only allow scalar division | ||
| if not isinstance(expr, (int, float, np.floating, np.integer)): | ||
| if not isinstance(expr, int | float | np.floating | np.integer): | ||
| raise ValueError("Invalid type for division.") | ||
| return Objective(self.expression / expr, self.model, self.sense) |
+4
-3
@@ -10,4 +10,5 @@ #!/usr/bin/env python3 | ||
| import tempfile | ||
| from collections.abc import Callable | ||
| from dataclasses import dataclass | ||
| from typing import TYPE_CHECKING, Any, Callable, Union | ||
| from typing import TYPE_CHECKING, Any, Union | ||
@@ -120,4 +121,4 @@ from linopy.io import read_netcdf | ||
| port: int = 22 | ||
| username: Union[str, None] = None | ||
| password: Union[str, None] = None | ||
| username: str | None = None | ||
| password: str | None = None | ||
| client: Union["paramiko.SSHClient", None] = None | ||
@@ -124,0 +125,0 @@ |
@@ -16,9 +16,15 @@ from __future__ import annotations | ||
| def assert_linequal(a: LinearExpression, b: LinearExpression) -> None: | ||
| def assert_linequal( | ||
| a: LinearExpression | QuadraticExpression, b: LinearExpression | QuadraticExpression | ||
| ) -> None: | ||
| """Assert that two linear expressions are equal.""" | ||
| assert isinstance(a, LinearExpression) | ||
| assert isinstance(b, LinearExpression) | ||
| return assert_equal(_expr_unwrap(a), _expr_unwrap(b)) | ||
| def assert_quadequal(a: QuadraticExpression, b: QuadraticExpression) -> None: | ||
| """Assert that two linear expressions are equal.""" | ||
| def assert_quadequal( | ||
| a: LinearExpression | QuadraticExpression, b: LinearExpression | QuadraticExpression | ||
| ) -> None: | ||
| """Assert that two quadratic or linear expressions are equal.""" | ||
| return assert_equal(_expr_unwrap(a), _expr_unwrap(b)) | ||
@@ -25,0 +31,0 @@ |
+8
-15
| from __future__ import annotations | ||
| import sys | ||
| from collections.abc import Hashable, Iterable, Mapping, Sequence | ||
@@ -14,8 +13,2 @@ from pathlib import Path | ||
| if sys.version_info >= (3, 10): | ||
| from types import EllipsisType, NotImplementedType | ||
| else: | ||
| EllipsisType = type(Ellipsis) | ||
| NotImplementedType = type(NotImplemented) | ||
| if TYPE_CHECKING: | ||
@@ -31,4 +24,4 @@ from linopy.constraints import AnonymousScalarConstraint, Constraint | ||
| # Type aliases using Union for Python 3.9 compatibility | ||
| CoordsLike = Union[ | ||
| Sequence[Union[Sequence, Index, DataArray]], | ||
| CoordsLike = Union[ # noqa: UP007 | ||
| Sequence[Sequence | Index | DataArray], | ||
| Mapping, | ||
@@ -38,5 +31,5 @@ DataArrayCoordinates, | ||
| ] | ||
| DimsLike = Union[str, Iterable[Hashable]] | ||
| DimsLike = Union[str, Iterable[Hashable]] # noqa: UP007 | ||
| ConstantLike = Union[ | ||
| ConstantLike = Union[ # noqa: UP007 | ||
| int, | ||
@@ -51,3 +44,3 @@ float, | ||
| ] | ||
| SignLike = Union[str, numpy.ndarray, DataArray, Series, DataFrame] | ||
| SignLike = Union[str, numpy.ndarray, DataArray, Series, DataFrame] # noqa: UP007 | ||
| VariableLike = Union["ScalarVariable", "Variable"] | ||
@@ -60,4 +53,4 @@ ExpressionLike = Union[ | ||
| ConstraintLike = Union["Constraint", "AnonymousScalarConstraint"] | ||
| MaskLike = Union[numpy.ndarray, DataArray, Series, DataFrame] | ||
| SideLike = Union[ConstantLike, VariableLike, ExpressionLike] | ||
| PathLike = Union[str, Path] | ||
| MaskLike = Union[numpy.ndarray, DataArray, Series, DataFrame] # noqa: UP007 | ||
| SideLike = Union[ConstantLike, VariableLike, ExpressionLike] # noqa: UP007 | ||
| PathLike = Union[str, Path] # noqa: UP007 |
+84
-46
@@ -13,2 +13,3 @@ """ | ||
| from dataclasses import dataclass | ||
| from types import NotImplementedType | ||
| from typing import ( | ||
@@ -55,3 +56,9 @@ TYPE_CHECKING, | ||
| from linopy.constants import HELPER_DIMS, TERM_DIM | ||
| from linopy.types import ConstantLike, DimsLike, NotImplementedType, SideLike | ||
| from linopy.types import ( | ||
| ConstantLike, | ||
| DimsLike, | ||
| ExpressionLike, | ||
| SideLike, | ||
| VariableLike, | ||
| ) | ||
@@ -61,2 +68,3 @@ if TYPE_CHECKING: | ||
| from linopy.expressions import ( | ||
| GenericExpression, | ||
| LinearExpression, | ||
@@ -387,12 +395,14 @@ LinearExpressionGroupby, | ||
| def __mul__( | ||
| self, other: float | int | ndarray | Variable | ||
| ) -> LinearExpression | QuadraticExpression: | ||
| @overload | ||
| def __mul__(self, other: ConstantLike) -> LinearExpression: ... | ||
| @overload | ||
| def __mul__(self, other: ExpressionLike | VariableLike) -> QuadraticExpression: ... | ||
| def __mul__(self, other: SideLike) -> ExpressionLike: | ||
| """ | ||
| Multiply variables with a coefficient. | ||
| Multiply variables with a coefficient, variable, or expression. | ||
| """ | ||
| try: | ||
| if isinstance( | ||
| other, (expressions.LinearExpression, Variable, ScalarVariable) | ||
| ): | ||
| if isinstance(other, Variable | ScalarVariable): | ||
| return self.to_linexpr() * other | ||
@@ -404,2 +414,11 @@ | ||
| def __rmul__(self, other: ConstantLike) -> LinearExpression: | ||
| """ | ||
| Right-multiply variables by a constant | ||
| """ | ||
| try: | ||
| return self * other | ||
| except TypeError: | ||
| return NotImplemented | ||
| def __pow__(self, other: int) -> QuadraticExpression: | ||
@@ -409,19 +428,20 @@ """ | ||
| """ | ||
| if isinstance(other, int) and other == 2: | ||
| if not isinstance(other, int): | ||
| return NotImplemented | ||
| if other == 2: | ||
| expr = self.to_linexpr() | ||
| return expr._multiply_by_linear_expression(expr) | ||
| return NotImplemented | ||
| raise ValueError("Can only raise to the power of 2") | ||
| def __rmul__(self, other: float | DataArray | int | ndarray) -> LinearExpression: | ||
| """ | ||
| Right-multiply variables with a coefficient. | ||
| """ | ||
| try: | ||
| return self.to_linexpr(other) | ||
| except TypeError: | ||
| return NotImplemented | ||
| @overload | ||
| def __matmul__(self, other: ConstantLike) -> LinearExpression: ... | ||
| @overload | ||
| def __matmul__( | ||
| self, other: LinearExpression | ndarray | Variable | ||
| ) -> QuadraticExpression | LinearExpression: | ||
| self, other: VariableLike | ExpressionLike | ||
| ) -> QuadraticExpression: ... | ||
| def __matmul__( | ||
| self, other: ConstantLike | VariableLike | ExpressionLike | ||
| ) -> LinearExpression | QuadraticExpression: | ||
| """ | ||
@@ -438,3 +458,3 @@ Matrix multiplication of variables with a coefficient. | ||
| """ | ||
| if isinstance(other, (expressions.LinearExpression, Variable)): | ||
| if isinstance(other, expressions.LinearExpression | Variable): | ||
| raise TypeError( | ||
@@ -458,5 +478,14 @@ "unsupported operand type(s) for /: " | ||
| @overload | ||
| def __add__( | ||
| self, other: int | QuadraticExpression | LinearExpression | Variable | ||
| ) -> QuadraticExpression | LinearExpression: | ||
| self, other: ConstantLike | Variable | ScalarLinearExpression | ||
| ) -> LinearExpression: ... | ||
| @overload | ||
| def __add__(self, other: GenericExpression) -> GenericExpression: ... | ||
| def __add__( | ||
| self, | ||
| other: ConstantLike | Variable | ScalarLinearExpression | GenericExpression, | ||
| ) -> LinearExpression | GenericExpression: | ||
| """ | ||
@@ -470,9 +499,20 @@ Add variables to linear expressions or other variables. | ||
| def __radd__(self, other: int) -> Variable | NotImplementedType: | ||
| # This is needed for using python's sum function | ||
| return self if other == 0 else NotImplemented | ||
| def __radd__(self, other: ConstantLike) -> LinearExpression: | ||
| try: | ||
| return self + other | ||
| except TypeError: | ||
| return NotImplemented | ||
| @overload | ||
| def __sub__( | ||
| self, other: QuadraticExpression | LinearExpression | Variable | ||
| ) -> QuadraticExpression | LinearExpression: | ||
| self, other: ConstantLike | Variable | ScalarLinearExpression | ||
| ) -> LinearExpression: ... | ||
| @overload | ||
| def __sub__(self, other: GenericExpression) -> GenericExpression: ... | ||
| def __sub__( | ||
| self, | ||
| other: ConstantLike | Variable | ScalarLinearExpression | GenericExpression, | ||
| ) -> LinearExpression | GenericExpression: | ||
| """ | ||
@@ -486,2 +526,11 @@ Subtract linear expressions or other variables from the variables. | ||
| def __rsub__(self, other: ConstantLike) -> LinearExpression: | ||
| """ | ||
| Subtract linear expressions or other variables from the variables. | ||
| """ | ||
| try: | ||
| return self.to_linexpr(-1) + other | ||
| except TypeError: | ||
| return NotImplemented | ||
| def __le__(self, other: SideLike) -> Constraint: | ||
@@ -719,15 +768,3 @@ return self.to_linexpr().__le__(other) | ||
| @classmethod # type: ignore | ||
| @property | ||
| def fill_value(cls) -> dict[str, Any]: | ||
| """ | ||
| Return the fill value of the variable. | ||
| """ | ||
| warn( | ||
| "The `.fill_value` attribute is deprecated, use linopy.variables.FILL_VALUE instead.", | ||
| DeprecationWarning, | ||
| ) | ||
| return cls._fill_value | ||
| @property | ||
| def mask(self) -> DataArray: | ||
@@ -993,3 +1030,3 @@ """ | ||
| _other = {"labels": other.label, "lower": other.lower, "upper": other.upper} | ||
| elif isinstance(other, (dict, Dataset)): | ||
| elif isinstance(other, dict | Dataset): | ||
| _other = other | ||
@@ -1398,3 +1435,3 @@ else: | ||
| """ | ||
| if not isinstance(label, (float, int, np.integer)) or label < 0: | ||
| if not isinstance(label, float | int | np.integer) or label < 0: | ||
| raise ValueError("Label must be a positive number.") | ||
@@ -1483,4 +1520,3 @@ for name, labels in self.labels.items(): | ||
| In contrast to the Variable class, a ScalarVariable only contains | ||
| only one label. Use this class to create a expression or constraint | ||
| In contrast to the Variable class, a ScalarVariable only contains one label. Use this class to create a expression or constraint | ||
| in a rule. | ||
@@ -1533,7 +1569,7 @@ """ | ||
| def to_scalar_linexpr(self, coeff: int | float = 1) -> ScalarLinearExpression: | ||
| if not isinstance(coeff, (int, np.integer, float)): | ||
| if not isinstance(coeff, int | np.integer | float): | ||
| raise TypeError(f"Coefficient must be a numeric value, got {type(coeff)}.") | ||
| return expressions.ScalarLinearExpression((coeff,), (self.label,), self.model) | ||
| def to_linexpr(self, coeff: int = 1) -> LinearExpression: | ||
| def to_linexpr(self, coeff: int | float = 1) -> LinearExpression: | ||
| return self.to_scalar_linexpr(coeff).to_linexpr() | ||
@@ -1558,2 +1594,4 @@ | ||
| def __rmul__(self, coeff: int | float) -> ScalarLinearExpression: | ||
| if isinstance(coeff, Variable | ScalarVariable): | ||
| return NotImplemented | ||
| return self.to_scalar_linexpr(coeff) | ||
@@ -1560,0 +1598,0 @@ |
+16
-3
| # file generated by setuptools-scm | ||
| # don't change, don't track in version control | ||
| __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] | ||
| __all__ = [ | ||
| "__version__", | ||
| "__version_tuple__", | ||
| "version", | ||
| "version_tuple", | ||
| "__commit_id__", | ||
| "commit_id", | ||
| ] | ||
@@ -12,4 +19,6 @@ TYPE_CHECKING = False | ||
| VERSION_TUPLE = Tuple[Union[int, str], ...] | ||
| COMMIT_ID = Union[str, None] | ||
| else: | ||
| VERSION_TUPLE = object | ||
| COMMIT_ID = object | ||
@@ -20,4 +29,8 @@ version: str | ||
| version_tuple: VERSION_TUPLE | ||
| commit_id: COMMIT_ID | ||
| __commit_id__: COMMIT_ID | ||
| __version__ = version = '0.5.5' | ||
| __version_tuple__ = version_tuple = (0, 5, 5) | ||
| __version__ = version = '0.5.6' | ||
| __version_tuple__ = version_tuple = (0, 5, 6) | ||
| __commit_id__ = commit_id = 'ge01cfed78' |
+8
-8
| Metadata-Version: 2.4 | ||
| Name: linopy | ||
| Version: 0.5.5 | ||
| Version: 0.5.6 | ||
| Summary: Linear optimization with N-D labeled arrays in Python | ||
@@ -8,6 +8,6 @@ Author-email: Fabian Hofmann <fabianmarikhofmann@gmail.com> | ||
| Project-URL: Source, https://github.com/PyPSA/linopy | ||
| Classifier: Programming Language :: Python :: 3.9 | ||
| Classifier: Programming Language :: Python :: 3.10 | ||
| Classifier: Programming Language :: Python :: 3.11 | ||
| Classifier: Programming Language :: Python :: 3.12 | ||
| Classifier: Programming Language :: Python :: 3.13 | ||
| Classifier: Development Status :: 3 - Alpha | ||
@@ -20,3 +20,3 @@ Classifier: Environment :: Console | ||
| Classifier: Operating System :: OS Independent | ||
| Requires-Python: >=3.9 | ||
| Requires-Python: >=3.10 | ||
| Description-Content-Type: text/markdown | ||
@@ -135,5 +135,5 @@ License-File: LICENSE.txt | ||
| >>> days = pd.Index(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], name='day') | ||
| >>> apples = m.add_variables(lower=0, name='apples', coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name='bananas', coords=[days]) | ||
| >>> days = pd.Index(["Mon", "Tue", "Wed", "Thu", "Fri"], name="day") | ||
| >>> apples = m.add_variables(lower=0, name="apples", coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name="bananas", coords=[days]) | ||
| >>> apples | ||
@@ -154,3 +154,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name='daily_vitamins') | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name="daily_vitamins") | ||
| ``` | ||
@@ -170,3 +170,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name='weekly_vitamins') | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name="weekly_vitamins") | ||
| ``` | ||
@@ -173,0 +173,0 @@ ``` |
+2
-2
@@ -13,6 +13,6 @@ [build-system] | ||
| classifiers = [ | ||
| "Programming Language :: Python :: 3.9", | ||
| "Programming Language :: Python :: 3.10", | ||
| "Programming Language :: Python :: 3.11", | ||
| "Programming Language :: Python :: 3.12", | ||
| "Programming Language :: Python :: 3.13", | ||
| "Development Status :: 3 - Alpha", | ||
@@ -27,3 +27,3 @@ "Environment :: Console", | ||
| requires-python = ">=3.9" | ||
| requires-python = ">=3.10" | ||
| dependencies = [ | ||
@@ -30,0 +30,0 @@ "numpy; python_version > '3.10'", |
+5
-5
@@ -68,5 +68,5 @@ # linopy: Optimization with array-like variables and constraints | ||
| >>> days = pd.Index(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], name='day') | ||
| >>> apples = m.add_variables(lower=0, name='apples', coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name='bananas', coords=[days]) | ||
| >>> days = pd.Index(["Mon", "Tue", "Wed", "Thu", "Fri"], name="day") | ||
| >>> apples = m.add_variables(lower=0, name="apples", coords=[days]) | ||
| >>> bananas = m.add_variables(lower=0, name="bananas", coords=[days]) | ||
| >>> apples | ||
@@ -87,3 +87,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name='daily_vitamins') | ||
| >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name="daily_vitamins") | ||
| ``` | ||
@@ -103,3 +103,3 @@ ``` | ||
| ```python | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name='weekly_vitamins') | ||
| >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name="weekly_vitamins") | ||
| ``` | ||
@@ -106,0 +106,0 @@ ``` |
@@ -7,5 +7,6 @@ from typing import Any | ||
| import xarray as xr | ||
| from xarray.testing import assert_equal | ||
| from linopy import LESS_EQUAL, Model, Variable | ||
| from linopy.testing import assert_linequal | ||
| from linopy.testing import assert_linequal, assert_quadequal | ||
@@ -98,13 +99,14 @@ | ||
| other_datatype = SomeOtherDatatype(data.copy()) | ||
| assert_linequal(x + data, x + other_datatype) # type: ignore | ||
| assert_linequal(x - data, x - other_datatype) # type: ignore | ||
| assert_linequal(x * data, x * other_datatype) # type: ignore | ||
| assert_linequal(x + data, x + other_datatype) | ||
| assert_linequal(x - data, x - other_datatype) | ||
| assert_linequal(x * data, x * other_datatype) | ||
| assert_linequal(x / data, x / other_datatype) # type: ignore | ||
| assert_linequal(data * x, other_datatype * x) # type: ignore | ||
| assert x.__add__(object()) is NotImplemented # type: ignore | ||
| assert x.__sub__(object()) is NotImplemented # type: ignore | ||
| assert x.__mul__(object()) is NotImplemented # type: ignore | ||
| assert x.__add__(object()) is NotImplemented | ||
| assert x.__sub__(object()) is NotImplemented | ||
| assert x.__mul__(object()) is NotImplemented | ||
| assert x.__truediv__(object()) is NotImplemented # type: ignore | ||
| assert x.__pow__(object()) is NotImplemented # type: ignore | ||
| assert x.__pow__(3) is NotImplemented | ||
| with pytest.raises(ValueError): | ||
| x.__pow__(3) | ||
@@ -128,2 +130,13 @@ | ||
| def test_arithmetric_operations_vars_and_expr(m: Model) -> None: | ||
| x = m.variables["x"] | ||
| x_expr = x * 1.0 | ||
| assert_quadequal(x**2, x_expr**2) | ||
| assert_quadequal(x**2 + x, x + x**2) | ||
| assert_quadequal(x**2 * 2, x**2 * 2) | ||
| with pytest.raises(TypeError): | ||
| _ = x**2 * x | ||
| def test_arithmetric_operations_con(m: Model) -> None: | ||
@@ -139,5 +152,6 @@ c = m.constraints["c"] | ||
| assert_linequal(c.lhs / data, c.lhs / other_datatype) | ||
| assert_linequal(c.rhs + data, c.rhs + other_datatype) # type: ignore | ||
| assert_linequal(c.rhs - data, c.rhs - other_datatype) # type: ignore | ||
| assert_linequal(c.rhs * data, c.rhs * other_datatype) # type: ignore | ||
| assert_linequal(c.rhs / data, c.rhs / other_datatype) # type: ignore | ||
| assert_equal(c.rhs + data, c.rhs + other_datatype) | ||
| assert_equal(c.rhs - data, c.rhs - other_datatype) | ||
| assert_equal(c.rhs * data, c.rhs * other_datatype) | ||
| assert_equal(c.rhs / data, c.rhs / other_datatype) |
@@ -313,3 +313,3 @@ #!/usr/bin/env python3 | ||
| ) -> None: | ||
| c.vars = x # type: ignore | ||
| c.vars = x | ||
| assert_equal(c.vars, x.labels) | ||
@@ -333,3 +333,3 @@ | ||
| def test_constraint_coeffs_setter(c: linopy.constraints.Constraint) -> None: | ||
| c.coeffs = 3 # type: ignore | ||
| c.coeffs = 3 | ||
| assert (c.coeffs == 3).all() | ||
@@ -350,3 +350,3 @@ | ||
| ) -> None: | ||
| c.lhs = x # type: ignore | ||
| c.lhs = x | ||
| assert c.lhs.nterm == 1 | ||
@@ -357,3 +357,3 @@ | ||
| sizes = c.sizes | ||
| c.lhs = 10 # type: ignore | ||
| c.lhs = 10 | ||
| assert (c.rhs == -10).all() | ||
@@ -365,3 +365,3 @@ assert c.lhs.nterm == 0 | ||
| def test_constraint_sign_setter(c: linopy.constraints.Constraint) -> None: | ||
| c.sign = EQUAL # type: ignore | ||
| c.sign = EQUAL | ||
| assert (c.sign == EQUAL).all() | ||
@@ -371,3 +371,3 @@ | ||
| def test_constraint_sign_setter_alternative(c: linopy.constraints.Constraint) -> None: | ||
| c.sign = long_EQUAL # type: ignore | ||
| c.sign = long_EQUAL | ||
| assert (c.sign == EQUAL).all() | ||
@@ -379,3 +379,3 @@ | ||
| with pytest.raises(ValueError): | ||
| c.sign = "asd" # type: ignore | ||
| c.sign = "asd" | ||
@@ -402,3 +402,3 @@ | ||
| ) -> None: | ||
| c.rhs = x + y # type: ignore | ||
| c.rhs = x + y | ||
| assert (c.rhs == 0).all() | ||
@@ -412,3 +412,3 @@ assert (c.coeffs.isel({c.term_dim: -1}) == -1).all() | ||
| ) -> None: | ||
| c.rhs = x + 1 # type: ignore | ||
| c.rhs = x + 1 | ||
| assert (c.rhs == 1).all() | ||
@@ -415,0 +415,0 @@ assert (c.coeffs.sum(c.term_dim) == 0).all() |
+1
-2
@@ -10,3 +10,2 @@ #!/usr/bin/env python3 | ||
| from pathlib import Path | ||
| from typing import Union | ||
@@ -169,3 +168,3 @@ import pandas as pd | ||
| fn: Union[str, None] = None | ||
| fn: str | None = None | ||
| model.to_file(fn) | ||
@@ -172,0 +171,0 @@ |
@@ -17,6 +17,7 @@ #!/usr/bin/env python3 | ||
| from linopy import LinearExpression, Model, Variable, merge | ||
| from linopy import LinearExpression, Model, QuadraticExpression, Variable, merge | ||
| from linopy.constants import HELPER_DIMS, TERM_DIM | ||
| from linopy.expressions import ScalarLinearExpression | ||
| from linopy.testing import assert_linequal | ||
| from linopy.testing import assert_linequal, assert_quadequal | ||
| from linopy.variables import ScalarVariable | ||
@@ -150,6 +151,3 @@ | ||
| with pytest.warns(DeprecationWarning): | ||
| LinearExpression.fill_value | ||
| def test_linexpr_with_scalars(m: Model) -> None: | ||
@@ -163,2 +161,9 @@ expr = m.linexpr((10, "x"), (1, "y")) | ||
| def test_linexpr_with_variables_and_constants( | ||
| m: Model, x: Variable, y: Variable | ||
| ) -> None: | ||
| expr = m.linexpr((10, x), (1, y), 2) | ||
| assert (expr.const == 2).all() | ||
| def test_linexpr_with_series(m: Model, v: Variable) -> None: | ||
@@ -193,2 +198,5 @@ lhs = pd.Series(np.arange(20)), v | ||
| expr3 = expr.mul(1) | ||
| assert_linequal(expr, expr3) | ||
| expr = x / 1 | ||
@@ -203,2 +211,5 @@ assert isinstance(expr, LinearExpression) | ||
| expr3 = expr.div(1) | ||
| assert_linequal(expr, expr3) | ||
| expr = np.array([1, 2]) * x | ||
@@ -216,3 +227,14 @@ assert isinstance(expr, LinearExpression) | ||
| quad = x * x | ||
| assert isinstance(quad, QuadraticExpression) | ||
| with pytest.raises(TypeError): | ||
| quad * quad | ||
| expr = x * 1 | ||
| assert isinstance(expr, LinearExpression) | ||
| assert expr.__mul__(object()) is NotImplemented | ||
| assert expr.__rmul__(object()) is NotImplemented | ||
| def test_linear_expression_with_addition(m: Model, x: Variable, y: Variable) -> None: | ||
@@ -234,3 +256,17 @@ expr = 10 * x + y | ||
| expr3 = (x * 1).add(y) | ||
| assert_linequal(expr, expr3) | ||
| expr3 = x + (x * x) | ||
| assert isinstance(expr3, QuadraticExpression) | ||
| def test_linear_expression_with_raddition(m: Model, x: Variable) -> None: | ||
| expr = x * 1.0 | ||
| expr_2: LinearExpression = 10.0 + expr | ||
| assert isinstance(expr, LinearExpression) | ||
| expr_3: LinearExpression = expr + 10.0 | ||
| assert_linequal(expr_2, expr_3) | ||
| def test_linear_expression_with_subtraction(m: Model, x: Variable, y: Variable) -> None: | ||
@@ -244,2 +280,6 @@ expr = x - y | ||
| expr3: LinearExpression = x * 1 | ||
| expr4 = expr3.sub(y) | ||
| assert_linequal(expr, expr4) | ||
| expr = -x - 8 * y | ||
@@ -250,2 +290,11 @@ assert isinstance(expr, LinearExpression) | ||
| def test_linear_expression_rsubtraction(x: Variable, y: Variable) -> None: | ||
| expr = x * 1.0 | ||
| expr_2: LinearExpression = 10.0 - expr | ||
| assert isinstance(expr_2, LinearExpression) | ||
| expr_3: LinearExpression = (expr - 10.0) * -1 | ||
| assert_linequal(expr_2, expr_3) | ||
| assert expr.__rsub__(object()) is NotImplemented | ||
| def test_linear_expression_with_constant(m: Model, x: Variable, y: Variable) -> None: | ||
@@ -289,5 +338,8 @@ expr = x + 1 | ||
| with pytest.raises(TypeError): | ||
| m.linexpr((10, x.labels), (1, "y")) # type: ignore | ||
| m.linexpr((10, x.labels), (1, "y")) | ||
| with pytest.raises(TypeError): | ||
| m.linexpr(a=2) # type: ignore | ||
| def test_linear_expression_from_rule(m: Model, x: Variable, y: Variable) -> None: | ||
@@ -335,2 +387,5 @@ def bound(m: Model, i: int) -> ScalarLinearExpression: | ||
| res2 = expr.add(other) | ||
| assert_linequal(res, res2) | ||
| assert isinstance(x - expr, LinearExpression) | ||
@@ -456,2 +511,14 @@ assert isinstance(x + expr, LinearExpression) | ||
| def test_linear_expression_power(x: Variable) -> None: | ||
| expr: LinearExpression = x * 1.0 | ||
| qd_expr = expr**2 | ||
| assert isinstance(qd_expr, QuadraticExpression) | ||
| qd_expr2 = expr.pow(2) | ||
| assert_quadequal(qd_expr, qd_expr2) | ||
| with pytest.raises(ValueError): | ||
| expr**3 | ||
| def test_linear_expression_multiplication( | ||
@@ -900,3 +967,3 @@ x: Variable, y: Variable, z: Variable | ||
| if use_fallback: | ||
| with pytest.raises(KeyError): | ||
| with pytest.raises((KeyError, IndexError)): | ||
| expr.groupby(groups).sum(use_fallback=use_fallback) | ||
@@ -1024,2 +1091,43 @@ return | ||
| def test_linear_expression_from_tuples(x: Variable, y: Variable) -> None: | ||
| expr = LinearExpression.from_tuples((10, x), (1, y)) | ||
| assert isinstance(expr, LinearExpression) | ||
| with pytest.warns(DeprecationWarning): | ||
| expr2 = LinearExpression.from_tuples((10, x), (1,)) | ||
| assert isinstance(expr2, LinearExpression) | ||
| assert (expr2.const == 1).all() | ||
| expr3 = LinearExpression.from_tuples((10, x), 1) | ||
| assert isinstance(expr3, LinearExpression) | ||
| assert_linequal(expr2, expr3) | ||
| expr4 = LinearExpression.from_tuples((10, x), (1, y), 1) | ||
| assert isinstance(expr4, LinearExpression) | ||
| assert (expr4.const == 1).all() | ||
| expr5 = LinearExpression.from_tuples(1, model=x.model) | ||
| assert isinstance(expr5, LinearExpression) | ||
| def test_linear_expression_from_tuples_bad_calls( | ||
| m: Model, x: Variable, y: Variable | ||
| ) -> None: | ||
| with pytest.raises(ValueError): | ||
| LinearExpression.from_tuples((10, x), (1, y), x) | ||
| with pytest.raises(ValueError): | ||
| LinearExpression.from_tuples((10, x, 3), (1, y), 1) | ||
| sv = ScalarVariable(label=0, model=m) | ||
| with pytest.raises(TypeError): | ||
| LinearExpression.from_tuples((np.array([1, 1]), sv)) | ||
| with pytest.raises(TypeError): | ||
| LinearExpression.from_tuples((x, x)) | ||
| with pytest.raises(ValueError): | ||
| LinearExpression.from_tuples(10) | ||
| def test_linear_expression_sanitize(x: Variable, y: Variable, z: Variable) -> None: | ||
@@ -1034,7 +1142,8 @@ expr = 10 * x + y + z | ||
| res = merge([expr1, expr2]) | ||
| res = merge([expr1, expr2], cls=LinearExpression) | ||
| assert res.nterm == 6 | ||
| res = merge([expr1, expr2]) | ||
| assert res.nterm == 6 | ||
| with pytest.warns(DeprecationWarning): | ||
| res: LinearExpression = merge([expr1, expr2]) # type: ignore | ||
| assert res.nterm == 6 | ||
@@ -1045,3 +1154,3 @@ # now concat with same length of terms | ||
| res = merge([expr1, expr2], dim="dim_1") | ||
| res = merge([expr1, expr2], dim="dim_1", cls=LinearExpression) | ||
| assert res.nterm == 3 | ||
@@ -1053,3 +1162,3 @@ | ||
| res = merge([expr1, expr2], dim="dim_1") | ||
| res = merge([expr1, expr2], dim="dim_1", cls=LinearExpression) | ||
| assert res.nterm == 3 | ||
@@ -1059,3 +1168,3 @@ assert res.sel(dim_1=0).vars[2].item() == -1 | ||
| with pytest.warns(DeprecationWarning): | ||
| merge(expr1, expr2) # type: ignore | ||
| merge(expr1, expr2) | ||
@@ -1062,0 +1171,0 @@ |
@@ -132,3 +132,3 @@ #!/usr/bin/env python3 | ||
| m.objective = 2 * x + y # type: ignore | ||
| m.objective = 2 * x + y | ||
@@ -146,3 +146,3 @@ return m | ||
| m.add_constraints(x + x, GREATER_EQUAL, 10) | ||
| m.objective = 1 * x # type: ignore | ||
| m.objective = 1 * x | ||
@@ -162,3 +162,3 @@ return m | ||
| m.add_constraints(x + y, GREATER_EQUAL, 10.5) | ||
| m.objective = 1 * x + 0.5 * y # type: ignore | ||
| m.objective = 1 * x + 0.5 * y | ||
@@ -276,5 +276,5 @@ return m | ||
| y.lower = 9 # type: ignore | ||
| y.lower = 9 | ||
| c.lhs = 2 * x + y | ||
| m.objective = 2 * x + y # type: ignore | ||
| m.objective = 2 * x + y | ||
@@ -393,3 +393,3 @@ return m | ||
| qexpr = 4 * x * y | ||
| qexpr = 4 * (x * y) # type: ignore | ||
| assert_equal(qexpr.solution, 4 * x.solution * y.solution) | ||
@@ -597,3 +597,3 @@ | ||
| if solver == "gurobi": | ||
| if solver in ["gurobi", "xpress"]: | ||
| # ignore deprecated warning | ||
@@ -600,0 +600,0 @@ with pytest.warns(DeprecationWarning): |
@@ -12,3 +12,3 @@ #!/usr/bin/env python3 | ||
| from linopy.constants import FACTOR_DIM, TERM_DIM | ||
| from linopy.expressions import QuadraticExpression | ||
| from linopy.expressions import LinearExpression, QuadraticExpression | ||
| from linopy.testing import assert_quadequal | ||
@@ -45,2 +45,9 @@ | ||
| def test_adding_quadratic_expressions(x: Variable) -> None: | ||
| quad_expr = x * x | ||
| double_quad = quad_expr + quad_expr | ||
| assert isinstance(double_quad, QuadraticExpression) | ||
| assert double_quad.__add__(object()) is NotImplemented | ||
| def test_quadratic_expression_from_variables_power(x: Variable) -> None: | ||
@@ -121,3 +128,15 @@ power_expr = x**2 | ||
| with pytest.raises(TypeError): | ||
| (x**2) @ (y**2) | ||
| def test_matmul_with_const(x: Variable) -> None: | ||
| expr = x * x | ||
| const = DataArray([2.0, 1.0], dims=["dim_0"]) | ||
| expr2 = expr @ const | ||
| assert isinstance(expr2, QuadraticExpression) | ||
| assert expr2.nterm == 2 | ||
| assert expr2.data.sizes[FACTOR_DIM] == 2 | ||
| def test_quadratic_expression_dot_and_matmul(x: Variable, y: Variable) -> None: | ||
@@ -150,6 +169,10 @@ matmul_expr: QuadraticExpression = 10 * x @ y # type: ignore | ||
| with pytest.raises(TypeError): | ||
| 5 + x * y + x | ||
| expr_2 = 5 + x * y + x | ||
| assert isinstance(expr_2, QuadraticExpression) | ||
| assert (expr_2.const == 5).all() | ||
| assert expr_2.nterm == 2 | ||
| assert_quadequal(expr, expr_2) | ||
| def test_quadratic_expression_subtraction(x: Variable, y: Variable) -> None: | ||
@@ -160,2 +183,3 @@ expr = x * y - x - 5 | ||
| assert expr.nterm == 2 | ||
| assert expr.__sub__(object()) is NotImplemented | ||
@@ -169,3 +193,8 @@ | ||
| expr2 = 5 - x * y | ||
| assert isinstance(expr2, QuadraticExpression) | ||
| assert (expr2.const == 5).all() | ||
| assert expr2.nterm == 1 | ||
| def test_quadratic_expression_sum(x: Variable, y: Variable) -> None: | ||
@@ -197,3 +226,7 @@ base_expr = x * y + x + 5 | ||
| quad = x * x | ||
| with pytest.raises(TypeError): | ||
| quad * quad | ||
| def merge_raise_deprecation_warning(x: Variable, y: Variable) -> None: | ||
@@ -208,12 +241,14 @@ expr: QuadraticExpression = x * y # type: ignore | ||
| ) -> None: | ||
| linexpr = 10 * x + y + 5 | ||
| quadexpr = x * y | ||
| linexpr: LinearExpression = 10 * x + y + 5 | ||
| quadexpr: QuadraticExpression = x * y # type: ignore | ||
| merge([linexpr.to_quadexpr(), quadexpr], cls=QuadraticExpression) | ||
| with pytest.raises(ValueError): | ||
| merge([linexpr, quadexpr], cls=QuadraticExpression) | ||
| with pytest.warns(DeprecationWarning): | ||
| merge(linexpr, quadexpr, cls=QuadraticExpression) # type: ignore | ||
| linexpr = linexpr.to_quadexpr() | ||
| merged_expr = merge([linexpr, quadexpr], cls=QuadraticExpression) | ||
| with pytest.warns(DeprecationWarning): | ||
| merge(quadexpr, quadexpr, cls=QuadraticExpression) # type: ignore | ||
| quadexpr_2 = linexpr.to_quadexpr() | ||
| merged_expr = merge([quadexpr_2, quadexpr], cls=QuadraticExpression) | ||
| assert isinstance(merged_expr, QuadraticExpression) | ||
@@ -228,3 +263,9 @@ assert merged_expr.nterm == 3 | ||
| qdexpr = merge([x**2, y**2], cls=QuadraticExpression) | ||
| assert isinstance(qdexpr, QuadraticExpression) | ||
| with pytest.raises(ValueError): | ||
| merge([x**2, y**2], cls=LinearExpression) | ||
| def test_quadratic_expression_loc(x: Variable) -> None: | ||
@@ -274,3 +315,3 @@ expr = x * x | ||
| expr = 10 * x * y | ||
| model.objective = expr # type: ignore | ||
| model.objective = expr | ||
@@ -286,3 +327,3 @@ Q = model.matrices.Q | ||
| quad_expr = x * y + x | ||
| model.objective = quad_expr + x # type: ignore | ||
| model.objective = quad_expr + x | ||
@@ -301,1 +342,14 @@ Q = model.matrices.Q | ||
| x * y <= 10 | ||
| def test_power_of_three(x: Variable) -> None: | ||
| with pytest.raises(TypeError): | ||
| x * x * x | ||
| with pytest.raises(TypeError): | ||
| (x * 1) * (x * x) | ||
| with pytest.raises(TypeError): | ||
| (x * x) * (x * 1) | ||
| with pytest.raises(ValueError): | ||
| x**3 | ||
| with pytest.raises(TypeError): | ||
| (x * x) * (x * x) |
+57
-1
@@ -11,4 +11,5 @@ #!/usr/bin/env python3 | ||
| import pytest | ||
| from test_io import model # noqa: F401 | ||
| from linopy import solvers | ||
| from linopy import Model, solvers | ||
@@ -68,1 +69,56 @@ free_mps_problem = """NAME sample_mip | ||
| assert result.solution.objective == 30.0 | ||
| @pytest.mark.skipif( | ||
| "gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed" | ||
| ) | ||
| def test_gurobi_environment_with_dict(model: Model, tmp_path: Path) -> None: # noqa: F811 | ||
| gurobi = solvers.Gurobi() | ||
| mps_file = tmp_path / "problem.mps" | ||
| mps_file.write_text(free_mps_problem) | ||
| sol_file = tmp_path / "solution.sol" | ||
| log1_file = tmp_path / "gurobi1.log" | ||
| result = gurobi.solve_problem( | ||
| problem_fn=mps_file, solution_fn=sol_file, env={"LogFile": str(log1_file)} | ||
| ) | ||
| assert result.status.is_ok | ||
| assert log1_file.exists() | ||
| log2_file = tmp_path / "gurobi2.log" | ||
| gurobi.solve_problem( | ||
| model=model, solution_fn=sol_file, env={"LogFile": str(log2_file)} | ||
| ) | ||
| assert result.status.is_ok | ||
| assert log2_file.exists() | ||
| @pytest.mark.skipif( | ||
| "gurobi" not in set(solvers.available_solvers), reason="Gurobi is not installed" | ||
| ) | ||
| def test_gurobi_environment_with_gurobi_env(model: Model, tmp_path: Path) -> None: # noqa: F811 | ||
| import gurobipy as gp | ||
| gurobi = solvers.Gurobi() | ||
| mps_file = tmp_path / "problem.mps" | ||
| mps_file.write_text(free_mps_problem) | ||
| sol_file = tmp_path / "solution.sol" | ||
| log1_file = tmp_path / "gurobi1.log" | ||
| with gp.Env(params={"LogFile": str(log1_file)}) as env: | ||
| result = gurobi.solve_problem( | ||
| problem_fn=mps_file, solution_fn=sol_file, env=env | ||
| ) | ||
| assert result.status.is_ok | ||
| assert log1_file.exists() | ||
| log2_file = tmp_path / "gurobi2.log" | ||
| with gp.Env(params={"LogFile": str(log2_file)}) as env: | ||
| gurobi.solve_problem(model=model, solution_fn=sol_file, env=env) | ||
| assert result.status.is_ok | ||
| assert log2_file.exists() |
@@ -20,2 +20,3 @@ #!/usr/bin/env python3 | ||
| from linopy import Model | ||
| from linopy.testing import assert_linequal | ||
@@ -131,3 +132,3 @@ | ||
| def test_variable_upper_setter(z: linopy.Variable) -> None: | ||
| z.upper = 20 # type: ignore | ||
| z.upper = 20 | ||
| assert z.upper.item() == 20 | ||
@@ -137,3 +138,3 @@ | ||
| def test_variable_lower_setter(z: linopy.Variable) -> None: | ||
| z.lower = 8 # type: ignore | ||
| z.lower = 8 | ||
| assert z.lower == 8 | ||
@@ -188,6 +189,3 @@ | ||
| with pytest.warns(DeprecationWarning): | ||
| linopy.variables.Variable.fill_value | ||
| def test_variable_where(x: linopy.Variable) -> None: | ||
@@ -312,1 +310,37 @@ x = x.where([True] * 4 + [False] * 6) | ||
| assert s.size <= 2 | ||
| def test_variable_addition(x: linopy.Variable) -> None: | ||
| expr1 = x + 1 | ||
| assert isinstance(expr1, linopy.expressions.LinearExpression) | ||
| expr2 = 1 + x | ||
| assert isinstance(expr2, linopy.expressions.LinearExpression) | ||
| assert_linequal(expr1, expr2) | ||
| assert x.__radd__(object()) is NotImplemented | ||
| assert x.__add__(object()) is NotImplemented | ||
| def test_variable_subtraction(x: linopy.Variable) -> None: | ||
| expr1 = -x + 1 | ||
| assert isinstance(expr1, linopy.expressions.LinearExpression) | ||
| expr2 = 1 - x | ||
| assert isinstance(expr2, linopy.expressions.LinearExpression) | ||
| assert_linequal(expr1, expr2) | ||
| assert x.__rsub__(object()) is NotImplemented | ||
| assert x.__sub__(object()) is NotImplemented | ||
| def test_variable_multiplication(x: linopy.Variable) -> None: | ||
| expr1 = x * 2 | ||
| assert isinstance(expr1, linopy.expressions.LinearExpression) | ||
| expr2 = 2 * x | ||
| assert isinstance(expr2, linopy.expressions.LinearExpression) | ||
| assert_linequal(expr1, expr2) | ||
| expr3 = x * x | ||
| assert isinstance(expr3, linopy.expressions.QuadraticExpression) | ||
| assert x.__rmul__(object()) is NotImplemented | ||
| assert x.__mul__(object()) is NotImplemented |
@@ -16,2 +16,3 @@ #!/usr/bin/env python3 | ||
| from linopy.testing import assert_varequal | ||
| from linopy.variables import ScalarVariable | ||
@@ -119,1 +120,7 @@ | ||
| m.variables.get_name_by_label("anystring") # type: ignore | ||
| def test_scalar_variable(m: Model) -> None: | ||
| x = ScalarVariable(label=0, model=m) | ||
| assert isinstance(x, ScalarVariable) | ||
| assert x.__rmul__(x) is NotImplemented # type: ignore |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
2416340
1.77%165
3.13%16174
4.67%