├── .git-blame-ignore-revs
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yaml
│ ├── config.yml
│ └── feature_request.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── codeql.yml
│ ├── release.yml
│ ├── test-models.yml
│ └── test.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yaml
├── CONTRIBUTING.md
├── LICENSE.txt
├── README.md
├── benchmark
├── README.md
├── Snakefile
├── benchmark-absolute.pdf
├── benchmark-absolute.png
├── benchmark-overhead.pdf
├── benchmark-overhead.png
├── benchmark_resource-absolute.pdf
├── benchmark_resource-absolute.png
├── benchmark_resource-overhead.pdf
├── benchmark_resource-overhead.png
├── config.yaml
├── environment.fixed.yaml
├── environment.yaml
├── notebooks
│ └── plot-benchmarks.py.ipynb
└── scripts
│ ├── benchmark_cvxpy.py
│ ├── benchmark_gurobipy.py
│ ├── benchmark_jump.jl
│ ├── benchmark_linopy.py
│ ├── benchmark_ortools.py
│ ├── benchmark_pulp.py
│ ├── benchmark_pyomo.py
│ ├── benchmarks-pypsa-eur
│ ├── benchmark-linopy.py
│ ├── benchmark-pyomo.py
│ ├── benchmark-pypsa-linopf.py
│ ├── common.py
│ └── plot-benchmarks.py
│ ├── common.py
│ ├── concat-benchmarks.py
│ ├── leftovers
│ ├── benchmark-linopy.py
│ ├── benchmarks-pypsa-eur
│ │ ├── __pycache__
│ │ │ ├── benchmark-linopy.cpython-310.pyc
│ │ │ ├── benchmark-pyomo.cpython-310.pyc
│ │ │ ├── benchmark-pypsa-linopf.cpython-310.pyc
│ │ │ ├── common.cpython-39.pyc
│ │ │ └── plot-benchmarks.cpython-310.pyc
│ │ ├── benchmark-linopy-pypsa-eur.dat
│ │ ├── benchmark-linopy.py
│ │ ├── benchmark-pyomo-pypsa-eur.dat
│ │ ├── benchmark-pyomo.py
│ │ ├── benchmark-pypsa-eur-with-pypsa.pdf
│ │ ├── benchmark-pypsa-eur.pdf
│ │ ├── benchmark-pypsa-linopf.py
│ │ ├── benchmark-pypsa_linopf-pypsa-eur.dat
│ │ ├── common.py
│ │ └── plot-benchmarks.py
│ └── common.py
│ ├── merge-benchmarks.py
│ ├── plot-benchmarks.py
│ ├── run-cvxpy.py
│ ├── run-gurobipy.py
│ ├── run-linopy.py
│ ├── run-ortools.py
│ ├── run-pulp.py
│ ├── run-pyomo.py
│ └── write-lp-file.py
├── codecov.yml
├── doc
├── Makefile
├── _static
│ └── theme_overrides.css
├── api.rst
├── benchmark.png
├── benchmark.rst
├── benchmark_resource-overhead.png
├── conf.py
├── contributing.rst
├── create-a-model-with-coordinates.nblink
├── create-a-model.nblink
├── creating-constraints.nblink
├── creating-expressions.nblink
├── creating-variables.nblink
├── gurobi-double-logging.rst
├── index.rst
├── infeasible-model.nblink
├── logo.pdf
├── logo.png
├── logo.py
├── make.bat
├── manipulating-models.nblink
├── migrating-from-pyomo.nblink
├── prerequisites.rst
├── release_notes.rst
├── solve-on-remote.nblink
├── syntax.rst
├── testing-framework.nblink
├── transport-tutorial.nblink
└── user-guide.rst
├── examples
├── create-a-model-with-coordinates.ipynb
├── create-a-model.ipynb
├── creating-constraints.ipynb
├── creating-expressions.ipynb
├── creating-variables.ipynb
├── infeasible-model.ipynb
├── manipulating-models.ipynb
├── migrating-from-pyomo.ipynb
├── solve-on-remote.ipynb
├── testing-framework.ipynb
└── transport-tutorial.ipynb
├── linopy
├── __init__.py
├── base.py
├── common.py
├── config.py
├── constants.py
├── constraints.py
├── examples.py
├── expressions.py
├── io.py
├── matrices.py
├── model.py
├── monkey_patch_xarray.py
├── objective.py
├── py.typed
├── remote.py
├── solvers.py
├── testing.py
├── types.py
└── variables.py
├── pyproject.toml
└── test
├── test_common.py
├── test_compatible_arithmetrics.py
├── test_constraint.py
├── test_constraints.py
├── test_examples.py
├── test_inconsistency_checks.py
├── test_io.py
├── test_linear_expression.py
├── test_matrices.py
├── test_model.py
├── test_objective.py
├── test_optimization.py
├── test_options.py
├── test_quadratic_expression.py
├── test_repr.py
├── test_scalar_constraint.py
├── test_scalar_linear_expression.py
├── test_solvers.py
├── test_typing.py
├── test_variable.py
├── test_variable_assignment.py
└── test_variables.py
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | 756928f89df774f79e28380640c3cf45865b5282
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.yaml:
--------------------------------------------------------------------------------
1 | name: Bug Report
2 | description: Create a report if something doesn't work quite right.
3 | labels: ["bug", "needs triage"]
4 |
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | Thanks for taking the time to fill out this bug report!
10 |
11 | - type: checkboxes
12 | id: checks
13 | attributes:
14 | label: Version Checks (indicate both or one)
15 | options:
16 | - label: >
17 | I have confirmed this bug exists on the lastest
18 | [release](https://github.com/pypsa/linopy/releases) of Linopy.
19 | - label: >
20 | I have confirmed this bug exists on the current
21 | [`master`](https://github.com/pypsa/linopy/tree/master) branch of Linopy.
22 |
23 | - type: textarea
24 | id: problem
25 | attributes:
26 | label: Issue Description
27 | description: >
28 | Please provide a description of the issue.
29 | validations:
30 | required: true
31 |
32 | - type: textarea
33 | id: example
34 | validations:
35 | required: true
36 | attributes:
37 | label: Reproducible Example
38 | description: >
39 | Please provide a minimal reproduciable example. See how to [craft minimal bug reports](https://matthewrocklin.com/minimal-bug-reports).
40 | placeholder: >
41 | from linopy import Model
42 |
43 | m = Model()
44 |
45 | render: python
46 |
47 | - type: textarea
48 | id: expected-behavior
49 | validations:
50 | required: true
51 | attributes:
52 | label: Expected Behavior
53 | description: >
54 | Please describe or show a code example of the expected behavior.
55 |
56 | - type: textarea
57 | id: version
58 | attributes:
59 | label: Installed Versions
60 | description: >
61 | Please share information on your environment. Paste the output below. For conda ``conda env export`` and for pip ``pip freeze``.
62 | value: >
63 |
64 |
65 | Replace this line.
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | blank_issues_enabled: true
2 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature Request
3 | about: Suggest an idea for this project
4 | title: ''
5 | labels: ["feature", "needs triage"]
6 | assignees: ''
7 |
8 | ---
9 |
10 | ## Describe the feature you'd like to see
11 |
12 | *Please give a clear and concise description and provide context why the feature would be useful.*
13 | *Also, we'd appreciate any implementation ideas and references you already have.*
14 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # dependabot
2 | # Ref: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3 | # ------------------------------------------------------------------------------
4 | version: 2
5 | updates:
6 | - package-ecosystem: github-actions
7 | directory: /
8 | schedule:
9 | interval: monthly
10 | groups:
11 | # open a single pull-request for all GitHub actions updates
12 | github-actions:
13 | patterns:
14 | - '*'
15 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | Closes # (if applicable).
2 |
3 | ## Changes proposed in this Pull Request
4 |
5 |
6 | ## Checklist
7 |
8 | - [ ] Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in `doc`.
9 | - [ ] Unit tests for new features were added (if applicable).
10 | - [ ] A note for the release notes `doc/release_notes.rst` of the upcoming release is included.
11 | - [ ] I consent to the release of this PR's code under the MIT license.
12 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | # For most projects, this workflow file will not need changing; you simply need
2 | # to commit it to your repository.
3 | #
4 | # You may wish to alter this file to override the set of languages analyzed,
5 | # or to provide custom queries or build logic.
6 | #
7 | # ******** NOTE ********
8 | # We have attempted to detect the languages in your repository. Please check
9 | # the `language` matrix defined below to confirm you have the correct set of
10 | # supported CodeQL languages.
11 | #
12 | name: "CodeQL"
13 |
14 | on:
15 | push:
16 | branches: [ "master" ]
17 | pull_request:
18 | branches: [ "master" ]
19 | schedule:
20 | - cron: '19 21 * * 6'
21 |
22 | jobs:
23 | analyze:
24 | name: Analyze (${{ matrix.language }})
25 | # Runner size impacts CodeQL analysis time. To learn more, please see:
26 | # - https://gh.io/recommended-hardware-resources-for-running-codeql
27 | # - https://gh.io/supported-runners-and-hardware-resources
28 | # - https://gh.io/using-larger-runners (GitHub.com only)
29 | # Consider using larger runners or machines with greater resources for possible analysis time improvements.
30 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
31 | permissions:
32 | # required for all workflows
33 | security-events: write
34 |
35 | # required to fetch internal or private CodeQL packs
36 | packages: read
37 |
38 | # only required for workflows in private repositories
39 | actions: read
40 | contents: read
41 |
42 | strategy:
43 | fail-fast: false
44 | matrix:
45 | include:
46 | - language: python
47 | build-mode: none
48 | # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift'
49 | # Use `c-cpp` to analyze code written in C, C++ or both
50 | # Use 'java-kotlin' to analyze code written in Java, Kotlin or both
51 | # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both
52 | # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis,
53 | # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning.
54 | # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how
55 | # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
56 | steps:
57 | - name: Checkout repository
58 | uses: actions/checkout@v4
59 |
60 | # Initializes the CodeQL tools for scanning.
61 | - name: Initialize CodeQL
62 | uses: github/codeql-action/init@v3
63 | with:
64 | languages: ${{ matrix.language }}
65 | build-mode: ${{ matrix.build-mode }}
66 | # If you wish to specify custom queries, you can do so here or in a config file.
67 | # By default, queries listed here will override any specified in a config file.
68 | # Prefix the list here with "+" to use these queries and those in the config file.
69 |
70 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
71 | # queries: security-extended,security-and-quality
72 |
73 | # If the analyze step fails for one of the languages you are analyzing with
74 | # "We were unable to automatically build your code", modify the matrix above
75 | # to set the build mode to "manual" for that language. Then modify this step
76 | # to build your code.
77 | # ℹ️ Command-line programs to run using the OS shell.
78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
79 | - if: matrix.build-mode == 'manual'
80 | shell: bash
81 | run: |
82 | echo 'If you are using a "manual" build mode for one or more of the' \
83 | 'languages you are analyzing, replace this with the commands to build' \
84 | 'your code, for example:'
85 | echo ' make bootstrap'
86 | echo ' make release'
87 | exit 1
88 |
89 | - name: Perform CodeQL Analysis
90 | uses: github/codeql-action/analyze@v3
91 | with:
92 | category: "/language:${{matrix.language}}"
93 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | push:
5 | tags:
6 | - v*.*.*
7 |
8 | jobs:
9 | build:
10 | # Build the Python SDist and wheel, performs metadata and readme linting
11 | name: Build and verify package
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: hynek/build-and-inspect-python-package@v2
16 |
17 | release:
18 | # Publish a GitHub release from the given git tag
19 | name: Create GitHub Release
20 | needs: [build]
21 | runs-on: ubuntu-latest
22 | steps:
23 | - uses: actions/checkout@v4
24 | - uses: softprops/action-gh-release@v2
25 | with:
26 | generate_release_notes: true
27 |
28 | publish:
29 | # Publish the built SDist and wheel from "dist" job to PyPI
30 | name: Publish to PyPI
31 | needs: [build]
32 | runs-on: ubuntu-latest
33 | environment:
34 | name: Release
35 | url: https://pypi.org/project/linopy/
36 | permissions:
37 | id-token: write
38 | steps:
39 | - uses: actions/download-artifact@v4
40 | with:
41 | name: Packages
42 | path: dist
43 | - uses: pypa/gh-action-pypi-publish@release/v1
44 |
--------------------------------------------------------------------------------
/.github/workflows/test-models.yml:
--------------------------------------------------------------------------------
1 | name: Test models
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | pull_request:
8 | branches:
9 | - master
10 | schedule:
11 | - cron: "0 5 * * *"
12 |
13 | # Cancel any in-progress runs when a new run is triggered
14 | concurrency:
15 | group: ${{ github.workflow }}-${{ github.ref }}
16 | cancel-in-progress: true
17 |
18 | jobs:
19 | test-pypsa-eur:
20 | name: PyPSA-Eur
21 | runs-on: ubuntu-latest
22 | strategy:
23 | fail-fast: false
24 | matrix:
25 | version:
26 | - master
27 | # - latest # Activate when v0.14.0 is released
28 |
29 | defaults:
30 | run:
31 | shell: bash -l {0}
32 |
33 | steps:
34 | - uses: actions/checkout@v4
35 | with:
36 | repository: PyPSA/pypsa-eur
37 | ref: master
38 |
39 | - name: Check out latest release
40 | if: matrix.version == 'latest'
41 | run: |
42 | git fetch --tags
43 | latest_tag=$(git describe --tags `git rev-list --tags --max-count=1`)
44 | git checkout $latest_tag
45 |
46 | # Only run check if package is not pinned
47 | - name: Check if inhouse package is pinned
48 | run: |
49 | grep_line=$(grep -- '- pypsa' envs/environment.yaml)
50 | if [[ $grep_line == *"<"* || $grep_line == *"=="* ]]; then
51 | echo "pinned=true" >> $GITHUB_ENV
52 | else
53 | echo "pinned=false" >> $GITHUB_ENV
54 | fi
55 |
56 | - name: Setup secrets & cache dates
57 | if: env.pinned == 'false'
58 | run: |
59 | echo -ne "url: ${CDSAPI_URL}\nkey: ${CDSAPI_TOKEN}\n" > ~/.cdsapirc
60 | echo "week=$(date +'%Y%U')" >> $GITHUB_ENV # data and cutouts
61 |
62 | - uses: actions/cache@v4
63 | if: env.pinned == 'false'
64 | with:
65 | path: |
66 | data
67 | cutouts
68 | key: data-cutouts-${{ env.week }}
69 |
70 | - uses: conda-incubator/setup-miniconda@v3
71 | if: env.pinned == 'false'
72 | with:
73 | activate-environment: pypsa-eur
74 |
75 | - name: Cache Conda env
76 | if: env.pinned == 'false'
77 | uses: actions/cache@v4
78 | with:
79 | path: ${{ env.CONDA }}/envs
80 | key: conda-pypsa-eur-${{ env.week }}-${{ hashFiles('envs/linux-pinned.yaml') }}
81 | id: cache-env
82 |
83 | - name: Update environment
84 | if: env.pinned == 'false' && steps.cache-env.outputs.cache-hit != 'true'
85 | run: conda env update -n pypsa-eur -f envs/linux-pinned.yaml
86 |
87 | - name: Install package from ref
88 | if: env.pinned == 'false'
89 | run: |
90 | python -m pip install git+https://github.com/${{ github.repository }}@${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
91 |
92 | - name: Run snakemake test workflows
93 | if: env.pinned == 'false'
94 | run: |
95 | make test
96 |
97 | - name: Run unit tests
98 | if: env.pinned == 'false'
99 | run: |
100 | make unit-test
101 |
102 | - name: Upload artifacts
103 | if: env.pinned == 'false'
104 | uses: actions/upload-artifact@v4
105 | with:
106 | name: results-pypsa-eur-${{ matrix.version }}
107 | path: |
108 | logs
109 | .snakemake/log
110 | results
111 | retention-days: 3
112 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ '*' ]
8 | schedule:
9 | - cron: "0 5 * * TUE"
10 |
11 | concurrency:
12 | group: ${{ github.workflow }}-${{ github.ref }}
13 | cancel-in-progress: true
14 |
15 | jobs:
16 | build:
17 | # Build the Python SDist and wheel, performs metadata and readme linting
18 | name: Build and verify package
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: actions/checkout@v4
22 | with:
23 | fetch-depth: 0 # Needed for setuptools_scm
24 | - uses: hynek/build-and-inspect-python-package@v2
25 | id: baipp
26 |
27 | outputs:
28 | python-versions: ${{ steps.baipp.outputs.supported_python_classifiers_json_array }}
29 |
30 | test:
31 | # Test package build in matrix of OS and Python versions
32 | name: Test package
33 | needs: [build]
34 | runs-on: ${{ matrix.os }}
35 | strategy:
36 | fail-fast: false
37 | matrix:
38 | python-version: ${{ fromJSON(needs.build.outputs.python-versions) }}
39 | os:
40 | - ubuntu-latest
41 | - macos-latest
42 | - windows-latest
43 |
44 | steps:
45 | - uses: actions/checkout@v4
46 | with:
47 | fetch-depth: 0 # Needed for setuptools_scm
48 |
49 | - name: Set up Python ${{ matrix.python-version }} on ${{ matrix.os }}
50 | uses: actions/setup-python@v5
51 | with:
52 | python-version: ${{ matrix.python-version }}
53 |
54 | - name: Install ubuntu dependencies
55 | if: matrix.os == 'ubuntu-latest'
56 | run: |
57 | sudo apt-get install glpk-utils
58 | sudo apt-get install coinor-cbc
59 |
60 | - name: Install macos dependencies
61 | if: matrix.os == 'macos-latest'
62 | run: |
63 | brew install glpk hdf5
64 |
65 | - name: Set up windows package manager
66 | if: matrix.os == 'windows-latest'
67 | uses: crazy-max/ghaction-chocolatey@v3
68 | with:
69 | args: -h
70 |
71 | - name: Install windows dependencies
72 | if: matrix.os == 'windows-latest'
73 | run: |
74 | choco install glpk
75 |
76 | - name: Download package
77 | uses: actions/download-artifact@v4
78 | with:
79 | name: Packages
80 | path: dist
81 |
82 | - name: Install package and dependencies
83 | run: |
84 | python -m pip install uv
85 | uv pip install --system "$(ls dist/*.whl)[dev,solvers]"
86 |
87 | - name: Test with pytest
88 | env:
89 | MOSEKLM_LICENSE_FILE: ${{ secrets.MSK_LICENSE }}
90 | run: |
91 | pytest --cov=./ --cov-report=xml linopy --doctest-modules test
92 |
93 | - name: Upload code coverage report
94 | if: matrix.os == 'ubuntu-latest'
95 | uses: codecov/codecov-action@v5
96 | with:
97 | token: ${{ secrets.CODECOV_TOKEN }}
98 |
99 | check-types:
100 | name: Check types
101 | needs: [build]
102 | runs-on: ubuntu-latest
103 |
104 | steps:
105 | - uses: actions/checkout@v4
106 | with:
107 | fetch-depth: 0 # Needed for setuptools_scm
108 |
109 | - name: Set up Python 3.12
110 | uses: actions/setup-python@v5
111 | with:
112 | python-version: 3.12
113 |
114 | - name: Download package
115 | uses: actions/download-artifact@v4
116 | with:
117 | name: Packages
118 | path: dist
119 |
120 | - name: Install package and dependencies
121 | run: |
122 | python -m pip install uv
123 | uv pip install --system "$(ls dist/*.whl)[dev]"
124 |
125 | - name: Run type checker (mypy)
126 | run: |
127 | mypy .
128 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .coverage
2 | .eggs
3 | .DS_Store
4 | linopy/__pycache__
5 | test/__pycache__
6 | linopy.egg-info
7 | dev-scripts/
8 | examples/.ipynb_checkpoints/
9 | linopy/version.py
10 | dist/
11 | build/
12 | doc/_build
13 | doc/generated
14 | doc/api
15 | .vscode
16 | Highs.log
17 | paper/
18 | monkeytype.sqlite3
19 | .github/copilot-instructions.md
20 | uv.lock
21 |
22 | # Environments
23 | .env
24 | .venv
25 | env/
26 | venv/
27 | ENV/
28 | env.bak/
29 | venv.bak/
30 |
31 | benchmark/*.pdf
32 | benchmark/benchmarks
33 | benchmark/.snakemake
34 | benchmark/gurobi.log
35 | benchmark/notebooks/.ipynb_checkpoints
36 | benchmark/scripts/__pycache__
37 | benchmark/scripts/benchmarks-pypsa-eur/__pycache__
38 | benchmark/scripts/leftovers/
39 |
40 | # IDE
41 | .idea/
42 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | ci:
2 | autoupdate_schedule: quarterly
3 |
4 | repos:
5 | - repo: https://github.com/pre-commit/pre-commit-hooks
6 | rev: v5.0.0
7 | hooks:
8 | - id: end-of-file-fixer
9 | - id: trailing-whitespace
10 | - id: check-merge-conflict
11 | - repo: https://github.com/astral-sh/ruff-pre-commit
12 | rev: v0.11.8
13 | hooks:
14 | - id: ruff
15 | args: [--fix]
16 | - id: ruff-format
17 | # Current docformatter version has a problem with urls (it's annoying)
18 | # - repo: https://github.com/PyCQA/docformatter
19 | # rev: v1.6.0.rc1
20 | # hooks:
21 | # - id: docformatter
22 | # args: [--in-place, --make-summary-multi-line, --pre-summary-newline]
23 | - repo: https://github.com/keewis/blackdoc
24 | rev: v0.3.9
25 | hooks:
26 | - id: blackdoc
27 | - repo: https://github.com/codespell-project/codespell
28 | rev: v2.4.1
29 | hooks:
30 | - id: codespell
31 | types_or: [python, rst, markdown]
32 | files: ^(linopy|doc)/
33 | - repo: https://github.com/aflc/pre-commit-jupyter
34 | rev: v1.2.1
35 | hooks:
36 | - id: jupyter-notebook-cleanup
37 | exclude: examples/solve-on-remote.ipynb
38 |
--------------------------------------------------------------------------------
/.readthedocs.yaml:
--------------------------------------------------------------------------------
1 | version: 2
2 | sphinx:
3 | configuration: doc/conf.py
4 | build:
5 | os: ubuntu-24.04
6 | tools:
7 | python: "3.12"
8 | jobs:
9 | pre_system_dependencies:
10 | - git fetch --unshallow # Needed to get version tags
11 | python:
12 | install:
13 | - method: pip
14 | path: .
15 | extra_requirements:
16 | - docs
17 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | Linopy's contributor guidelines can be found in the official [documentation](https://linopy.readthedocs.io/en/latest/contributing.html).
2 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 |
2 | MIT License
3 |
4 | Copyright 2015-2021 PyPSA Developers
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
9 |
10 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
11 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # linopy: Optimization with array-like variables and constraints
2 |
3 | [](https://pypi.org/project/linopy/)
4 | [](LICENSE.txt)
5 | [](https://github.com/PyPSA/linopy/actions/workflows/test.yml)
6 | [](https://linopy.readthedocs.io/en/latest/)
7 | [](https://codecov.io/gh/PyPSA/linopy)
8 |
9 |
10 |
11 | **L**inear\
12 | **I**nteger\
13 | **N**on-linear\
14 | **O**ptimization in\
15 | **PY**thon
16 |
17 | **linopy** is an open-source python package that facilitates **optimization** with **real world data**. It builds a bridge between data analysis packages like [xarray](https://github.com/pydata/xarray) & [pandas](https://pandas.pydata.org/) and problem solvers like [cbc](https://projects.coin-or.org/Cbc), [gurobi](https://www.gurobi.com/) (see the full list below). **Linopy** supports **Linear, Integer, Mixed-Integer and Quadratic Programming** while aiming to make linear programming in Python easy, highly-flexible and performant.
18 |
19 |
20 |
21 | ## Benchmarks
22 |
23 | **linopy** is designed to be fast and efficient. The following benchmark compares the performance of **linopy** with the alternative popular optimization packages.
24 |
25 | 
26 |
27 |
28 | ## Main features
29 |
30 | **linopy** is heavily based on [xarray](https://github.com/pydata/xarray) which allows for many flexible data-handling features:
31 |
32 | * Define (arrays of) continuous or binary variables with **coordinates**, e.g. time, consumers, etc.
33 | * Apply **arithmetic operations** on the variables like adding, substracting, multiplying with all the **broadcasting** potentials of xarray
34 | * Apply **arithmetic operations** on the **linear expressions** (combination of variables)
35 | * **Group terms** of a linear expression by coordinates
36 | * Get insight into the **clear and transparent data model**
37 | * **Modify** and **delete** assigned variables and constraints on the fly
38 | * Use **lazy operations** for large linear programs with [dask](https://dask.org/)
39 | * Choose from **different commercial and non-commercial solvers**
40 | * Fast **import and export** a linear model using xarray's netcdf IO
41 |
42 |
43 | ## Installation
44 |
45 | So far **linopy** is available on the PyPI repository
46 |
47 | ```bash
48 | pip install linopy
49 | ```
50 |
51 | or on conda-forge
52 |
53 | ```bash
54 | conda install -c conda-forge linopy
55 | ```
56 |
57 | ## In a Nutshell
58 |
59 | Linopy aims to make optimization programs transparent and flexible. To illustrate its usage, let's consider a scenario where we aim to minimize the cost of buying apples and bananas over a week, subject to daily and weekly vitamin intake constraints.
60 |
61 |
62 | ```python
63 | >>> import pandas as pd
64 | >>> import linopy
65 |
66 | >>> m = linopy.Model()
67 |
68 | >>> days = pd.Index(['Mon', 'Tue', 'Wed', 'Thu', 'Fri'], name='day')
69 | >>> apples = m.add_variables(lower=0, name='apples', coords=[days])
70 | >>> bananas = m.add_variables(lower=0, name='bananas', coords=[days])
71 | >>> apples
72 | ```
73 | ```
74 | Variable (day: 5)
75 | -----------------
76 | [Mon]: apples[Mon] ∈ [0, inf]
77 | [Tue]: apples[Tue] ∈ [0, inf]
78 | [Wed]: apples[Wed] ∈ [0, inf]
79 | [Thu]: apples[Thu] ∈ [0, inf]
80 | [Fri]: apples[Fri] ∈ [0, inf]
81 | ```
82 |
83 | Add daily vitamin constraints
84 |
85 | ```python
86 | >>> m.add_constraints(3 * apples + 2 * bananas >= 8, name='daily_vitamins')
87 | ```
88 | ```
89 | Constraint `daily_vitamins` (day: 5):
90 | -------------------------------------
91 | [Mon]: +3 apples[Mon] + 2 bananas[Mon] ≥ 8
92 | [Tue]: +3 apples[Tue] + 2 bananas[Tue] ≥ 8
93 | [Wed]: +3 apples[Wed] + 2 bananas[Wed] ≥ 8
94 | [Thu]: +3 apples[Thu] + 2 bananas[Thu] ≥ 8
95 | [Fri]: +3 apples[Fri] + 2 bananas[Fri] ≥ 8
96 | ```
97 |
98 | Add weekly vitamin constraint
99 |
100 | ```python
101 | >>> m.add_constraints((3 * apples + 2 * bananas).sum() >= 50, name='weekly_vitamins')
102 | ```
103 | ```
104 | Constraint `weekly_vitamins`
105 | ----------------------------
106 | +3 apples[Mon] + 2 bananas[Mon] + 3 apples[Tue] ... +2 bananas[Thu] + 3 apples[Fri] + 2 bananas[Fri] ≥ 50
107 | ```
108 |
109 | Define the prices of apples and bananas and the objective function
110 |
111 | ```python
112 | >>> apple_price = [1, 1.5, 1, 2, 1]
113 | >>> banana_price = [1, 1, 0.5, 1, 0.5]
114 | >>> m.objective = apple_price * apples + banana_price * bananas
115 | ```
116 |
117 | Finally, we can solve the problem and get the optimal solution:
118 |
119 | ```python
120 | >>> m.solve()
121 | >>> m.objective.value
122 | ```
123 | ```
124 | 17.166
125 | ```
126 |
127 | ... and display the solution as a pandas DataFrame
128 | ```python
129 | >>> m.solution.to_pandas()
130 | ```
131 | ```
132 | apples bananas
133 | day
134 | Mon 2.667 0
135 | Tue 0 4
136 | Wed 0 9
137 | Thu 0 4
138 | Fri 0 4
139 | ```
140 | ## Supported solvers
141 |
142 | **linopy** supports the following solvers
143 |
144 | * [Cbc](https://projects.coin-or.org/Cbc)
145 | * [GLPK](https://www.gnu.org/software/glpk/)
146 | * [HiGHS](https://www.maths.ed.ac.uk/hall/HiGHS/)
147 | * [Gurobi](https://www.gurobi.com/)
148 | * [Xpress](https://www.fico.com/en/products/fico-xpress-solver)
149 | * [Cplex](https://www.ibm.com/de-de/analytics/cplex-optimizer)
150 | * [MOSEK](https://www.mosek.com/)
151 | * [COPT](https://www.shanshu.ai/copt)
152 |
153 | Note that these do have to be installed by the user separately.
154 |
155 | ## Development Setup
156 |
157 | To set up a local development environment for linopy and to run the same tests that are run in the CI, you can run:
158 |
159 | ```sh
160 | python -m venv venv
161 | source venv/bin/activate
162 | pip install uv
163 | uv pip install -e .[dev,solvers]
164 | pytest
165 | ```
166 |
167 | The `-e` flag of the install command installs the `linopy` package in editable mode, which means that the virtualenv (and thus the tests) will run the code from your local checkout.
168 |
169 | ## Citing Linopy
170 |
171 | If you use Linopy in your research, please cite the following paper:
172 |
173 | - Hofmann, F., (2023). Linopy: Linear optimization with n-dimensional labeled variables.
174 | Journal of Open Source Software, 8(84), 4823, [https://doi.org/10.21105/joss.04823](https://doi.org/10.21105/joss.04823)
175 |
176 | A BibTeX entry for LaTeX users is
177 |
178 | ```latex
179 | @article{Hofmann2023,
180 | doi = {10.21105/joss.04823},
181 | url = {https://doi.org/10.21105/joss.04823},
182 | year = {2023}, publisher = {The Open Journal},
183 | volume = {8},
184 | number = {84},
185 | pages = {4823},
186 | author = {Fabian Hofmann},
187 | title = {Linopy: Linear optimization with n-dimensional labeled variables},
188 | journal = {Journal of Open Source Software}
189 | }
190 | ```
191 |
192 |
193 | ## License
194 |
195 | Copyright 2021 Fabian Hofmann
196 |
197 |
198 |
199 | This package is published under MIT license. See [LICENSE.txt](LICENSE.txt) for details.
200 |
--------------------------------------------------------------------------------
/benchmark/README.md:
--------------------------------------------------------------------------------
1 | # Benchmark
2 |
3 | 
4 |
5 | This benchmark compares the performance of `linopy` against similar packages considering memory and time overhead for solving an optimization problem with N variables. The overhead is defined by the resources needed to define the problem, pass it to the solver and retrieve the solution. For the sake of reproducibility, this directory contains all necessary code in a Snakemake workflow. The workflow allows to choose between two different optimization problems to be used in the benchmarking:
6 |
7 | 1. A one-dimensional knapsack problem, a standard linear program following the formulation described in [here](https://www.wikiwand.com/en/Knapsack_problem). The figure above was created using this problem for benchmarking. The problem is solved for a range of different values `N` representing the number of variables.
8 |
9 | 2. The second problem choice `"basic"` is a simple linear program with the following formulation
10 |
∑i, j2xi, j + yi, j
11 |
12 | s.t.
13 |
14 | xi, j − yi, j ≥ i ∀ i, j ∈ {1, ..., N}
15 |
16 | xi, j + yi, j ≥ 0 ∀ i, j ∈ {1, ..., N}
17 |
18 | which is initialized and solved for different values of `N` with each of the API's.
19 |
20 |
21 | To run the benchmark, install the conda environment with
22 |
23 | ```bash
24 | conda env create -f environment.yaml
25 | conda activate linopy-benchmark
26 | ```
27 |
28 | Replace `environment.yaml` by `environment.fixed.yaml` if you want to use the fixed versions of the packages used for creating the figure above. Important package version are specified below.
29 | Make sure to have a working installation of `Julia` and `JuMP`, and optionally install the version stated below.
30 |
31 | Then, run the benchmark with
32 |
33 | ```bash
34 | snakemake --cores 4
35 | ```
36 | This will call `snakemake` with 4 cores and finally reproduce the figure above together with other figures in the directory `benchmark/`.
37 |
38 | ### Versions Specfications
39 |
40 | For creating the figure above, the following versions of the packages were used
41 |
42 | - python v3.9.0
43 | - cxvpy v1.3
44 | - gurobipy v10.0
45 | - pulp v2.7
46 | - Pyomo v6.4.4
47 | - ortools v9.5
48 | - linopy v0.1.3
49 | - julia v1.6.7
50 | - JuMP v1.1.1
51 |
52 | For a full list of all python packages see the `environment.fixed.yaml`. The benchmark was performed on a machine with the following specifications
53 |
54 | - CPU: AMD Ryzen 7 PRO 6850U with Radeon Graphics
55 | - RAM: 32 GB
56 | - OS: Ubuntu 22.04 LTS
57 |
--------------------------------------------------------------------------------
/benchmark/benchmark-absolute.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark-absolute.pdf
--------------------------------------------------------------------------------
/benchmark/benchmark-absolute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark-absolute.png
--------------------------------------------------------------------------------
/benchmark/benchmark-overhead.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark-overhead.pdf
--------------------------------------------------------------------------------
/benchmark/benchmark-overhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark-overhead.png
--------------------------------------------------------------------------------
/benchmark/benchmark_resource-absolute.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark_resource-absolute.pdf
--------------------------------------------------------------------------------
/benchmark/benchmark_resource-absolute.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark_resource-absolute.png
--------------------------------------------------------------------------------
/benchmark/benchmark_resource-overhead.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark_resource-overhead.pdf
--------------------------------------------------------------------------------
/benchmark/benchmark_resource-overhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/benchmark_resource-overhead.png
--------------------------------------------------------------------------------
/benchmark/config.yaml:
--------------------------------------------------------------------------------
1 |
2 | benchmark: 'knapsack'
3 |
4 | solver: gurobi
5 |
6 | basic:
7 | name: basic
8 | nrange: [20, 50, 100, 180, 400, 600, 900, 1200]
9 | apis: ['linopy', 'jump', 'pyomo', 'cvxpy', 'pulp', 'gurobipy', 'solver']
10 |
11 | knapsack:
12 | name: knapsack
13 | nrange: [1000, 100000, 200000, 400000, 1000000]
14 | apis: ['linopy', 'jump', 'pyomo', 'cvxpy', 'pulp', 'gurobipy', 'solver']
15 |
--------------------------------------------------------------------------------
/benchmark/environment.yaml:
--------------------------------------------------------------------------------
1 | name: 'linopy-benchmark'
2 | channels:
3 | - conda-forge
4 | - gurobi
5 | - bioconda
6 | dependencies:
7 | - python==3.9
8 | - pip
9 | - linopy
10 | - gurobi
11 | - ortools-python
12 | - pyomo
13 | - pulp
14 | - seaborn
15 | - matplotlib
16 | - snakemake
17 | - memory_profiler
18 | - jupyter
19 | - cvxpy
20 |
21 | - pip:
22 | - ortools>=9.5
23 |
--------------------------------------------------------------------------------
/benchmark/notebooks/plot-benchmarks.py.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "code",
5 | "execution_count": null,
6 | "id": "9a85db47",
7 | "metadata": {},
8 | "outputs": [],
9 | "source": [
10 | "import matplotlib.pyplot as plt\n",
11 | "import pandas as pd\n",
12 | "import seaborn as sns\n",
13 | "\n",
14 | "sns.set_theme(\"paper\", \"white\")\n",
15 | "# plt.rc('text', usetex=True)\n",
16 | "# plt.rc('font', family='sans-serif')"
17 | ]
18 | },
19 | {
20 | "cell_type": "code",
21 | "execution_count": null,
22 | "id": "709bdf49",
23 | "metadata": {},
24 | "outputs": [],
25 | "source": [
26 | "data = pd.read_csv(snakemake.input[0], index_col=0)\n",
27 | "cols = [\"Time\", \"Memory\"]\n",
28 | "df = data.melt(id_vars=data.columns.drop(cols), value_vars=cols, var_name=\"kind\")"
29 | ]
30 | },
31 | {
32 | "cell_type": "code",
33 | "execution_count": null,
34 | "id": "f36897fb",
35 | "metadata": {},
36 | "outputs": [],
37 | "source": [
38 | "if snakemake.wildcards[\"kind\"] == \"overhead\":\n",
39 | " labels = [\"Overhead time (s)\", \"Overhead memory (MB)\"]\n",
40 | "else:\n",
41 | " labels = [\"Time (s)\", \"Memory (MB)\"]\n",
42 | "\n",
43 | "g = sns.FacetGrid(data=df, row=\"kind\", sharey=False, height=2.0, aspect=2)\n",
44 | "g.map_dataframe(\n",
45 | " sns.lineplot,\n",
46 | " x=\"Number of Variables\",\n",
47 | " y=\"value\",\n",
48 | " hue=\"API\",\n",
49 | " style=\"API\",\n",
50 | " marker=\".\",\n",
51 | " legend=\"full\",\n",
52 | " zorder=8,\n",
53 | ")\n",
54 | "for ax, label in zip(g.axes.ravel(), labels):\n",
55 | " ax.set_ylabel(label)\n",
56 | " ax.set_title(\"\")\n",
57 | " ax.grid(axis=\"y\", lw=0.2, color=\"grey\", zorder=3, alpha=0.4)\n",
58 | "g.fig.tight_layout()\n",
59 | "g.add_legend()\n",
60 | "g.fig.savefig(\n",
61 | " snakemake.output.time_memory, bbox_inches=\"tight\", pad_inches=0.1, dpi=300\n",
62 | ")"
63 | ]
64 | },
65 | {
66 | "cell_type": "code",
67 | "execution_count": null,
68 | "id": "c5c93666",
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "if snakemake.wildcards[\"kind\"] == \"overhead\":\n",
73 | " label = \"Computational overhead [MBs]\"\n",
74 | "else:\n",
75 | " label = \"Computational resource [MBs]\"\n",
76 | "\n",
77 | "df = data.assign(Resource=data[\"Time\"] * data[\"Memory\"])\n",
78 | "cols = [\"Resource\"]\n",
79 | "df = df.melt(id_vars=df.columns.drop(cols), value_vars=cols, var_name=\"kind\")\n",
80 | "\n",
81 | "fig, ax = plt.subplots(figsize=(6, 3))\n",
82 | "sns.lineplot(\n",
83 | " data=df,\n",
84 | " x=\"Number of Variables\",\n",
85 | " y=\"value\",\n",
86 | " hue=\"API\",\n",
87 | " style=\"API\",\n",
88 | " marker=\".\",\n",
89 | " legend=\"full\",\n",
90 | " zorder=8,\n",
91 | ")\n",
92 | "sns.despine()\n",
93 | "ax.set_ylabel(label)\n",
94 | "ax.set_title(\"\")\n",
95 | "plt.ticklabel_format(axis=\"both\", style=\"sci\", scilimits=(3, 3))\n",
96 | "ax.grid(axis=\"y\", lw=0.2, color=\"grey\", zorder=3, alpha=0.4)\n",
97 | "fig.tight_layout()\n",
98 | "fig.savefig(snakemake.output.resource, bbox_inches=\"tight\", pad_inches=0.1, dpi=300)"
99 | ]
100 | }
101 | ],
102 | "metadata": {
103 | "kernelspec": {
104 | "display_name": "Python 3",
105 | "language": "python",
106 | "name": "python3"
107 | },
108 | "language_info": {
109 | "codemirror_mode": {
110 | "name": "ipython",
111 | "version": 3
112 | },
113 | "file_extension": ".py",
114 | "mimetype": "text/x-python",
115 | "name": "python",
116 | "nbconvert_exporter": "python",
117 | "pygments_lexer": "ipython3",
118 | "version": "3.10.9 | packaged by conda-forge | (main, Feb 2 2023, 20:20:04) [GCC 11.3.0]"
119 | },
120 | "vscode": {
121 | "interpreter": {
122 | "hash": "ed88634a96f0f44ddf87d723a7b512fbeabb17521926a161ee96c50fffea2b11"
123 | }
124 | }
125 | },
126 | "nbformat": 4,
127 | "nbformat_minor": 5
128 | }
129 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_cvxpy.py:
--------------------------------------------------------------------------------
1 | import cvxpy as cp
2 | import numpy as np
3 | from common import profile
4 | from numpy.random import default_rng
5 |
6 | # Random seed for reproducibility
7 | rng = default_rng(125)
8 |
9 |
10 | def basic_model(n, solver):
11 | # Create variables
12 | x = cp.Variable((n, n))
13 | y = cp.Variable((n, n))
14 |
15 | constraints = [x - y >= np.repeat(np.arange(n)[:, np.newaxis], n, 1), x + y >= 0]
16 |
17 | # Create objective
18 | objective = cp.Minimize(2 * cp.sum(x) + cp.sum(y))
19 |
20 | # Optimize the model
21 | m = cp.Problem(objective, constraints)
22 | m.solve(solver=solver.upper())
23 |
24 | return m.value
25 |
26 |
27 | def knapsack_model(n, solver):
28 | # Define the variables
29 | weight = rng.integers(1, 100, size=n)
30 | value = rng.integers(1, 100, size=n)
31 |
32 | x = cp.Variable(n, boolean=True)
33 |
34 | # Define the constraints
35 | constraints = [weight @ x <= 200]
36 |
37 | # Define the objective function
38 | objective = cp.Maximize(value @ x)
39 |
40 | # Optimize the model
41 | m = cp.Problem(objective, constraints)
42 | m.solve(solver=solver.upper())
43 |
44 | # return objective
45 | return m.value
46 |
47 |
48 | if __name__ == "__main__":
49 | solver = snakemake.config["solver"]
50 |
51 | if snakemake.config["benchmark"] == "basic":
52 | model = basic_model
53 | elif snakemake.config["benchmark"] == "knapsack":
54 | model = knapsack_model
55 |
56 | # dry run first
57 | model(2, solver)
58 |
59 | res = profile(snakemake.params.nrange, model, solver)
60 | res["API"] = "cvxpy"
61 | res = res.rename_axis("N").reset_index()
62 |
63 | res.to_csv(snakemake.output[0])
64 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_gurobipy.py:
--------------------------------------------------------------------------------
1 | import gurobipy as gp
2 | import numpy as np
3 | from common import profile
4 | from numpy.random import default_rng
5 |
6 | # Random seed for reproducibility
7 | rng = default_rng(125)
8 |
9 |
10 | def basic_model(n, solver):
11 | # Create a new model
12 | m = gp.Model()
13 |
14 | # Create variables
15 | x = m.addMVar((n, n), lb=-gp.GRB.INFINITY, ub=gp.GRB.INFINITY, name="x")
16 | y = m.addMVar((n, n), lb=-gp.GRB.INFINITY, ub=gp.GRB.INFINITY, name="y")
17 |
18 | m.addConstr(x - y >= np.arange(n))
19 | m.addConstr(x + y >= 0)
20 |
21 | # Create objective
22 | obj = gp.quicksum(gp.quicksum(2 * x + y))
23 | m.setObjective(obj, sense=gp.GRB.MINIMIZE)
24 |
25 | # Optimize the model
26 | m.optimize()
27 |
28 | return m.ObjVal
29 |
30 |
31 | def knapsack_model(n, solver):
32 | # Create a new model
33 | m = gp.Model()
34 |
35 | weight = rng.integers(1, 100, size=n)
36 | value = rng.integers(1, 100, size=n)
37 |
38 | # Create variables
39 | x = m.addMVar(n, vtype=gp.GRB.BINARY, name="x")
40 |
41 | # Create constraints
42 | m.addConstr(weight @ x <= 200)
43 |
44 | # Create objective
45 | obj = value @ x
46 | m.setObjective(obj, sense=gp.GRB.MAXIMIZE)
47 |
48 | # Optimize the model
49 | m.optimize()
50 |
51 | return m.ObjVal
52 |
53 |
54 | if __name__ == "__main__":
55 | solver = snakemake.config["solver"]
56 |
57 | if snakemake.config["benchmark"] == "basic":
58 | model = basic_model
59 | elif snakemake.config["benchmark"] == "knapsack":
60 | model = knapsack_model
61 |
62 | # dry run first
63 | model(2, None)
64 |
65 | res = profile(snakemake.params.nrange, model, solver)
66 | res["API"] = "gurobipy"
67 | res = res.rename_axis("N").reset_index()
68 |
69 | res.to_csv(snakemake.output[0])
70 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_jump.jl:
--------------------------------------------------------------------------------
1 | using JuMP
2 | using Gurobi
3 | using DataFrames
4 | using CSV
5 | using Dates
6 | using Random
7 | Random.seed!(125)
8 |
9 | function basic_model(n, solver)
10 | m = Model(solver)
11 | @variable(m, x[1:n, 1:n])
12 | @variable(m, y[1:n, 1:n])
13 | @constraint(m, x - y .>= 0:(n-1))
14 | @constraint(m, x + y .>= 0)
15 | @objective(m, Min, 2 * sum(x) + sum(y))
16 | optimize!(m)
17 | return objective_value(m)
18 | end
19 |
20 | function knapsack_model(n, solver)
21 | m = Model(solver)
22 | @variable(m, x[1:n], Bin)
23 | weight = rand(1:100, n)
24 | value = rand(1:100, n)
25 | @constraint(m, weight' * x <= 200)
26 | @objective(m, Max, value' * x)
27 | optimize!(m)
28 | return objective_value(m)
29 | end
30 |
31 |
32 | if snakemake.config["solver"] == "gurobi"
33 | solver = Gurobi.Optimizer
34 | elseif snakemake.config["solver"] == "cbc"
35 | using Cbc
36 | solver = Cbc.Optimizer
37 | end
38 |
39 | if snakemake.config["benchmark"] == "basic"
40 | model = basic_model
41 | elseif snakemake.config["benchmark"] == "knapsack"
42 | model = knapsack_model
43 | end
44 |
45 | # jit compile everything
46 | model(1, solver)
47 |
48 | profile = DataFrame(N=Int[], Time=Float64[], Memory=Float64[], Objective=Float64[])
49 |
50 | for N in snakemake.params[1]
51 | mem = @allocated(model(N, solver))/10^6
52 | time = @elapsed(model(N, solver))
53 | objective = model(N, solver)
54 | push!(profile, [N, time, mem, objective])
55 | end
56 | profile[!, :API] .= "jump"
57 | insertcols!(profile, 1, :Row => 1:nrow(profile))
58 |
59 | CSV.write(snakemake.output[1], profile)
60 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_linopy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Nov 19 17:40:33 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | from common import profile
9 | from numpy import arange
10 | from numpy.random import default_rng
11 |
12 | from linopy import Model
13 |
14 | # Random seed for reproducibility
15 | rng = default_rng(125)
16 |
17 |
18 | def basic_model(n, solver):
19 | m = Model()
20 | N, M = [arange(n), arange(n)]
21 | x = m.add_variables(coords=[N, M])
22 | y = m.add_variables(coords=[N, M])
23 | m.add_constraints(x - y >= N)
24 | m.add_constraints(x + y >= 0)
25 | m.add_objective(2 * x.sum() + y.sum())
26 | # m.to_file(f"linopy-model.lp")
27 | m.solve(solver)
28 | return m.objective.value
29 |
30 |
31 | def knapsack_model(n, solver):
32 | m = Model()
33 | packages = m.add_variables(coords=[arange(n)], binary=True)
34 | weight = rng.integers(1, 100, size=n)
35 | value = rng.integers(1, 100, size=n)
36 | m.add_constraints((weight * packages).sum() <= 200)
37 | m.add_objective(-(value * packages).sum()) # use minus because of minimization
38 | m.solve(solver_name=solver)
39 | return -m.objective.value
40 |
41 |
42 | if __name__ == "__main__":
43 | solver = snakemake.config["solver"]
44 |
45 | if snakemake.config["benchmark"] == "basic":
46 | model = basic_model
47 | elif snakemake.config["benchmark"] == "knapsack":
48 | model = knapsack_model
49 |
50 | # dry run first
51 | model(2, solver)
52 |
53 | res = profile(snakemake.params.nrange, model, solver)
54 | res["API"] = "linopy"
55 | res = res.rename_axis("N").reset_index()
56 |
57 | res.to_csv(snakemake.output[0])
58 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_ortools.py:
--------------------------------------------------------------------------------
1 | from common import profile
2 | from numpy.random import default_rng
3 | from ortools.linear_solver import pywraplp
4 |
5 | # Random seed for reproducibility
6 | rng = default_rng(125)
7 |
8 |
9 | def model(n, solver):
10 | # Create a new linear solver
11 | solver = pywraplp.Solver("LinearExample", pywraplp.Solver.GUROBI_LINEAR_PROGRAMMING)
12 |
13 | # Create variables
14 | x = {}
15 | y = {}
16 | for i in range(n):
17 | for j in range(n):
18 | x[i, j] = solver.NumVar(lb=None, ub=None, name=f"x_{i}_{j}")
19 | y[i, j] = solver.NumVar(lb=None, ub=None, name=f"y_{i}_{j}")
20 |
21 | # Create constraints
22 | for i in range(n):
23 | for j in range(n):
24 | solver.Add(x[i, j] - y[i, j] >= i)
25 | solver.Add(x[i, j] + y[i, j] >= 0)
26 |
27 | # Create objective
28 | obj = solver.Objective()
29 | for i in range(n):
30 | for j in range(n):
31 | obj.Add(2 * x[i, j] + y[i, j])
32 | obj.SetMinimization()
33 |
34 | # Solve the model
35 | solver.Solve()
36 |
37 | return solver
38 |
39 |
40 | def knapsack_model(n, solver):
41 | # Create a new linear solver
42 | solver = pywraplp.Solver("LinearExample", pywraplp.Solver.GUROBI_LINEAR_PROGRAMMING)
43 |
44 | weight = rng.integers(1, 100, size=n)
45 | value = rng.integers(1, 100, size=n)
46 |
47 | x = {i: solver.BoolVar(f"x_{i}") for i in range(n)}
48 | # Create constraints
49 | solver.Add(solver.Sum([weight[i] * x[i] for i in range(n)]) <= 200)
50 |
51 | # Create objective
52 | obj = solver.Objective()
53 | for i in range(n):
54 | obj.Add(value[i] * x[i])
55 | obj.SetMaximization()
56 |
57 | # Solve the model
58 | solver.Solve()
59 |
60 | return solver
61 |
62 |
63 | if snakemake.config["benchmark"] == "basic":
64 | model = basic_model
65 | elif snakemake.config["benchmark"] == "knapsack":
66 | model = knapsack_model
67 |
68 |
69 | if __name__ == "__main__":
70 | solver = snakemake.config["solver"]
71 |
72 | # dry run first
73 | model(2, solver)
74 |
75 | res = profile(snakemake.params.nrange, model, solver)
76 | res["API"] = "ortools"
77 | res = res.rename_axis("N").reset_index()
78 |
79 | res.to_csv(snakemake.output[0])
80 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_pulp.py:
--------------------------------------------------------------------------------
1 | import pulp
2 | from common import profile
3 | from numpy.random import default_rng
4 |
5 | # Random seed for reproducibility
6 | rng = default_rng(125)
7 |
8 |
9 | def basic_model(n, solver):
10 | m = pulp.LpProblem("Model", pulp.LpMinimize)
11 |
12 | m.i = list(range(n))
13 | m.j = list(range(n))
14 |
15 | x = pulp.LpVariable.dicts("x", (m.i, m.j), lowBound=None, upBound=None)
16 | y = pulp.LpVariable.dicts("y", (m.i, m.j), lowBound=None, upBound=None)
17 |
18 | for i in m.i:
19 | for j in m.j:
20 | m += x[i][j] - y[i][j] >= i
21 | m += x[i][j] + y[i][j] >= 0
22 | m += pulp.lpSum(2 * x[i][j] + y[i][j] for i in m.i for j in m.j)
23 |
24 | solver = pulp.getSolver(solver.upper())
25 | m.solve(solver)
26 | return pulp.value(m.objective)
27 |
28 |
29 | def knapsack_model(n, solver):
30 | # Define the problem
31 | m = pulp.LpProblem("Knapsack Problem", pulp.LpMaximize)
32 |
33 | m.i = list(range(n))
34 |
35 | # Define the variables
36 | weight = rng.integers(1, 100, size=n)
37 | value = rng.integers(1, 100, size=n)
38 |
39 | x = pulp.LpVariable.dicts("x", (m.i,), lowBound=0, upBound=1, cat=pulp.LpInteger)
40 |
41 | # Define the constraints
42 | m += pulp.lpSum([weight[i] * x[i] for i in m.i]) <= 200
43 |
44 | # Define the objective function
45 | m += pulp.lpSum([value[i] * x[i] for i in m.i])
46 |
47 | # Solve the problem
48 | solver = pulp.getSolver(solver.upper())
49 | m.solve(solver)
50 |
51 | return pulp.value(m.objective)
52 |
53 |
54 | if __name__ == "__main__":
55 | solver = snakemake.config["solver"]
56 |
57 | if snakemake.config["benchmark"] == "basic":
58 | model = basic_model
59 | elif snakemake.config["benchmark"] == "knapsack":
60 | model = knapsack_model
61 |
62 | # dry run first
63 | model(2, solver)
64 |
65 | res = profile(snakemake.params.nrange, model, solver)
66 | res["API"] = "pulp"
67 | res = res.rename_axis("N").reset_index()
68 |
69 | res.to_csv(snakemake.output[0])
70 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmark_pyomo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Nov 19 17:40:33 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | from common import profile
9 | from numpy import arange
10 | from numpy.random import default_rng
11 | from pyomo.environ import (
12 | Binary,
13 | ConcreteModel,
14 | Constraint,
15 | Objective,
16 | Set,
17 | Var,
18 | maximize,
19 | )
20 | from pyomo.opt import SolverFactory
21 |
22 | # Random seed for reproducibility
23 | rng = default_rng(125)
24 |
25 |
26 | def basic_model(n, solver):
27 | m = ConcreteModel()
28 | m.i = Set(initialize=arange(n))
29 | m.j = Set(initialize=arange(n))
30 |
31 | m.x = Var(m.i, m.j, bounds=(None, None))
32 | m.y = Var(m.i, m.j, bounds=(None, None))
33 |
34 | def bound1(m, i, j):
35 | return m.x[(i, j)] - m.y[(i, j)] >= i
36 |
37 | def bound2(m, i, j):
38 | return m.x[(i, j)] + m.y[(i, j)] >= 0
39 |
40 | def objective(m):
41 | return sum(2 * m.x[(i, j)] + m.y[(i, j)] for i in m.i for j in m.j)
42 |
43 | m.con1 = Constraint(m.i, m.j, rule=bound1)
44 | m.con2 = Constraint(m.i, m.j, rule=bound2)
45 | m.obj = Objective(rule=objective)
46 |
47 | opt = SolverFactory(solver)
48 | opt.solve(m)
49 | return m.obj()
50 |
51 |
52 | def knapsack_model(n, solver):
53 | m = ConcreteModel()
54 | m.i = Set(initialize=arange(n))
55 |
56 | m.x = Var(m.i, domain=Binary)
57 | m.weight = rng.integers(1, 100, size=n)
58 | m.value = rng.integers(1, 100, size=n)
59 |
60 | def bound1(m):
61 | return sum(m.x[i] * m.weight[i] for i in m.i) <= 200
62 |
63 | def objective(m):
64 | return sum(m.x[i] * m.value[i] for i in m.i)
65 |
66 | m.con1 = Constraint(rule=bound1)
67 | m.obj = Objective(rule=objective, sense=maximize)
68 |
69 | opt = SolverFactory(solver)
70 | opt.solve(m)
71 | return m.obj()
72 |
73 |
74 | if __name__ == "__main__":
75 | solver = snakemake.config["solver"]
76 |
77 | if snakemake.config["benchmark"] == "basic":
78 | model = basic_model
79 | elif snakemake.config["benchmark"] == "knapsack":
80 | model = knapsack_model
81 |
82 | # dry run first
83 | model(2, solver)
84 |
85 | res = profile(snakemake.params.nrange, model, solver)
86 | res["API"] = "pyomo"
87 | res = res.rename_axis("N").reset_index()
88 |
89 | res.to_csv(snakemake.output[0])
90 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmarks-pypsa-eur/benchmark-linopy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | n.optimize(solver_name=SOLVER, **SOLVER_PARAMS)
21 |
22 |
23 | solve()
24 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmarks-pypsa-eur/benchmark-pyomo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | m = n.lopf( # noqa: F841
21 | solver_options=SOLVER_PARAMS, formulation="kirchhoff", solver_name=SOLVER
22 | )
23 |
24 |
25 | solve()
26 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmarks-pypsa-eur/benchmark-pypsa-linopf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | m = n.lopf(solver_options=SOLVER_PARAMS, pyomo=False, solver_name=SOLVER) # noqa: F841
21 |
22 |
23 | solve()
24 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmarks-pypsa-eur/common.py:
--------------------------------------------------------------------------------
1 | PATH = "/home/fabian/vres/py/pypsa-eur/networks/elec_s_37.nc"
2 | NSNAPSHOTS = -1
3 | SOLVER = "gurobi"
4 | SOLVER_PARAMS = {
5 | "crossover": 0,
6 | "method": 2,
7 | "BarConvTol": 1.0e-3,
8 | "FeasibilityTol": 1.0e-3,
9 | }
10 | # SOLVER = "cbc"
11 | # SOLVER_PARAMS = {}
12 |
--------------------------------------------------------------------------------
/benchmark/scripts/benchmarks-pypsa-eur/plot-benchmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 17:11:01 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import matplotlib.pyplot as plt
9 | import pandas as pd
10 | import seaborn as sns
11 |
12 | dfs = []
13 | for api, path in zip(snakemake.input.keys(), snakemake.input):
14 | df = pd.read_csv(path, skiprows=1, header=None, sep=" ")
15 |
16 | df.columns = ["API", "Memory", "Time"]
17 | df.API = api
18 | df.Time -= df.Time[0]
19 | dfs.append(df)
20 |
21 | df = pd.concat(dfs, ignore_index=True)
22 |
23 | fig, ax = plt.subplots(figsize=(8, 6))
24 | sns.lineplot(data=df, y="Memory", x="Time", hue="API", style="API", ax=ax)
25 | ax.set_xlabel("Time [s]")
26 | ax.set_ylabel("Memory Usage [MB]")
27 | # ax.set_xlim()
28 | fig.tight_layout()
29 | fig.savefig(snakemake.output[0])
30 |
--------------------------------------------------------------------------------
/benchmark/scripts/common.py:
--------------------------------------------------------------------------------
1 | import gc
2 | from time import time
3 |
4 | import pandas as pd
5 |
6 | # from memory_profiler import memory_usage
7 |
8 |
9 | def profile(nrange, func, *args):
10 | res = pd.DataFrame(index=nrange, columns=["Time", "Memory", "Objective"])
11 |
12 | for N in res.index:
13 | start = time()
14 |
15 | objective = func(N, *args)
16 |
17 | end = time()
18 | duration = end - start
19 |
20 | res.loc[N, "Time"] = duration
21 | res.loc[N, "Objective"] = objective
22 |
23 | gc.collect()
24 |
25 | return res
26 |
--------------------------------------------------------------------------------
/benchmark/scripts/concat-benchmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Wed Feb 9 09:34:00 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | from pathlib import Path
9 |
10 | import pandas as pd
11 |
12 | dfs = [pd.read_csv(fn, sep="\t") for fn in snakemake.input.memory]
13 | df = pd.concat(dfs, axis=0, ignore_index=True)
14 | df["N"] = snakemake.params.nrange
15 |
16 | df = df.rename(columns={"s": "Time", "max_rss": "Memory"})
17 | df = df.replace("-", 0)
18 | df["Memory"] = df["Memory"].astype(float)
19 |
20 | if snakemake.params.api == "solver":
21 | df["API"] = "Solving Process"
22 | else:
23 | df["API"] = snakemake.params.api
24 |
25 | df = df[["N", "Time", "Memory", "API"]]
26 |
27 | benchmark_time = snakemake.input.time
28 | if benchmark_time is not None:
29 | if isinstance(benchmark_time, str):
30 | df_time = pd.read_csv(benchmark_time, index_col=0)
31 | df["Time"] = df_time.Time.values
32 | df["Objective"] = df_time.Objective.values
33 | else:
34 | # for solvers we need to read the time from the single output files
35 | df["Time"] = [float(Path(fn).read_text()) for fn in benchmark_time]
36 |
37 |
38 | df.to_csv(snakemake.output.benchmark)
39 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmark-linopy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Nov 19 17:40:33 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | from common import profile
9 | from numpy import arange
10 |
11 | from linopy import Model
12 |
13 | SOLVER = snakemake.wildcards.solver
14 |
15 |
16 | def model(N):
17 | m = Model()
18 | coords = [arange(N), arange(N)]
19 | x = m.add_variables(coords=coords)
20 | y = m.add_variables(coords=coords)
21 | m.add_constraints(x - y >= arange(N))
22 | m.add_constraints(x + y >= 0)
23 | m.add_objective((2 * x).sum() + y.sum())
24 | m.solve(SOLVER)
25 | return
26 |
27 |
28 | res = profile(snakemake.params.nrange, model)
29 | res["API"] = "linopy"
30 | res = res.rename_axis("N").reset_index()
31 |
32 | res.to_csv(snakemake.output[0])
33 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-linopy.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-linopy.cpython-310.pyc
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-pyomo.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-pyomo.cpython-310.pyc
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-pypsa-linopf.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/benchmark-pypsa-linopf.cpython-310.pyc
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/common.cpython-39.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/common.cpython-39.pyc
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/plot-benchmarks.cpython-310.pyc:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/__pycache__/plot-benchmarks.cpython-310.pyc
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-linopy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | n.optimize(solver_name=SOLVER, **SOLVER_PARAMS)
21 |
22 |
23 | solve()
24 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pyomo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | m = n.lopf( # noqa: F841
21 | solver_options=SOLVER_PARAMS, formulation="kirchhoff", solver_name=SOLVER
22 | )
23 |
24 |
25 | solve()
26 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pypsa-eur-with-pypsa.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pypsa-eur-with-pypsa.pdf
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pypsa-eur.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pypsa-eur.pdf
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/benchmark-pypsa-linopf.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 16:20:51 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pypsa
10 | from common import NSNAPSHOTS, PATH, SOLVER, SOLVER_PARAMS
11 | from memory_profiler import profile
12 |
13 |
14 | @profile
15 | def solve():
16 | n = pypsa.Network(PATH)
17 | n.generators.p_nom_max.fillna(np.inf, inplace=True)
18 | n.snapshots = n.snapshots[:NSNAPSHOTS]
19 |
20 | m = n.lopf(solver_options=SOLVER_PARAMS, pyomo=False, solver_name=SOLVER) # noqa: F841
21 |
22 |
23 | solve()
24 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/common.py:
--------------------------------------------------------------------------------
1 | PATH = "/home/fabian/vres/py/pypsa-eur/networks/elec_s_37.nc"
2 | NSNAPSHOTS = -1
3 | SOLVER = "gurobi"
4 | SOLVER_PARAMS = {
5 | "crossover": 0,
6 | "method": 2,
7 | "BarConvTol": 1.0e-3,
8 | "FeasibilityTol": 1.0e-3,
9 | }
10 | # SOLVER = "cbc"
11 | # SOLVER_PARAMS = {}
12 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/benchmarks-pypsa-eur/plot-benchmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Feb 15 17:11:01 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import matplotlib.pyplot as plt
9 | import pandas as pd
10 | import seaborn as sns
11 |
12 | dfs = []
13 | for api, path in zip(snakemake.input.keys(), snakemake.input):
14 | df = pd.read_csv(path, skiprows=1, header=None, sep=" ")
15 |
16 | df.columns = ["API", "Memory", "Time"]
17 | df.API = api
18 | df.Time -= df.Time[0]
19 | dfs.append(df)
20 |
21 | df = pd.concat(dfs, ignore_index=True)
22 |
23 | fig, ax = plt.subplots(figsize=(8, 6))
24 | sns.lineplot(data=df, y="Memory", x="Time", hue="API", style="API", ax=ax)
25 | ax.set_xlabel("Time [s]")
26 | ax.set_ylabel("Memory Usage [MB]")
27 | # ax.set_xlim()
28 | fig.tight_layout()
29 | fig.savefig(snakemake.output[0])
30 |
--------------------------------------------------------------------------------
/benchmark/scripts/leftovers/common.py:
--------------------------------------------------------------------------------
1 | import tracemalloc
2 | from subprocess import PIPE
3 | from time import sleep, time
4 |
5 | import pandas as pd
6 | import psutil as ps
7 | from memory_profiler import memory_usage
8 |
9 |
10 | def profile(nrange, func):
11 | res = pd.DataFrame(index=nrange, columns=["Time [s]", "Memory Usage"])
12 |
13 | for N in res.index:
14 | start = time()
15 |
16 | func(N)
17 |
18 | end = time()
19 | duration = end - start
20 |
21 | memory = memory_usage((func, (N,)))
22 |
23 | res.loc[N] = duration, max(memory)
24 |
25 | return res
26 |
27 |
28 | def profile_shell(nrange, cmd):
29 | res = pd.DataFrame(index=nrange, columns=["Time [s]", "Memory Usage"])
30 |
31 | for N in res.index:
32 | tracemalloc.start()
33 | start = time()
34 |
35 | process = ps.Popen(cmd(N).split(), stdout=PIPE)
36 |
37 | peak_mem = 0
38 | peak_cpu = 0
39 |
40 | # while the process is running calculate resource utilization.
41 | while process.is_running():
42 | # set the sleep time to monitor at an interval of every second.
43 | sleep(0.01)
44 |
45 | # capture the memory and cpu utilization at an instance
46 | mem = process.memory_info().rss
47 | cpu = process.cpu_percent()
48 |
49 | # track the peak utilization of the process
50 | if mem > peak_mem:
51 | peak_mem = mem
52 | if cpu > peak_cpu:
53 | peak_cpu = cpu
54 | if mem == 0.0:
55 | break
56 |
57 | end = time()
58 | duration = end - start
59 |
60 | res.loc[N] = duration, peak_mem
61 |
62 | return res
63 |
--------------------------------------------------------------------------------
/benchmark/scripts/merge-benchmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Mon Feb 14 18:00:44 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import pandas as pd
9 |
10 | df = [pd.read_csv(fn, index_col=0) for fn in snakemake.input.benchmarks]
11 | df = pd.concat(df, ignore_index=True)
12 |
13 | if snakemake.config["benchmark"] == "basic":
14 | df["Number of Variables"] = df.N**2 * 2
15 | df["Number of Constraints"] = df.N**2 * 2
16 | elif snakemake.config["benchmark"] == "knapsack":
17 | df["Number of Variables"] = df.N
18 | df["Number of Constraints"] = df.N
19 |
20 | solver_memory = df.loc[df.API == "Solving Process", "Memory"].values
21 | solver_time = df.loc[df.API == "Solving Process", "Time"].values
22 |
23 | # Make a correction of the memory usage, some APIs use external processes for the solving process
24 | api_with_external_process = {"pyomo"}
25 | api_with_internal_process = set(snakemake.params.apis).difference(
26 | api_with_external_process
27 | )
28 |
29 |
30 | absolute = df.copy()
31 | for api in api_with_external_process:
32 | absolute.loc[absolute.API == api, "Memory"] += solver_memory
33 | absolute.to_csv(snakemake.output.absolute)
34 |
35 | overhead = df.copy()
36 | for api in snakemake.params.apis:
37 | overhead.loc[overhead.API == api, "Time"] -= solver_time
38 | for api in api_with_internal_process:
39 | overhead.loc[overhead.API == api, "Memory"] -= solver_memory
40 | overhead = overhead.query("API != 'Solving Process'")
41 | overhead.to_csv(snakemake.output.overhead)
42 |
--------------------------------------------------------------------------------
/benchmark/scripts/plot-benchmarks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Wed Jan 26 23:37:38 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import matplotlib.pyplot as plt
9 | import pandas as pd
10 | import seaborn as sns
11 |
12 | df = [pd.read_csv(fn) for fn in snakemake.input]
13 | df = pd.concat(df, ignore_index=True)
14 |
15 | df["# Variables"] = df.N**2 * 2
16 |
17 | fig, ax = plt.subplots()
18 | sns.lineplot(x="# Variables", y="Time [s]", hue="API", data=df, ax=ax)
19 | fig.tight_layout()
20 | fig.savefig(snakemake.output.time)
21 |
22 | fig, ax = plt.subplots()
23 | sns.lineplot(x="# Variables", y="Memory Usage", hue="API", data=df, ax=ax)
24 | fig.tight_layout()
25 | fig.savefig(snakemake.output.memory)
26 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-cvxpy.py:
--------------------------------------------------------------------------------
1 | from benchmark_gurobipy import basic_model, knapsack_model
2 |
3 | if snakemake.config["benchmark"] == "basic":
4 | model = basic_model
5 | elif snakemake.config["benchmark"] == "knapsack":
6 | model = knapsack_model
7 |
8 | n = int(snakemake.wildcards.N)
9 | solver = snakemake.config["solver"]
10 | model(n, solver)
11 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-gurobipy.py:
--------------------------------------------------------------------------------
1 | from benchmark_gurobipy import basic_model, knapsack_model
2 |
3 | if snakemake.config["benchmark"] == "basic":
4 | model = basic_model
5 | elif snakemake.config["benchmark"] == "knapsack":
6 | model = knapsack_model
7 |
8 | n = int(snakemake.wildcards.N)
9 | solver = snakemake.config["solver"]
10 | model(n, solver)
11 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-linopy.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Nov 19 17:40:33 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | from benchmark_linopy import basic_model, knapsack_model
9 |
10 | if snakemake.config["benchmark"] == "basic":
11 | model = basic_model
12 | elif snakemake.config["benchmark"] == "knapsack":
13 | model = knapsack_model
14 |
15 | n = int(snakemake.wildcards.N)
16 | solver = snakemake.config["solver"]
17 | model(n, solver)
18 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-ortools.py:
--------------------------------------------------------------------------------
1 | from benchmark_ortools import basic_model, knapsack_model
2 |
3 | if snakemake.config["benchmark"] == "basic":
4 | model = basic_model
5 | elif snakemake.config["benchmark"] == "knapsack":
6 | model = knapsack_model
7 |
8 | n = int(snakemake.wildcards.N)
9 | solver = snakemake.config["solver"]
10 | model(n, solver)
11 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-pulp.py:
--------------------------------------------------------------------------------
1 | from benchmark_pulp import basic_model, knapsack_model
2 |
3 | if snakemake.config["benchmark"] == "basic":
4 | model = basic_model
5 | elif snakemake.config["benchmark"] == "knapsack":
6 | model = knapsack_model
7 |
8 | n = int(snakemake.wildcards.N)
9 | solver = snakemake.config["solver"]
10 | model(n, solver)
11 |
--------------------------------------------------------------------------------
/benchmark/scripts/run-pyomo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Nov 19 17:40:33 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | from benchmark_pyomo import basic_model, knapsack_model
9 |
10 | if snakemake.config["benchmark"] == "basic":
11 | model = basic_model
12 | elif snakemake.config["benchmark"] == "knapsack":
13 | model = knapsack_model
14 |
15 | n = int(snakemake.wildcards.N)
16 | solver = snakemake.config["solver"]
17 | model(n, solver)
18 |
--------------------------------------------------------------------------------
/benchmark/scripts/write-lp-file.py:
--------------------------------------------------------------------------------
1 | from numpy import arange
2 | from numpy.random import default_rng
3 |
4 | from linopy import Model
5 |
6 | # Random seed for reproducibility
7 | rng = default_rng(125)
8 |
9 |
10 | if snakemake.config["benchmark"] == "basic":
11 |
12 | def create_model(n):
13 | m = Model()
14 | N, M = [arange(n), arange(n)]
15 | x = m.add_variables(coords=[N, M])
16 | y = m.add_variables(coords=[N, M])
17 | m.add_constraints(x - y >= N)
18 | m.add_constraints(x + y >= 0)
19 | m.add_objective((2 * x).sum() + y.sum())
20 | return m
21 |
22 | elif snakemake.config["benchmark"] == "knapsack":
23 |
24 | def create_model(n):
25 | m = Model()
26 | packages = m.add_variables(coords=[arange(n)], binary=True)
27 | weight = rng.integers(1, 100, size=n)
28 | value = rng.integers(1, 100, size=n)
29 | m.add_constraints((weight * packages).sum() <= 200)
30 | m.add_objective(-(value * packages).sum()) # use minus because of minimization
31 | return m
32 |
33 |
34 | for fn in snakemake.output:
35 | N = int(fn.split("/")[-1][:-3])
36 | create_model(N).to_file(fn)
37 |
--------------------------------------------------------------------------------
/codecov.yml:
--------------------------------------------------------------------------------
1 | comment: false
2 |
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Minimal makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line, and also
5 | # from the environment for the first two.
6 | SPHINXOPTS ?=
7 | SPHINXBUILD ?= sphinx-build
8 | SOURCEDIR = .
9 | BUILDDIR = _build
10 |
11 | # Put it first so that "make" without argument is like "make help".
12 | help:
13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
14 |
15 | .PHONY: help Makefile
16 |
17 | # Catch-all target: route all unknown targets to Sphinx using the new
18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
19 | %: Makefile
20 | @$(SPHINXBUILD) -b $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
21 |
--------------------------------------------------------------------------------
/doc/_static/theme_overrides.css:
--------------------------------------------------------------------------------
1 | .wy-side-nav-search {
2 | background-color: #eeeeee;
3 | }
4 |
5 | .wy-side-nav-search .wy-dropdown>a,
6 | .wy-side-nav-search>a {
7 | color: rgb(34, 97, 156)
8 | }
9 |
10 | .wy-side-nav-search>div.version {
11 | color: rgb(34, 97, 156)
12 | }
13 |
14 | .wy-menu-vertical header,
15 | .wy-menu-vertical p.caption,
16 | .rst-versions a {
17 | color: #999999;
18 | }
19 |
20 | .wy-menu-vertical a.reference:hover,
21 | .wy-menu-vertical a.reference.internal:hover {
22 | background: #dddddd;
23 | color: #fff;
24 | }
25 |
26 | .wy-nav-side {
27 | background: #efefef;
28 | }
29 |
30 | .wy-menu-vertical a.reference {
31 | color: #000;
32 | }
33 |
34 | .rst-versions .rst-current-version,
35 | .wy-nav-top,
36 | .wy-menu-vertical li.toctree-l2.current li.toctree-l3>a:hover {
37 | background: #002221;
38 | }
39 |
40 | .wy-nav-content .highlight {
41 | background: #ffffff;
42 | }
43 |
44 | .rst-content code.literal,
45 | .rst-content tt.literal {
46 | color: rgb(34, 97, 156)
47 | }
48 |
49 | .wy-nav-content a.reference {
50 | color: rgb(34, 97, 156);
51 | }
52 |
53 |
54 | /* override table width restrictions */
55 |
56 | @media screen and (min-width: 767px) {
57 | .wy-table-responsive table td {
58 | /* !important prevents the common CSS stylesheets from overriding
59 | this as on RTD they are loaded after this stylesheet */
60 | white-space: normal !important;
61 | background: rgb(250, 250, 250) !important;
62 | }
63 | .wy-table-responsive {
64 | max-width: 100%;
65 | overflow: visible !important;
66 | }
67 | .wy-nav-content {
68 | max-width: 910px !important;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/doc/api.rst:
--------------------------------------------------------------------------------
1 | .. currentmodule:: linopy
2 |
3 | #############
4 | API reference
5 | #############
6 |
7 | This page provides an auto-generated summary of linopy's API.
8 |
9 |
10 |
11 | Creating a model
12 | ================
13 |
14 | .. autosummary::
15 | :toctree: generated/
16 |
17 | model.Model
18 | model.Model.add_variables
19 | model.Model.add_constraints
20 | model.Model.add_objective
21 | model.Model.linexpr
22 | model.Model.remove_constraints
23 |
24 |
25 | Classes under the hook
26 | ======================
27 |
28 | Variable
29 | --------
30 |
31 | ``Variable`` is a subclass of ``xarray.DataArray`` and contains all labels referring to a multi-dimensional variable.
32 |
33 | .. autosummary::
34 | :toctree: generated/
35 |
36 | variables.Variable
37 | variables.Variable.lower
38 | variables.Variable.upper
39 | variables.Variable.sum
40 | variables.Variable.where
41 | variables.Variable.sanitize
42 | variables.Variables
43 | variables.ScalarVariable
44 |
45 | Variables
46 | ---------
47 |
48 | ``Variables`` is a container for multiple N-D labeled variables. It is automatically added to a ``Model`` instance when initialized.
49 |
50 | .. autosummary::
51 | :toctree: generated/
52 |
53 | variables.Variables
54 | variables.Variables.add
55 | variables.Variables.remove
56 | variables.Variables.continuous
57 | variables.Variables.integers
58 | variables.Variables.binaries
59 | variables.Variables.integers
60 | variables.Variables.flat
61 |
62 |
63 | LinearExpressions
64 | -----------------
65 |
66 | .. autosummary::
67 | :toctree: generated/
68 |
69 | expressions.LinearExpression
70 | expressions.LinearExpression.sum
71 | expressions.LinearExpression.where
72 | expressions.LinearExpression.groupby
73 | expressions.LinearExpression.rolling
74 | expressions.LinearExpression.from_tuples
75 | expressions.merge
76 | expressions.ScalarLinearExpression
77 |
78 | Constraint
79 | ----------
80 |
81 | ``Constraint`` is a subclass of ``xarray.DataArray`` and contains all labels referring to a multi-dimensional constraint.
82 |
83 | .. autosummary::
84 | :toctree: generated/
85 |
86 | constraints.Constraint
87 | constraints.Constraint.coeffs
88 | constraints.Constraint.vars
89 | constraints.Constraint.lhs
90 | constraints.Constraint.sign
91 | constraints.Constraint.rhs
92 | constraints.Constraint.flat
93 |
94 |
95 | Constraints
96 | -----------
97 |
98 | .. autosummary::
99 | :toctree: generated/
100 |
101 | constraints.Constraints
102 | constraints.Constraints.add
103 | constraints.Constraints.remove
104 | constraints.Constraints.coefficientrange
105 | constraints.Constraints.inequalities
106 | constraints.Constraints.equalities
107 | constraints.Constraints.sanitize_missings
108 | constraints.Constraints.flat
109 | constraints.Constraints.to_matrix
110 |
111 |
112 | IO functions
113 | ============
114 |
115 | .. autosummary::
116 | :toctree: generated/
117 |
118 | model.Model.get_problem_file
119 | model.Model.get_solution_file
120 | model.Model.to_file
121 | model.Model.to_netcdf
122 | io.read_netcdf
123 |
124 | Solvers
125 | ========
126 |
127 | .. autosummary::
128 | :toctree: generated/
129 |
130 | solvers.run_cbc
131 | solvers.run_glpk
132 | solvers.run_highs
133 | solvers.run_cplex
134 | solvers.run_gurobi
135 | solvers.run_xpress
136 | solvers.run_mosek
137 | solvers.run_mindopt
138 | solvers.run_copt
139 |
140 | Solving
141 | ========
142 |
143 | .. autosummary::
144 | :toctree: generated/
145 |
146 | model.Model.solve
147 | constants.SolverStatus
148 | constants.TerminationCondition
149 | constants.Status
150 | constants.Solution
151 | constants.Result
152 |
--------------------------------------------------------------------------------
/doc/benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/doc/benchmark.png
--------------------------------------------------------------------------------
/doc/benchmark.rst:
--------------------------------------------------------------------------------
1 | .. _benchmark:
2 |
3 | Benchmarks
4 | ==========
5 |
6 |
7 | Linopy's performance scales well with the problem size. Its overall speed is comparable with the famous `JuMP `_ package written in `Julia `_. It even outperforms `JuMP` in total memory efficiency when it comes to large models. Compared to `Pyomo `_, the common optimization package in python, one can expect
8 |
9 | * a **speedup of times 4-6**
10 | * a **memory reduction of roughly 50%**
11 |
12 | for large problems. The following figure shows the memory usage and speed for solving the problem
13 |
14 | .. math::
15 |
16 | & \min \;\; \sum_{i,j} 2 x_{i,j} \; y_{i,j} \\
17 | s.t. & \\
18 | & x_{i,j} - y_{i,j} \; \ge \; i \qquad \forall \; i,j \in \{1,...,N\} \\
19 | & x_{i,j} + y_{i,j} \; \ge \; 0 \qquad \forall \; i,j \in \{1,...,N\}
20 |
21 |
22 | with the different API's using the `Gurobi `_ solver. The workflow, that produces the figure, can be found `here `_.
23 |
24 | .. image:: benchmark.png
25 | :width: 1500
26 | :alt: benchmark
27 | :align: center
28 |
--------------------------------------------------------------------------------
/doc/benchmark_resource-overhead.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/doc/benchmark_resource-overhead.png
--------------------------------------------------------------------------------
/doc/conf.py:
--------------------------------------------------------------------------------
1 | # Configuration file for the Sphinx documentation builder.
2 | #
3 | # This file only contains a selection of the most common options. For a full
4 | # list see the documentation:
5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html
6 |
7 | # -- Path setup --------------------------------------------------------------
8 |
9 | # If extensions (or modules to document with autodoc) are in another directory,
10 | # add these directories to sys.path here. If the directory is relative to the
11 | # documentation root, use os.path.abspath to make it absolute, like shown here.
12 | #
13 | # import os
14 | # import sys
15 | # sys.path.insert(0, os.path.abspath('.'))
16 | import pkg_resources # part of setuptools
17 |
18 | # -- Project information -----------------------------------------------------
19 |
20 | project = "linopy"
21 | copyright = "2021, Fabian Hofmann"
22 | author = "Fabian Hofmann"
23 |
24 | # The full version, including alpha/beta/rc tags
25 | version = pkg_resources.get_distribution("linopy").version
26 | release = "master" if "dev" in version else version
27 |
28 | # For some reason is this needed, otherwise autosummary does fail on RTD but not locally
29 | import linopy # noqa
30 |
31 | # -- General configuration ---------------------------------------------------
32 |
33 | # Add any Sphinx extension module names here, as strings. They can be
34 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
35 | # ones.
36 | extensions = [
37 | "sphinx.ext.autodoc", # Core Sphinx library for auto html doc generation from docstrings
38 | "sphinx.ext.autosummary", # Create neat summary tables for modules/classes/methods etc
39 | "sphinx.ext.intersphinx",
40 | "sphinx.ext.napoleon",
41 | "sphinx.ext.mathjax",
42 | "nbsphinx",
43 | "nbsphinx_link",
44 | "sphinx.ext.imgconverter", # for SVG conversion
45 | ]
46 |
47 | # Add any paths that contain templates here, relative to this directory.
48 | templates_path = ["_templates"]
49 |
50 | # The language for content autogenerated by Sphinx. Refer to documentation
51 | # for a list of supported languages.
52 | #
53 | # This is also used if you do content translation via gettext catalogs.
54 | # Usually you set "language" from the command line for these cases.
55 | language = "en"
56 |
57 | # List of patterns, relative to source directory, that match files and
58 | # directories to ignore when looking for source files.
59 | # This pattern also affects html_static_path and html_extra_path.
60 | exclude_patterns = ["_build", "Thumbs.db", ".DS_Store", "**.ipynb_checkpoints"]
61 |
62 |
63 | source_suffix = {
64 | ".rst": "restructuredtext",
65 | ".txt": "markdown",
66 | ".md": "markdown",
67 | }
68 |
69 | # Autosummary
70 |
71 | autosummary_generate = True
72 | autodoc_typehints = "none"
73 |
74 | # Napoleon configurations
75 |
76 | napoleon_google_docstring = False
77 | napoleon_numpy_docstring = True
78 | napoleon_use_param = False
79 | napoleon_use_rtype = False
80 | napoleon_preprocess_types = True
81 |
82 |
83 | # -- Options for nbsphinx -------------------------------------------------
84 | nbsphinx_prolog = """
85 | {% set docname = env.doc2path(env.docname, base=None).replace("nblink","ipynb") %}
86 | .. note::
87 |
88 | You can `download `_ this example as a Jupyter notebook
89 | or start it `in interactive mode `_.
90 |
91 | """
92 |
93 | nbsphinx_allow_errors = False
94 |
95 | # -- Options for HTML output -------------------------------------------------
96 |
97 | # The theme to use for HTML and HTML Help pages. See the documentation for
98 | # a list of builtin themes.
99 | html_theme = "sphinx_book_theme"
100 |
101 | html_title = "Linopy: Optimization with n-dimensional labeled variables"
102 |
103 |
104 | # Theme options are theme-specific and customize the look and feel of a theme
105 | # further. For a list of options available for each theme, see the
106 | # documentation.
107 | html_theme_options = {
108 | "repository_url": "https://github.com/pypsa/linopy",
109 | "use_repository_button": True,
110 | }
111 |
112 |
113 | # These folders are copied to the documentation's HTML output
114 | html_static_path = ["_static"]
115 | html_logo = "logo.png"
116 |
117 |
118 | # These paths are either relative to html_static_path
119 | # or fully qualified paths (eg. https://...)
120 | html_css_files = ["theme_overrides.css"]
121 |
--------------------------------------------------------------------------------
/doc/contributing.rst:
--------------------------------------------------------------------------------
1 |
2 | ============
3 | Contributing
4 | ============
5 |
6 | We welcome anyone interested in contributing to this project,
7 | be it with new ideas, suggestions, by filing bug reports or
8 | contributing code.
9 |
10 | You are invited to submit pull requests / issues to our
11 | `Github repository `_.
12 |
13 | For linting, formatting and checking your code contributions
14 | against our guidelines (e.g. we use `Black `_ as code style
15 | and use `pre-commit `_:
16 |
17 | 1. Installation ``conda install -c conda-forge pre-commit`` or ``pip install pre-commit``
18 | 2. Usage:
19 | * To automatically activate ``pre-commit`` on every ``git commit``: Run ``pre-commit install``
20 | * To manually run it: ``pre-commit run --all``
21 |
22 | Contributing examples
23 | =====================
24 |
25 | Nice examples are always welcome.
26 |
27 | You can even submit your `Jupyter notebook`_ (``.ipynb``) directly
28 | as an example.
29 | For contributing notebooks (and working with notebooks in `git`
30 | in general) we have compiled a workflow for you which we suggest
31 | you follow:
32 |
33 | * Locally install `this precommit hook for git`_
34 |
35 | This obviously has to be done only once.
36 | The hook checks if any of the notebooks you are including in a commit
37 | contain a non-empty output cells.
38 |
39 | Then for every notebook:
40 |
41 | 1. Write the notebook (let's call it ``foo.ipynb``) and place it
42 | in ``examples/foo.ipynb``.
43 | 2. Ask yourself: Is the output in each of the notebook's cells
44 | relevant for to example?
45 |
46 | * Yes: Leave it there.
47 | Just make sure to keep the amount of pictures/... to a minimum.
48 | * No: Clear the output of all cells,
49 | e.g. `Edit -> Clear all output` in JupyterLab.
50 |
51 | 3. Provide a link to the documentation:
52 | Include a file ``foo.nblink`` located in ``doc/examples/foo.nblink``
53 | with this content
54 |
55 | .. code-block:
56 | {
57 | 'path' : '../../examples/foo.ipynb'
58 | }
59 |
60 | Adjust the path for your file's name.
61 | This ``nblink`` allows us to link your notebook into the documentation.
62 | 4. Link your file in the documentation:
63 |
64 | Either
65 |
66 | * Include your ``examples/foo.nblink`` directly into one of
67 | the documentations toctrees; or
68 | * Tell us where in the documentation you want your example to show up
69 |
70 | 5. Commit your changes.
71 | If the precommit hook you installed above kicks in, confirm
72 | your decision ('y') or go back ('n') and delete the output
73 | of the notebook's cells.
74 | 6. Create a pull request for us to accept your example.
75 |
76 | The support for the the ``.ipynb`` notebook format in our documentation
77 | is realised via the extensions `nbsphinx`_ and `nbsphinx_link`_.
78 |
79 | .. _Jupyter notebook: https://jupyter-notebook-beginner-guide.readthedocs.io/en/latest/what_is_jupyter.html
80 | .. _this precommit hook for git: https://jamesfolberth.org/articles/2017/08/07/git-commit-hook-for-jupyter-notebooks/
81 | .. _nbsphinx: https://nbsphinx.readthedocs.io/en/0.4.2/installation.html
82 | .. _nbsphinx_link: https://nbsphinx.readthedocs.io/en/latest/
83 |
--------------------------------------------------------------------------------
/doc/create-a-model-with-coordinates.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/create-a-model-with-coordinates.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/create-a-model.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/create-a-model.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/creating-constraints.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/creating-constraints.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/creating-expressions.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/creating-expressions.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/creating-variables.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/creating-variables.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/gurobi-double-logging.rst:
--------------------------------------------------------------------------------
1 |
2 | ========================
3 | Double logging in gurobi
4 | ========================
5 |
6 | When using the gurobi solver you may see some of the logs generated by gurobi during the solve
7 | are duplicated.
8 |
9 | e.g.
10 |
11 | .. code-block:: bash
12 |
13 | Total elapsed time = 498.27s
14 | [INFO] Total elapsed time = 498.27s
15 |
16 | This is because the gurobi logger both prints to the console and propagates to the root logger.
17 |
18 | Adding the following to your application code before call solve should fix the issue.
19 |
20 | .. code-block:: python
21 |
22 | logger = logging.getLogger("gurobipy")
23 | logger.propagate = False
24 |
25 |
26 | .. _Further information: https://groups.google.com/g/gurobi/c/sV7xxN_mzCk
27 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. linopy documentation master file, created by
2 | sphinx-quickstart on Tue Jun 15 10:15:57 2021.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | .. module:: linopy
7 |
8 | linopy: Linear optimization with N-D labeled variables
9 | ======================================================
10 |
11 | |PyPI| |CI| |License|
12 |
13 |
14 | Welcome to Linopy! This Python library is designed to make linear programming easy, flexible, and performant. Whether you're dealing with Linear, Integer, Mixed-Integer, or Quadratic Programming, Linopy is as a user-friendly interface to define variables and constraints. It serves as a bridge, connecting data analysis packages such like
15 | `xarray `__ &
16 | `pandas `__ with problem solvers.
17 |
18 |
19 | Main features
20 | -------------
21 |
22 | **linopy** is heavily based on
23 | `xarray `__ which allows for many
24 | flexible data-handling features:
25 |
26 | - Define (arrays of) contnuous or binary variables with
27 | **coordinates**, e.g. time, consumers, etc.
28 | - Apply **arithmetic operations** on the variables like adding,
29 | subtracting, multiplying with all the **broadcasting** potentials of
30 | xarray
31 | - Apply **arithmetic operations** on the **linear expressions**
32 | (combination of variables)
33 | - **Group terms** of a linear expression by coordinates
34 | - Get insight into the **clear and transparent data model**
35 | - **Modify** and **delete** assigned variables and constraints on the
36 | fly
37 | - Use **lazy operations** for large linear programs with
38 | `dask `__
39 | - Choose from **different commercial and non-commercial solvers**
40 | - Fast **import and export** a linear model using xarray’s netcdf IO
41 | - Support of various solvers
42 | - `Cbc `__
43 | - `GLPK `__
44 | - `HiGHS `__
45 | - `MindOpt `__
46 | - `Gurobi `__
47 | - `Xpress `__
48 | - `Cplex `__
49 | - `MOSEK `__
50 | - `COPT `__
51 |
52 |
53 |
54 | Citing Linopy
55 | -------------
56 |
57 | If you use Linopy in your research, please cite it as follows:
58 |
59 |
60 | Hofmann, F., (2023). Linopy: Linear optimization with n-dimensional labeled variables.
61 | Journal of Open Source Software, 8(84), 4823, https://doi.org/10.21105/joss.04823
62 |
63 |
64 | A BibTeX entry for LaTeX users is
65 |
66 | @article{Hofmann2023,
67 | doi = {10.21105/joss.04823},
68 | url = {https://doi.org/10.21105/joss.04823},
69 | year = {2023}, publisher = {The Open Journal},
70 | volume = {8},
71 | number = {84},
72 | pages = {4823},
73 | author = {Fabian Hofmann},
74 | title = {Linopy: Linear optimization with n-dimensional labeled variables},
75 | journal = {Journal of Open Source Software} }
76 |
77 |
78 |
79 | License
80 | -------
81 |
82 | Copyright 2021-2023 Fabian Hofmann
83 |
84 | This package is published under MIT license.
85 |
86 | .. |PyPI| image:: https://img.shields.io/pypi/v/linopy
87 | :target: https://pypi.org/project/linopy/
88 | .. |CI| image:: https://github.com/FabianHofmann/linopy/actions/workflows/CI.yaml/badge.svg
89 | :target: https://github.com/FabianHofmann/linopy/actions/workflows/CI.yaml
90 | .. |License| image:: https://img.shields.io/pypi/l/linopy.svg
91 | :target: https://mit-license.org/
92 |
93 |
94 | .. toctree::
95 | :hidden:
96 | :maxdepth: 2
97 | :caption: Getting Started
98 |
99 | prerequisites
100 | create-a-model
101 | create-a-model-with-coordinates
102 |
103 | .. toctree::
104 | :hidden:
105 | :maxdepth: 2
106 | :caption: User Guide
107 |
108 | user-guide
109 | creating-variables
110 | creating-expressions
111 | creating-constraints
112 | manipulating-models
113 | testing-framework
114 | transport-tutorial
115 | infeasible-model
116 | solve-on-remote
117 | migrating-from-pyomo
118 | gurobi-double-logging
119 |
120 |
121 | .. toctree::
122 | :hidden:
123 | :maxdepth: 2
124 | :caption: Benchmarking
125 |
126 | benchmark
127 | syntax
128 |
129 | .. toctree::
130 | :hidden:
131 | :maxdepth: 2
132 | :caption: References
133 |
134 | api
135 | release_notes
136 | contributing
137 |
--------------------------------------------------------------------------------
/doc/infeasible-model.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/infeasible-model.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/logo.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/doc/logo.pdf
--------------------------------------------------------------------------------
/doc/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/doc/logo.png
--------------------------------------------------------------------------------
/doc/logo.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Sun Oct 17 12:08:47 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | import matplotlib.pyplot as plt
9 | import numpy as np
10 | import pandas as pd
11 |
12 | textcolor = "#6f6070"
13 | textparams = dict(font="Ubuntu", color=textcolor)
14 |
15 | fig, (ax, ax1) = plt.subplots(
16 | 1, 2, gridspec_kw={"width_ratios": [2, 3]}, figsize=(10, 3)
17 | )
18 | # fig, ax = plt.subplots(1, 2, gridspec_kw={'width_ratios': [1,3]})
19 | ax.axis("off")
20 | ax.set_aspect("equal", adjustable="box")
21 | N = 11
22 | ax.set_xlim(0, N - 1)
23 |
24 | c = np.array([0, 3])
25 | x = np.array([2.5, 3])
26 | carray = pd.DataFrame([[c @ np.array([x1, x2]) for x1 in range(N)] for x2 in range(N)])
27 |
28 | # Define the diamond-shaped region of interest
29 | midpoint_x = N / 2
30 | midpoint_y = N / 2
31 |
32 | x, y = np.meshgrid(np.arange(N), np.arange(N))
33 | roi = (np.abs(x - midpoint_x) + np.abs(y - midpoint_y) <= midpoint_x) | ( # Top half
34 | np.abs(x - midpoint_x) + np.abs(y - midpoint_y) <= midpoint_y
35 | ) # Bottom half (mirrored)
36 |
37 | # Mask
38 |
39 |
40 | # Mask the contour plot using the region of interest
41 | masked_array = np.ma.masked_array(carray.values, ~roi)
42 | # ax.contourf(masked_array, levels=1000, cmap="Greens")
43 | # Draw boundaries around the masked contour shape
44 | contour_lines = ax.contour(masked_array, levels=20, colors=textcolor)
45 |
46 | # image = ax.imshow(masked_array, cmap="Greens")
47 |
48 | # ax.contourf(carray, levels=500, cmap="Greens")
49 | # ax.fill_between(np.linspace(-1, 7, N), np.linspace(4, 0, N), color="white")
50 | # ax.fill_between(np.linspace(1, 9, N), np.linspace(0, 2, N), color='white')
51 | # # ax.plot(np.linspace(-1, 7, N), np.linspace(6, 0, N), alpha=0.3, color=textcolor)
52 | # # ax.plot(np.linspace(1, 9, N), np.linspace(0, 4, N), alpha=0.2, color=textcolor)
53 |
54 |
55 | ax.scatter(5.54, 0.63, marker="8", color="orange", zorder=8)
56 |
57 | ax1.text(0, 0.44, "linopy", **textparams, size=170, ha="left", va="center")
58 | ax1.axis("off")
59 |
60 |
61 | fig.tight_layout()
62 | fig.savefig("logo.png", bbox_inches="tight", transparent=True)
63 | fig.savefig("logo.pdf", bbox_inches="tight", transparent=True)
64 |
--------------------------------------------------------------------------------
/doc/make.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | pushd %~dp0
4 |
5 | REM Command file for Sphinx documentation
6 |
7 | if "%SPHINXBUILD%" == "" (
8 | set SPHINXBUILD=sphinx-build
9 | )
10 | set SOURCEDIR=.
11 | set BUILDDIR=_build
12 |
13 | if "%1" == "" goto help
14 |
15 | %SPHINXBUILD% >NUL 2>NUL
16 | if errorlevel 9009 (
17 | echo.
18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
19 | echo.installed, then set the SPHINXBUILD environment variable to point
20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you
21 | echo.may add the Sphinx directory to PATH.
22 | echo.
23 | echo.If you don't have Sphinx installed, grab it from
24 | echo.http://sphinx-doc.org/
25 | exit /b 1
26 | )
27 |
28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
29 | goto end
30 |
31 | :help
32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
33 |
34 | :end
35 | popd
36 |
--------------------------------------------------------------------------------
/doc/manipulating-models.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/manipulating-models.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/migrating-from-pyomo.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/migrating-from-pyomo.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/prerequisites.rst:
--------------------------------------------------------------------------------
1 | Getting Started
2 | ===============
3 |
4 | This guide will provide you with the necessary steps to get started with Linopy, from installation to creating your first model and beyond.
5 |
6 | Before you start, make sure you have the following:
7 |
8 | - Python 3.9 or later installed on your system.
9 | - Basic knowledge of Python and linear programming.
10 |
11 |
12 | Install Linopy
13 | --------------
14 |
15 | You can install Linopy using pip or conda. Here are the commands for each method:
16 |
17 | .. code-block:: bash
18 |
19 | pip install linopy
20 |
21 | or
22 |
23 | .. code-block:: bash
24 |
25 | conda install -c conda-forge linopy
26 |
27 |
28 | Install a solver
29 | ----------------
30 |
31 | Linopy won't work without a solver. Currently, the following solvers are supported:
32 |
33 | - `Cbc `__ - open source, free, fast
34 | - `GLPK `__ - open source, free, not very fast
35 | - `HiGHS `__ - open source, free, fast
36 | - `Gurobi `__ - closed source, commercial, very fast
37 | - `Xpress `__ - closed source, commercial, very fast
38 | - `Cplex `__ - closed source, commercial, very fast
39 | - `MOSEK `__
40 | - `MindOpt `__ -
41 | - `COPT `__ - closed source, commercial, very fast
42 |
43 | For a subset of the solvers, Linopy provides a wrapper.
44 |
45 | .. code:: bash
46 |
47 | pip install linopy[solvers]
48 |
49 |
50 | We recommend to install the HiGHS solver if possible, which is free and open source but not yet available on all platforms.
51 |
52 | .. code:: bash
53 |
54 | pip install highspy
55 |
56 |
57 | For most of the other solvers, please click on the links to get further installation information.
58 |
59 |
60 |
61 | If you're ready, let's dive in!
62 |
--------------------------------------------------------------------------------
/doc/solve-on-remote.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/solve-on-remote.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/syntax.rst:
--------------------------------------------------------------------------------
1 |
2 | Syntax comparison
3 | =================
4 |
5 | In order to compare the syntax between different API's, let's initialize the following problem in the different API's:
6 |
7 | .. math::
8 |
9 | & \min \;\; \sum_{i,j} 2 x_{i,j} + \; y_{i,j} \\
10 | s.t. & \\
11 | & x_{i,j} - y_{i,j} \; \ge \; i-1 \qquad \forall \; i,j \in \{1,...,N\} \\
12 | & x_{i,j} + y_{i,j} \; \ge \; 0 \qquad \forall \; i,j \in \{1,...,N\}
13 |
14 |
15 |
16 |
17 |
18 | In ``JuMP`` the formulation translates to the following code:
19 |
20 | .. code-block:: julia
21 |
22 | using JuMP
23 |
24 | function create_model(N)
25 | m = Model()
26 | @variable(m, x[1:N, 1:N])
27 | @variable(m, y[1:N, 1:N])
28 | @constraint(m, x - y .>= 0:(N-1))
29 | @constraint(m, x + y .>= 0)
30 | @objective(m, Min, 2 * sum(x) + sum(y))
31 | return m
32 | end
33 |
34 | The same model in ``linopy`` is initialized by
35 |
36 | .. code-block:: python
37 |
38 | from linopy import Model
39 | from numpy import arange
40 |
41 |
42 | def create_model(N):
43 | m = Model()
44 | x = m.add_variables(coords=[arange(N), arange(N)])
45 | y = m.add_variables(coords=[arange(N), arange(N)])
46 | m.add_constraints(x - y >= arange(N))
47 | m.add_constraints(x + y >= 0)
48 | m.add_objective((2 * x).sum() + y.sum())
49 | return m
50 |
51 | Note that the syntax is quite similar.
52 |
53 | In ``Pyomo`` the code would look like
54 |
55 | .. code-block:: python
56 |
57 | from numpy import arange
58 | from pyomo.environ import ConcreteModel, Constraint, Objective, Set, Var
59 |
60 |
61 | def create_model(N):
62 | m = ConcreteModel()
63 | m.N = Set(initialize=arange(N))
64 |
65 | m.x = Var(m.N, m.N, bounds=(None, None))
66 | m.y = Var(m.N, m.N, bounds=(None, None))
67 |
68 | def bound1(m, i, j):
69 | return m.x[(i, j)] - m.y[(i, j)] >= i
70 |
71 | def bound2(m, i, j):
72 | return m.x[(i, j)] + m.y[(i, j)] >= 0
73 |
74 | def objective(m):
75 | return sum(2 * m.x[(i, j)] + m.y[(i, j)] for i in m.N for j in m.N)
76 |
77 | m.con1 = Constraint(m.N, m.N, rule=bound1)
78 | m.con2 = Constraint(m.N, m.N, rule=bound2)
79 | m.obj = Objective(rule=objective)
80 | return m
81 |
82 | which is heavily based on the internal call of functions in order to define the constraints.
83 |
--------------------------------------------------------------------------------
/doc/testing-framework.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/testing-framework.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/transport-tutorial.nblink:
--------------------------------------------------------------------------------
1 | {
2 | "path": "../examples/transport-tutorial.ipynb"
3 | }
4 |
--------------------------------------------------------------------------------
/doc/user-guide.rst:
--------------------------------------------------------------------------------
1 | .. _user-guide:
2 |
3 | Overview
4 | ========
5 |
6 | Welcome to the User Guide for Linopy. This guide is designed to help you understand and effectively use Linopy's features to solve your optimization problems, complementing the ``Getting Started`` section.
7 |
8 | In the following sections, we will take a closer look at how to create and manipulate models, variables, and constraints, and how to solve these models to find optimal solutions. Each section includes detailed explanations and code examples to help you understand the concepts and apply them to your own projects.
9 |
10 | If you are completely new to Linopy, consider to first have a look at the `Getting Started` section.
11 |
12 | Let's get started!
13 |
--------------------------------------------------------------------------------
/examples/create-a-model-with-coordinates.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "4db583af",
6 | "metadata": {},
7 | "source": [
8 | "# Use Coordinates\n",
9 | "\n",
10 | "Now, the real power of the package comes into play! \n",
11 | "\n",
12 | "Linopy is structured around the concept that variables, and therefore expressions and constraints, have coordinates. That is, a `Variable` object actually contains multiple variables across dimensions, just as we know it from a `numpy` array or a `pandas.DataFrame`.\n",
13 | "\n",
14 | "Suppose the two variables `x` and `y` are now functions of time `t` and we would modify the problem according to: "
15 | ]
16 | },
17 | {
18 | "cell_type": "markdown",
19 | "id": "comparable-talent",
20 | "metadata": {},
21 | "source": [
22 | "Minimize:\n",
23 | "$$\\sum_t x_t + 2 y_t$$\n",
24 | "\n",
25 | "subject to:\n",
26 | "\n",
27 | "$$\n",
28 | "x_t \\ge 0 \\qquad \\forall t \\\\\n",
29 | "y_t \\ge 0 \\qquad \\forall t \\\\\n",
30 | "3x_t + 7y_t \\ge 10 t \\qquad \\forall t\\\\\n",
31 | "5x_t + 2y_t \\ge 3 t \\qquad \\forall t\n",
32 | "$$\n",
33 | "\n",
34 | "whereas `t` spans all the range from 0 to 10."
35 | ]
36 | },
37 | {
38 | "cell_type": "markdown",
39 | "id": "proprietary-receipt",
40 | "metadata": {},
41 | "source": [
42 | "In order to formulate the new problem with linopy, we start again by initializing a model."
43 | ]
44 | },
45 | {
46 | "cell_type": "code",
47 | "execution_count": null,
48 | "id": "close-maximum",
49 | "metadata": {},
50 | "outputs": [],
51 | "source": [
52 | "import linopy\n",
53 | "\n",
54 | "m = linopy.Model()"
55 | ]
56 | },
57 | {
58 | "cell_type": "markdown",
59 | "id": "positive-appearance",
60 | "metadata": {},
61 | "source": [
62 | "Again, we define `x` and `y` using the `add_variables` function, but now we are adding a `coords` argument. This automatically creates optimization variables for all coordinates, in this case time-steps."
63 | ]
64 | },
65 | {
66 | "cell_type": "code",
67 | "execution_count": null,
68 | "id": "included-religious",
69 | "metadata": {},
70 | "outputs": [],
71 | "source": [
72 | "import pandas as pd\n",
73 | "\n",
74 | "time = pd.Index(range(10), name=\"time\")\n",
75 | "\n",
76 | "x = m.add_variables(\n",
77 | " lower=0,\n",
78 | " coords=[time],\n",
79 | " name=\"x\",\n",
80 | ")\n",
81 | "y = m.add_variables(lower=0, coords=[time], name=\"y\")"
82 | ]
83 | },
84 | {
85 | "cell_type": "markdown",
86 | "id": "terminal-ethernet",
87 | "metadata": {},
88 | "source": [
89 | "Following the previous example, we write the constraints out using the syntax from above, while multiplying the rhs with `t`. Note that the coordinates from the lhs and the rhs have to match. \n",
90 | "\n",
91 | ".. note::\n",
92 | " In the beginning, it is recommended to use explicit dimension names. Like that, things remain clear and no unexpected broadcasting (which we show later) will happen. "
93 | ]
94 | },
95 | {
96 | "cell_type": "code",
97 | "execution_count": null,
98 | "id": "c24d120a",
99 | "metadata": {},
100 | "outputs": [],
101 | "source": [
102 | "factor = pd.Series(time, index=time)\n",
103 | "\n",
104 | "3 * x + 7 * y >= 10 * factor"
105 | ]
106 | },
107 | {
108 | "cell_type": "markdown",
109 | "id": "f09803f4",
110 | "metadata": {},
111 | "source": [
112 | "It always helps to write out the constraints before adding them to the model. Since they look good, let's assign them."
113 | ]
114 | },
115 | {
116 | "cell_type": "code",
117 | "execution_count": null,
118 | "id": "comprehensive-blend",
119 | "metadata": {},
120 | "outputs": [],
121 | "source": [
122 | "con1 = m.add_constraints(3 * x + 7 * y >= 10 * factor, name=\"con1\")\n",
123 | "con2 = m.add_constraints(5 * x + 2 * y >= 3 * factor, name=\"con2\")\n",
124 | "m"
125 | ]
126 | },
127 | {
128 | "cell_type": "markdown",
129 | "id": "induced-professor",
130 | "metadata": {},
131 | "source": [
132 | "Now, when it comes to the objective, we use the `sum` function of `linopy.LinearExpression`. This stacks all terms all terms of the `time` dimension and writes them into one big expression. "
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "id": "alternate-story",
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "obj = (x + 2 * y).sum()\n",
143 | "m.add_objective(obj)"
144 | ]
145 | },
146 | {
147 | "cell_type": "code",
148 | "execution_count": null,
149 | "id": "outer-presence",
150 | "metadata": {},
151 | "outputs": [],
152 | "source": [
153 | "m.solve()"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "id": "495cd082",
159 | "metadata": {},
160 | "source": [
161 | "In order to inspect the solution. You can go via the variables, i.e. `y.solution` or via the `solution` aggregator of the model, which combines the solution of all variables. This can sometimes be helpful."
162 | ]
163 | },
164 | {
165 | "cell_type": "code",
166 | "execution_count": null,
167 | "id": "monthly-census",
168 | "metadata": {},
169 | "outputs": [],
170 | "source": [
171 | "m.solution.to_dataframe().plot(grid=True, ylabel=\"Optimal Value\");"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "id": "owned-europe",
177 | "metadata": {},
178 | "source": [
179 | "Alright! Now you learned how to set up linopy variables and expressions with coordinates. In the User Guide, which follows, we are going to see, how the representation of variables with coordinates allows us to formulate more advanced operations."
180 | ]
181 | }
182 | ],
183 | "metadata": {
184 | "@webio": {
185 | "lastCommId": null,
186 | "lastKernelId": null
187 | },
188 | "kernelspec": {
189 | "display_name": "Python 3",
190 | "language": "python",
191 | "name": "python3"
192 | },
193 | "language_info": {
194 | "codemirror_mode": {
195 | "name": "ipython",
196 | "version": 3
197 | },
198 | "file_extension": ".py",
199 | "mimetype": "text/x-python",
200 | "name": "python",
201 | "nbconvert_exporter": "python",
202 | "pygments_lexer": "ipython3",
203 | "version": "3.11.3"
204 | }
205 | },
206 | "nbformat": 4,
207 | "nbformat_minor": 5
208 | }
209 |
--------------------------------------------------------------------------------
/examples/creating-constraints.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "e8249281",
6 | "metadata": {},
7 | "source": [
8 | "# Creating Constraints\n",
9 | "\n",
10 | "Constraints are created and at the same time assigned to the model using the function \n",
11 | "\n",
12 | "```\n",
13 | "model.add_constraints\n",
14 | "```\n",
15 | "where `model` is a `linopy.Model` instance. Again, we want to understand this function and its argument. So, let's create a model first."
16 | ]
17 | },
18 | {
19 | "cell_type": "code",
20 | "execution_count": null,
21 | "id": "e0c196e4",
22 | "metadata": {},
23 | "outputs": [],
24 | "source": [
25 | "from linopy import Model\n",
26 | "\n",
27 | "m = Model()"
28 | ]
29 | },
30 | {
31 | "cell_type": "markdown",
32 | "id": "043c0b06",
33 | "metadata": {},
34 | "source": [
35 | "Given a variable `x` which has to by lower than 10/3, the constraint would be formulated as \n",
36 | "\n",
37 | "$$ x \\le \\frac{10}{3} $$\n",
38 | "\n",
39 | "or\n",
40 | "\n",
41 | "$$ 3 x \\le 10 $$\n",
42 | " \n",
43 | "or \n",
44 | "\n",
45 | "$$ x - \\frac{3}{10} \\le 0 $$\n",
46 | "\n",
47 | "\n",
48 | "of which all formulations can be written out with linopy just like that. "
49 | ]
50 | },
51 | {
52 | "cell_type": "code",
53 | "execution_count": null,
54 | "id": "6b496b92",
55 | "metadata": {},
56 | "outputs": [],
57 | "source": [
58 | "x = m.add_variables(name=\"x\")"
59 | ]
60 | },
61 | {
62 | "cell_type": "markdown",
63 | "id": "73541c03",
64 | "metadata": {},
65 | "source": [
66 | "When applying one of the operators `<=`, `>=`, `==` to the expression, an unassigned constraint is built:"
67 | ]
68 | },
69 | {
70 | "cell_type": "code",
71 | "execution_count": null,
72 | "id": "4c8aba7e",
73 | "metadata": {},
74 | "outputs": [],
75 | "source": [
76 | "con = 3 * x <= 10\n",
77 | "con"
78 | ]
79 | },
80 | {
81 | "cell_type": "markdown",
82 | "id": "0d75781d",
83 | "metadata": {},
84 | "source": [
85 | "Unasssigned means, it is not yet added to the model. We can inspect the elements of the anonymous constraint: "
86 | ]
87 | },
88 | {
89 | "cell_type": "code",
90 | "execution_count": null,
91 | "id": "01f182b5",
92 | "metadata": {},
93 | "outputs": [],
94 | "source": [
95 | "con.lhs"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "id": "783287b3",
102 | "metadata": {},
103 | "outputs": [],
104 | "source": [
105 | "con.rhs"
106 | ]
107 | },
108 | {
109 | "cell_type": "markdown",
110 | "id": "aac468c3",
111 | "metadata": {},
112 | "source": [
113 | "We can now add the constraint to the model by passing the unassigned `Constraint` to the `.add_constraint` function. "
114 | ]
115 | },
116 | {
117 | "cell_type": "code",
118 | "execution_count": null,
119 | "id": "0adf929b",
120 | "metadata": {},
121 | "outputs": [],
122 | "source": [
123 | "c = m.add_constraints(con, name=\"my-constraint\")\n",
124 | "c"
125 | ]
126 | },
127 | {
128 | "cell_type": "markdown",
129 | "id": "e78c2635",
130 | "metadata": {},
131 | "source": [
132 | "The same output would be generated if passing lhs, sign and rhs as separate arguments to the function:"
133 | ]
134 | },
135 | {
136 | "cell_type": "code",
137 | "execution_count": null,
138 | "id": "c084adec",
139 | "metadata": {},
140 | "outputs": [],
141 | "source": [
142 | "m.add_constraints(3 * x <= 10, name=\"the-same-constraint\")"
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "id": "2b4db4d5",
148 | "metadata": {},
149 | "source": [
150 | "Note that the return value of the operation is a `Constraint` which contains the reference labels to the constraints in the optimization model. Also is redirects to its lhs, sign and rhs, for example we can call"
151 | ]
152 | },
153 | {
154 | "cell_type": "code",
155 | "execution_count": null,
156 | "id": "ea6e990c",
157 | "metadata": {},
158 | "outputs": [],
159 | "source": [
160 | "c.lhs"
161 | ]
162 | },
163 | {
164 | "cell_type": "markdown",
165 | "id": "e6ae2a19",
166 | "metadata": {},
167 | "source": [
168 | "to inspect the lhs of a defined constraint."
169 | ]
170 | },
171 | {
172 | "cell_type": "markdown",
173 | "id": "efb74da3",
174 | "metadata": {},
175 | "source": [
176 | "When moving the constant value to the left hand side in the initialization, it will be pulled to the right hand side as soon as the constraint is defined"
177 | ]
178 | },
179 | {
180 | "cell_type": "code",
181 | "execution_count": null,
182 | "id": "e582051e",
183 | "metadata": {},
184 | "outputs": [],
185 | "source": [
186 | "3 * x - 10"
187 | ]
188 | },
189 | {
190 | "cell_type": "code",
191 | "execution_count": null,
192 | "id": "e2c2dbb3",
193 | "metadata": {},
194 | "outputs": [],
195 | "source": [
196 | "3 * x - 10 <= 0"
197 | ]
198 | },
199 | {
200 | "cell_type": "markdown",
201 | "id": "15909055",
202 | "metadata": {},
203 | "source": [
204 | "Like this, the all defined constraints have a clear separation between variable on the left, and constants on the right. "
205 | ]
206 | },
207 | {
208 | "cell_type": "markdown",
209 | "id": "b9d31509",
210 | "metadata": {},
211 | "source": [
212 | "All constraints are added to the `.constraints` container from where all assigned constraints can be accessed."
213 | ]
214 | },
215 | {
216 | "cell_type": "code",
217 | "execution_count": null,
218 | "id": "d205e695",
219 | "metadata": {},
220 | "outputs": [],
221 | "source": [
222 | "m.constraints"
223 | ]
224 | },
225 | {
226 | "cell_type": "code",
227 | "execution_count": null,
228 | "id": "cc5baaf4",
229 | "metadata": {},
230 | "outputs": [],
231 | "source": [
232 | "m.constraints[\"my-constraint\"]"
233 | ]
234 | }
235 | ],
236 | "metadata": {
237 | "@webio": {
238 | "lastCommId": null,
239 | "lastKernelId": null
240 | },
241 | "kernelspec": {
242 | "display_name": "Python 3",
243 | "language": "python",
244 | "name": "python3"
245 | },
246 | "language_info": {
247 | "codemirror_mode": {
248 | "name": "ipython",
249 | "version": 3
250 | },
251 | "file_extension": ".py",
252 | "mimetype": "text/x-python",
253 | "name": "python",
254 | "nbconvert_exporter": "python",
255 | "pygments_lexer": "ipython3",
256 | "version": "3.11.3"
257 | }
258 | },
259 | "nbformat": 4,
260 | "nbformat_minor": 5
261 | }
262 |
--------------------------------------------------------------------------------
/examples/infeasible-model.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "attachments": {},
5 | "cell_type": "markdown",
6 | "metadata": {},
7 | "source": [
8 | "# Trouble shooting - Infeasible Model\n",
9 | "\n",
10 | "From time to time, you encounter models that are infeasible. This means that there is no solution that satisfies all the constraints. Some solvers allow you to get the set of constraints that are infeasible. This is useful for debugging your model. \n",
11 | "\n",
12 | "In the following, we show how `linopy` can help you with the infeasibility diagnostics by finding the set of constraints that are infeasible. So far the functionality is limited to the `gurobi` solver. Hopefully, we will be able to extend this to other solvers in the future.\n",
13 | "\n",
14 | "We start by creating a simple model that is infeasible."
15 | ]
16 | },
17 | {
18 | "cell_type": "code",
19 | "execution_count": null,
20 | "metadata": {},
21 | "outputs": [],
22 | "source": [
23 | "import pandas as pd\n",
24 | "\n",
25 | "import linopy\n",
26 | "\n",
27 | "m = linopy.Model()\n",
28 | "\n",
29 | "time = pd.RangeIndex(10, name=\"time\")\n",
30 | "x = m.add_variables(lower=0, coords=[time], name=\"x\")\n",
31 | "y = m.add_variables(lower=0, coords=[time], name=\"y\")\n",
32 | "\n",
33 | "m.add_constraints(x <= 5)\n",
34 | "m.add_constraints(y <= 5)\n",
35 | "m.add_constraints(x + y >= 12)"
36 | ]
37 | },
38 | {
39 | "attachments": {},
40 | "cell_type": "markdown",
41 | "metadata": {},
42 | "source": [
43 | "If we now try to solve the model, we get an message that the model is infeasible."
44 | ]
45 | },
46 | {
47 | "cell_type": "code",
48 | "execution_count": null,
49 | "metadata": {},
50 | "outputs": [],
51 | "source": [
52 | "m.solve(solver_name=\"gurobi\")"
53 | ]
54 | },
55 | {
56 | "attachments": {},
57 | "cell_type": "markdown",
58 | "metadata": {},
59 | "source": [
60 | "Now, we can use the model in the background to find the set of infeasible constraints. The following code will return a list constraint labels that are infeasible."
61 | ]
62 | },
63 | {
64 | "cell_type": "code",
65 | "execution_count": null,
66 | "metadata": {},
67 | "outputs": [],
68 | "source": [
69 | "labels = m.compute_infeasibilities()\n",
70 | "labels"
71 | ]
72 | },
73 | {
74 | "attachments": {},
75 | "cell_type": "markdown",
76 | "metadata": {},
77 | "source": [
78 | "Using the `print_labels` function, we can print the constraints that we found to be infeasible."
79 | ]
80 | },
81 | {
82 | "cell_type": "code",
83 | "execution_count": null,
84 | "metadata": {},
85 | "outputs": [],
86 | "source": [
87 | "m.constraints.print_labels(labels)"
88 | ]
89 | },
90 | {
91 | "attachments": {},
92 | "cell_type": "markdown",
93 | "metadata": {},
94 | "source": [
95 | "The two function calls above can be combined into a single call:"
96 | ]
97 | },
98 | {
99 | "cell_type": "code",
100 | "execution_count": null,
101 | "metadata": {},
102 | "outputs": [],
103 | "source": [
104 | "m.print_infeasibilities()"
105 | ]
106 | }
107 | ],
108 | "metadata": {
109 | "kernelspec": {
110 | "display_name": "base",
111 | "language": "python",
112 | "name": "python3"
113 | },
114 | "language_info": {
115 | "codemirror_mode": {
116 | "name": "ipython",
117 | "version": 3
118 | },
119 | "file_extension": ".py",
120 | "mimetype": "text/x-python",
121 | "name": "python",
122 | "nbconvert_exporter": "python",
123 | "pygments_lexer": "ipython3",
124 | "version": "3.11.3"
125 | },
126 | "orig_nbformat": 4
127 | },
128 | "nbformat": 4,
129 | "nbformat_minor": 2
130 | }
131 |
--------------------------------------------------------------------------------
/examples/migrating-from-pyomo.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "ded90143",
6 | "metadata": {},
7 | "source": [
8 | "## Migrating from Pyomo\n",
9 | "\n",
10 | "Similar to the implementation in Pyomo, expressions and constraints can be created using a combination of a function and a set of coordinates to iterate over. For creating expressions, the function itself has to return a `ScalarLinearExpression` which can be obtained by selecting single values of the variables are combining them: "
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": null,
16 | "id": "19f3b954",
17 | "metadata": {},
18 | "outputs": [],
19 | "source": [
20 | "import pandas as pd\n",
21 | "\n",
22 | "import linopy\n",
23 | "\n",
24 | "m = linopy.Model()\n",
25 | "coords = pd.RangeIndex(10), [\"a\", \"b\"]\n",
26 | "x = m.add_variables(0, 100, coords, name=\"x\")\n",
27 | "x"
28 | ]
29 | },
30 | {
31 | "cell_type": "code",
32 | "execution_count": null,
33 | "id": "2bbfd13b",
34 | "metadata": {},
35 | "outputs": [],
36 | "source": [
37 | "x.at[0, \"a\"]"
38 | ]
39 | },
40 | {
41 | "cell_type": "markdown",
42 | "id": "a1631a76",
43 | "metadata": {},
44 | "source": [
45 | ".. important::\n",
46 | " The creation of scalar variables has changed in version `0.3.10` to use the `.at[]` method. When creating a `ScalarVariable` with the `[]` operator, a future warning is raised. The `[]` operator will reserver for integer and boolean indexing only, aligning to the xarray functionality. \n",
47 | "\n",
48 | "\n",
49 | "\n",
50 | "Such a `ScalarVariable` is very light-weight and can be used in functions in order to create expressions, just like you know it from `Pyomo`. The following function shows how:"
51 | ]
52 | },
53 | {
54 | "cell_type": "code",
55 | "execution_count": null,
56 | "id": "4ed6eafb",
57 | "metadata": {},
58 | "outputs": [],
59 | "source": [
60 | "def bound(m, i, j):\n",
61 | " if i % 2:\n",
62 | " return (i / 2) * x.at[i, j]\n",
63 | " else:\n",
64 | " return i * x.at[i, j]\n",
65 | "\n",
66 | "\n",
67 | "expr = m.linexpr(bound, coords)\n",
68 | "expr"
69 | ]
70 | },
71 | {
72 | "cell_type": "markdown",
73 | "id": "4faecead",
74 | "metadata": {},
75 | "source": [
76 | "Note that the function's first argument has to be the model itself, even though it might not be used in the function."
77 | ]
78 | },
79 | {
80 | "cell_type": "markdown",
81 | "id": "d7368607",
82 | "metadata": {},
83 | "source": [
84 | "This functionality is also supported by the `.add_constraints` function. When passing a function as a first argument, `.add_constraints` expects `coords` to by non-empty. The function itself has to return a `AnonymousScalarConstraint`, as done by "
85 | ]
86 | },
87 | {
88 | "cell_type": "code",
89 | "execution_count": null,
90 | "id": "eeebb710",
91 | "metadata": {},
92 | "outputs": [],
93 | "source": [
94 | "x.at[0, \"a\"] <= 3"
95 | ]
96 | },
97 | {
98 | "cell_type": "code",
99 | "execution_count": null,
100 | "id": "087203ad",
101 | "metadata": {},
102 | "outputs": [],
103 | "source": [
104 | "def bound(m, i, j):\n",
105 | " if i % 2:\n",
106 | " return (i / 2) * x.at[i, j] >= i\n",
107 | " else:\n",
108 | " return i * x.at[i, j] == 0.0\n",
109 | "\n",
110 | "\n",
111 | "con = m.add_constraints(bound, coords=coords)\n",
112 | "con"
113 | ]
114 | }
115 | ],
116 | "metadata": {
117 | "@webio": {
118 | "lastCommId": null,
119 | "lastKernelId": null
120 | },
121 | "kernelspec": {
122 | "display_name": "Python 3",
123 | "language": "python",
124 | "name": "python3"
125 | },
126 | "language_info": {
127 | "codemirror_mode": {
128 | "name": "ipython",
129 | "version": 3
130 | },
131 | "file_extension": ".py",
132 | "mimetype": "text/x-python",
133 | "name": "python",
134 | "nbconvert_exporter": "python",
135 | "pygments_lexer": "ipython3",
136 | "version": "3.11.3"
137 | }
138 | },
139 | "nbformat": 4,
140 | "nbformat_minor": 5
141 | }
142 |
--------------------------------------------------------------------------------
/examples/testing-framework.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "attachments": {},
5 | "cell_type": "markdown",
6 | "metadata": {},
7 | "source": [
8 | "# Testing with Linopy\n",
9 | "\n",
10 | "In some cases you want to make sure the objects you create with `linopy` are correct and behave as expected. For this purpose, `linopy` provides a small testing framework, available at `linopy.testing`. In the following, we will show how to use it."
11 | ]
12 | },
13 | {
14 | "cell_type": "code",
15 | "execution_count": null,
16 | "metadata": {},
17 | "outputs": [],
18 | "source": [
19 | "import pandas as pd\n",
20 | "\n",
21 | "import linopy\n",
22 | "from linopy.testing import assert_conequal, assert_linequal, assert_varequal"
23 | ]
24 | },
25 | {
26 | "cell_type": "code",
27 | "execution_count": null,
28 | "metadata": {},
29 | "outputs": [],
30 | "source": [
31 | "m = linopy.Model()"
32 | ]
33 | },
34 | {
35 | "attachments": {},
36 | "cell_type": "markdown",
37 | "metadata": {},
38 | "source": [
39 | "## Check the equality of two variables\n",
40 | "\n",
41 | "The most basic test is to check the equality of two variables. This can be done with the `assert_varequal` function. If the two variables are not equal, an `AssertionError` is raised."
42 | ]
43 | },
44 | {
45 | "cell_type": "code",
46 | "execution_count": null,
47 | "metadata": {},
48 | "outputs": [],
49 | "source": [
50 | "x = m.add_variables(coords=[pd.RangeIndex(10, name=\"first\")], name=\"x\")\n",
51 | "\n",
52 | "assert_varequal(x, m.variables.x)\n",
53 | "# or\n",
54 | "assert_varequal(x, m.variables[\"x\"])"
55 | ]
56 | },
57 | {
58 | "attachments": {},
59 | "cell_type": "markdown",
60 | "metadata": {},
61 | "source": [
62 | "## Check the equality of two expressions\n",
63 | "\n",
64 | "Analogeous to the `assert_varequal` function, the `assert_expr_equal` function can be used to check the equality of two expressions. "
65 | ]
66 | },
67 | {
68 | "cell_type": "code",
69 | "execution_count": null,
70 | "metadata": {},
71 | "outputs": [],
72 | "source": [
73 | "y = m.add_variables(coords=[pd.RangeIndex(10, name=\"first\")], name=\"y\")\n",
74 | "\n",
75 | "expr = 2 * x + y\n",
76 | "assert_linequal(expr, 2 * x + y)"
77 | ]
78 | },
79 | {
80 | "attachments": {},
81 | "cell_type": "markdown",
82 | "metadata": {},
83 | "source": [
84 | "## Check the equality of two constraints\n",
85 | "\n",
86 | "And finally, the `assert_constraint_equal` function can be used to check the equality of two constraints."
87 | ]
88 | },
89 | {
90 | "cell_type": "code",
91 | "execution_count": null,
92 | "metadata": {},
93 | "outputs": [],
94 | "source": [
95 | "con = m.add_constraints(expr >= 3, name=\"con\")\n",
96 | "\n",
97 | "assert_conequal(con, m.constraints.con)"
98 | ]
99 | },
100 | {
101 | "attachments": {},
102 | "cell_type": "markdown",
103 | "metadata": {},
104 | "source": [
105 | "Note that we cannot compare the `con` object with the original unassigned constraint `expr >= 3`. This is because a constraint object gets labels as soon as we add it to model. However we can make a workaround, and compare a new unassigned constraint, derived from `con` with the original constraint."
106 | ]
107 | },
108 | {
109 | "cell_type": "code",
110 | "execution_count": null,
111 | "metadata": {},
112 | "outputs": [],
113 | "source": [
114 | "assert_conequal(expr >= 3, con.lhs >= con.rhs)"
115 | ]
116 | }
117 | ],
118 | "metadata": {
119 | "kernelspec": {
120 | "display_name": "base",
121 | "language": "python",
122 | "name": "python3"
123 | },
124 | "language_info": {
125 | "codemirror_mode": {
126 | "name": "ipython",
127 | "version": 3
128 | },
129 | "file_extension": ".py",
130 | "mimetype": "text/x-python",
131 | "name": "python",
132 | "nbconvert_exporter": "python",
133 | "pygments_lexer": "ipython3",
134 | "version": "3.11.3"
135 | },
136 | "orig_nbformat": 4
137 | },
138 | "nbformat": 4,
139 | "nbformat_minor": 2
140 | }
141 |
--------------------------------------------------------------------------------
/linopy/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Wed Mar 10 11:03:06 2021.
4 |
5 | @author: fabulous
6 | """
7 |
8 | from importlib.metadata import version
9 |
10 | __version__ = version("linopy")
11 |
12 | # Note: For intercepting multiplications between xarray dataarrays, Variables and Expressions
13 | # we need to extend their __mul__ functions with a quick special case
14 | import linopy.monkey_patch_xarray # noqa: F401
15 | from linopy.common import align
16 | from linopy.config import options
17 | from linopy.constants import EQUAL, GREATER_EQUAL, LESS_EQUAL
18 | from linopy.constraints import Constraint, Constraints
19 | from linopy.expressions import LinearExpression, QuadraticExpression, merge
20 | from linopy.io import read_netcdf
21 | from linopy.model import Model, Variable, Variables, available_solvers
22 | from linopy.objective import Objective
23 | from linopy.remote import RemoteHandler
24 |
25 | __all__ = (
26 | "Constraint",
27 | "Constraints",
28 | "EQUAL",
29 | "GREATER_EQUAL",
30 | "LESS_EQUAL",
31 | "LinearExpression",
32 | "Model",
33 | "Objective",
34 | "QuadraticExpression",
35 | "RemoteHandler",
36 | "Variable",
37 | "Variables",
38 | "available_solvers",
39 | "align",
40 | "merge",
41 | "options",
42 | "read_netcdf",
43 | )
44 |
--------------------------------------------------------------------------------
/linopy/base.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/linopy/base.py
--------------------------------------------------------------------------------
/linopy/config.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Fri Jan 13 12:57:45 2023.
4 |
5 | @author: fabian
6 | """
7 |
8 | from __future__ import annotations
9 |
10 | from typing import Any
11 |
12 |
13 | class OptionSettings:
14 | def __init__(self, **kwargs: int) -> None:
15 | self._defaults = kwargs
16 | self._current_values = kwargs.copy()
17 |
18 | def __call__(self, **kwargs: int) -> None:
19 | self.set_value(**kwargs)
20 |
21 | def __getitem__(self, key: str) -> int:
22 | return self.get_value(key)
23 |
24 | def __setitem__(self, key: str, value: int) -> None:
25 | return self.set_value(**{key: value})
26 |
27 | def set_value(self, **kwargs: int) -> None:
28 | for k, v in kwargs.items():
29 | if k not in self._defaults:
30 | raise KeyError(f"{k} is not a valid setting.")
31 | self._current_values[k] = v
32 |
33 | def get_value(self, name: str) -> int:
34 | if name in self._defaults:
35 | return self._current_values[name]
36 | else:
37 | raise KeyError(f"{name} is not a valid setting.")
38 |
39 | def reset(self) -> None:
40 | self._current_values = self._defaults.copy()
41 |
42 | def __enter__(self) -> OptionSettings:
43 | return self
44 |
45 | def __exit__(
46 | self,
47 | exc_type: type[BaseException] | None,
48 | exc_val: BaseException | None,
49 | exc_tb: Any | None,
50 | ) -> None:
51 | self.reset()
52 |
53 | def __repr__(self) -> str:
54 | settings = "\n ".join(
55 | f"{name}={value}" for name, value in self._current_values.items()
56 | )
57 | return f"OptionSettings:\n {settings}"
58 |
59 |
60 | options = OptionSettings(display_max_rows=14, display_max_terms=6)
61 |
--------------------------------------------------------------------------------
/linopy/constants.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Linopy module for defining constant values used within the package.
4 | """
5 |
6 | import logging
7 | from dataclasses import dataclass, field
8 | from enum import Enum
9 | from typing import Any, Union
10 |
11 | import numpy as np
12 | import pandas as pd
13 |
14 | logger = logging.getLogger(__name__)
15 |
16 |
17 | EQUAL = "="
18 | GREATER_EQUAL = ">="
19 | LESS_EQUAL = "<="
20 |
21 | long_EQUAL = "=="
22 | short_GREATER_EQUAL = ">"
23 | short_LESS_EQUAL = "<"
24 |
25 |
26 | SIGNS: set[str] = {EQUAL, GREATER_EQUAL, LESS_EQUAL}
27 | SIGNS_alternative: set[str] = {long_EQUAL, short_GREATER_EQUAL, short_LESS_EQUAL}
28 | SIGNS_pretty: dict[str, str] = {EQUAL: "=", GREATER_EQUAL: "≥", LESS_EQUAL: "≤"}
29 |
30 | sign_replace_dict: dict[str, str] = {
31 | long_EQUAL: EQUAL,
32 | short_GREATER_EQUAL: GREATER_EQUAL,
33 | short_LESS_EQUAL: LESS_EQUAL,
34 | }
35 |
36 | TERM_DIM = "_term"
37 | STACKED_TERM_DIM = "_stacked_term"
38 | GROUPED_TERM_DIM = "_grouped_term"
39 | GROUP_DIM = "_group"
40 | FACTOR_DIM = "_factor"
41 | CONCAT_DIM = "_concat"
42 | HELPER_DIMS: list[str] = [
43 | TERM_DIM,
44 | STACKED_TERM_DIM,
45 | GROUPED_TERM_DIM,
46 | FACTOR_DIM,
47 | CONCAT_DIM,
48 | ]
49 |
50 |
51 | class ModelStatus(Enum):
52 | """
53 | Model status.
54 |
55 | The set of possible model status is a superset of the solver status
56 | set.
57 | """
58 |
59 | ok = "ok"
60 | warning = "warning"
61 | error = "error"
62 | aborted = "aborted"
63 | unknown = "unknown"
64 | initialized = "initialized"
65 |
66 |
67 | class SolverStatus(Enum):
68 | """
69 | Solver status.
70 | """
71 |
72 | ok = "ok"
73 | warning = "warning"
74 | error = "error"
75 | aborted = "aborted"
76 | unknown = "unknown"
77 |
78 | @classmethod
79 | def process(cls, status: str) -> "SolverStatus":
80 | try:
81 | return cls(status)
82 | except ValueError:
83 | return cls("unknown")
84 |
85 | @classmethod
86 | def from_termination_condition(
87 | cls, termination_condition: "TerminationCondition"
88 | ) -> "SolverStatus":
89 | for status in STATUS_TO_TERMINATION_CONDITION_MAP:
90 | if termination_condition in STATUS_TO_TERMINATION_CONDITION_MAP[status]:
91 | return status
92 | return cls("unknown")
93 |
94 |
95 | class TerminationCondition(Enum):
96 | """
97 | Termination condition of the solver.
98 | """
99 |
100 | # UNKNOWN
101 | unknown = "unknown"
102 |
103 | # OK
104 | optimal = "optimal"
105 | time_limit = "time_limit"
106 | iteration_limit = "iteration_limit"
107 | terminated_by_limit = "terminated_by_limit"
108 | suboptimal = "suboptimal"
109 |
110 | # WARNING
111 | unbounded = "unbounded"
112 | infeasible = "infeasible"
113 | infeasible_or_unbounded = "infeasible_or_unbounded"
114 | other = "other"
115 |
116 | # ERROR
117 | internal_solver_error = "internal_solver_error"
118 | error = "error"
119 |
120 | # ABORTED
121 | user_interrupt = "user_interrupt"
122 | resource_interrupt = "resource_interrupt"
123 | licensing_problems = "licensing_problems"
124 |
125 | @classmethod
126 | def process(
127 | cls, termination_condition: Union["TerminationCondition", str]
128 | ) -> "TerminationCondition":
129 | if isinstance(termination_condition, TerminationCondition):
130 | termination_condition = termination_condition.value
131 | try:
132 | return cls(termination_condition)
133 | except ValueError:
134 | return cls("unknown")
135 |
136 |
137 | STATUS_TO_TERMINATION_CONDITION_MAP: dict[SolverStatus, list[TerminationCondition]] = {
138 | SolverStatus.ok: [
139 | TerminationCondition.optimal,
140 | TerminationCondition.iteration_limit,
141 | TerminationCondition.time_limit,
142 | TerminationCondition.terminated_by_limit,
143 | TerminationCondition.suboptimal,
144 | ],
145 | SolverStatus.warning: [
146 | TerminationCondition.unbounded,
147 | TerminationCondition.infeasible,
148 | TerminationCondition.infeasible_or_unbounded,
149 | TerminationCondition.other,
150 | ],
151 | SolverStatus.error: [
152 | TerminationCondition.internal_solver_error,
153 | TerminationCondition.error,
154 | ],
155 | SolverStatus.aborted: [
156 | TerminationCondition.user_interrupt,
157 | TerminationCondition.resource_interrupt,
158 | TerminationCondition.licensing_problems,
159 | ],
160 | SolverStatus.unknown: [TerminationCondition.unknown],
161 | }
162 |
163 |
164 | @dataclass
165 | class Status:
166 | """
167 | Status and termination condition of the solver.
168 | """
169 |
170 | status: SolverStatus
171 | termination_condition: TerminationCondition
172 | legacy_status: Union[tuple[str, str], str] = ""
173 |
174 | @classmethod
175 | def process(cls, status: str, termination_condition: str) -> "Status":
176 | return cls(
177 | status=SolverStatus.process(status),
178 | termination_condition=TerminationCondition.process(termination_condition),
179 | legacy_status=(status, termination_condition),
180 | )
181 |
182 | @classmethod
183 | def from_termination_condition(
184 | cls, termination_condition: Union["TerminationCondition", str]
185 | ) -> "Status":
186 | termination_condition = TerminationCondition.process(termination_condition)
187 | solver_status = SolverStatus.from_termination_condition(termination_condition)
188 | return cls(solver_status, termination_condition)
189 |
190 | @property
191 | def is_ok(self) -> bool:
192 | return self.status == SolverStatus.ok
193 |
194 |
195 | def _pd_series_float() -> pd.Series:
196 | return pd.Series(dtype=float)
197 |
198 |
199 | @dataclass
200 | class Solution:
201 | """
202 | Solution returned by the solver.
203 | """
204 |
205 | primal: pd.Series = field(default_factory=_pd_series_float)
206 | dual: pd.Series = field(default_factory=_pd_series_float)
207 | objective: float = field(default=np.nan)
208 |
209 |
210 | @dataclass
211 | class Result:
212 | """
213 | Result of the optimization.
214 | """
215 |
216 | status: Status
217 | solution: Union[Solution, None] = None
218 | solver_model: Any = None
219 |
220 | def __repr__(self) -> str:
221 | solver_model_string = (
222 | "not available" if self.solver_model is None else "available"
223 | )
224 | if self.solution is not None:
225 | solution_string = (
226 | f"Solution: {len(self.solution.primal)} primals, {len(self.solution.dual)} duals\n"
227 | f"Objective: {self.solution.objective:.2e}\n"
228 | )
229 | else:
230 | solution_string = "Solution: None\n"
231 | return (
232 | f"Status: {self.status.status.value}\n"
233 | f"Termination condition: {self.status.termination_condition.value}\n"
234 | + solution_string
235 | + f"Solver model: {solver_model_string}\n"
236 | f"Solver message: {self.status.legacy_status}"
237 | )
238 |
239 | def info(self) -> None:
240 | status = self.status
241 |
242 | if status.is_ok:
243 | if status.termination_condition == TerminationCondition.suboptimal:
244 | logger.warning("Optimization solution is sub-optimal: \n%s\n", self)
245 | else:
246 | logger.info(" Optimization successful: \n%s\n", self)
247 | else:
248 | logger.warning("Optimization potentially failed: \n%s\n", self)
249 |
--------------------------------------------------------------------------------
/linopy/examples.py:
--------------------------------------------------------------------------------
1 | """
2 | This module contains examples of linear programming models using the linopy library.
3 | """
4 |
5 | from numpy import arange
6 |
7 | from linopy import Model
8 |
9 |
10 | def simple_two_single_variables_model() -> Model:
11 | """
12 | Creates a simple linear programming model with two single variables.
13 |
14 | Returns
15 | -------
16 | Model: The created linear programming model.
17 | """
18 | m = Model()
19 |
20 | x = m.add_variables(name="x")
21 | y = m.add_variables(name="y")
22 |
23 | m.add_constraints(2 * x + 6 * y >= 10)
24 | m.add_constraints(4 * x + 2 * y >= 3)
25 |
26 | m.add_objective(2 * y + x)
27 | return m
28 |
29 |
30 | def simple_two_array_variables_model() -> Model:
31 | """
32 | Creates a simple linear programming model with two array variables.
33 |
34 | Returns
35 | -------
36 | Model: The created linear programming model.
37 | """
38 | m = Model()
39 |
40 | lower = [-10, -5]
41 | upper = [10, 15]
42 | x = m.add_variables(lower, upper, name="x")
43 |
44 | lower = [4, 0]
45 | upper = [8, 15]
46 | y = m.add_variables(lower, upper, name="y")
47 |
48 | m.add_constraints(2 * x + 2 * y >= 10)
49 | m.add_constraints(6 * x + 2 * y <= 100)
50 |
51 | m.add_objective(y + 2 * x)
52 | return m
53 |
54 |
55 | def benchmark_model(n: int = 10, integerlabels: bool = False) -> Model:
56 | """
57 | Creates a benchmark linear programming model used in https://doi.org/10.21105/joss.04823.
58 |
59 | Args:
60 | ----
61 | n (int): The size of the benchmark models dimensions.
62 | integerlabels (bool, optional): Whether to use integer labels for variables.
63 | Defaults to False.
64 |
65 | Returns:
66 | -------
67 | Model: The created linear programming model.
68 | """
69 | m = Model()
70 | if integerlabels:
71 | naxis, maxis = [arange(n, dtype=int), arange(n, dtype=int)]
72 | else:
73 | naxis, maxis = [arange(n, dtype=float), arange(n).astype(str)]
74 | x = m.add_variables(coords=[naxis, maxis])
75 | y = m.add_variables(coords=[naxis, maxis])
76 | m.add_constraints(x - y >= naxis)
77 | m.add_constraints(x + y >= 0)
78 | m.add_objective((2 * x).sum() + y.sum())
79 | return m
80 |
--------------------------------------------------------------------------------
/linopy/matrices.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Mon Oct 10 13:33:55 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | from __future__ import annotations
9 |
10 | from functools import cached_property
11 | from typing import TYPE_CHECKING
12 |
13 | import numpy as np
14 | import pandas as pd
15 | from numpy import ndarray
16 | from pandas.core.indexes.base import Index
17 | from pandas.core.series import Series
18 | from scipy.sparse._csc import csc_matrix
19 |
20 | from linopy import expressions
21 |
22 | if TYPE_CHECKING:
23 | from linopy.model import Model
24 |
25 |
26 | def create_vector(
27 | indices: Series | Index,
28 | values: Series | ndarray,
29 | fill_value: str | float | int = np.nan,
30 | shape: int | None = None,
31 | ) -> ndarray:
32 | """Create a vector of a size equal to the maximum index plus one."""
33 | if shape is None:
34 | max_value = indices.max()
35 | if not isinstance(max_value, (np.integer, int)):
36 | raise ValueError("Indices must be integers.")
37 | shape = max_value + 1
38 | vector = np.full(shape, fill_value)
39 | vector[indices] = values
40 | return vector
41 |
42 |
43 | class MatrixAccessor:
44 | """
45 | Helper class to quickly access model related vectors and matrices.
46 | """
47 |
48 | def __init__(self, model: Model) -> None:
49 | self._parent = model
50 |
51 | def clean_cached_properties(self) -> None:
52 | """Clear the cache for all cached properties of an object"""
53 |
54 | for cached_prop in ["flat_vars", "flat_cons", "sol", "dual"]:
55 | # check existence of cached_prop without creating it
56 | if cached_prop in self.__dict__:
57 | delattr(self, cached_prop)
58 |
59 | @cached_property
60 | def flat_vars(self) -> pd.DataFrame:
61 | m = self._parent
62 | return m.variables.flat
63 |
64 | @cached_property
65 | def flat_cons(self) -> pd.DataFrame:
66 | m = self._parent
67 | return m.constraints.flat
68 |
69 | @property
70 | def vlabels(self) -> ndarray:
71 | """Vector of labels of all non-missing variables."""
72 | df: pd.DataFrame = self.flat_vars
73 | return create_vector(df.key, df.labels, -1)
74 |
75 | @property
76 | def vtypes(self) -> ndarray:
77 | """Vector of types of all non-missing variables."""
78 | m = self._parent
79 | df: pd.DataFrame = self.flat_vars
80 | specs = []
81 | for name in m.variables:
82 | if name in m.binaries:
83 | val = "B"
84 | elif name in m.integers:
85 | val = "I"
86 | else:
87 | val = "C"
88 | specs.append(pd.Series(val, index=m.variables[name].flat.labels))
89 |
90 | ds = pd.concat(specs)
91 | ds = df.set_index("key").labels.map(ds)
92 | return create_vector(ds.index, ds.to_numpy(), fill_value="")
93 |
94 | @property
95 | def lb(self) -> ndarray:
96 | """Vector of lower bounds of all non-missing variables."""
97 | df: pd.DataFrame = self.flat_vars
98 | return create_vector(df.key, df.lower)
99 |
100 | @cached_property
101 | def sol(self) -> ndarray:
102 | """Vector of solution values of all non-missing variables."""
103 | if not self._parent.status == "ok":
104 | raise ValueError("Model is not optimized.")
105 | if "solution" not in self.flat_vars:
106 | del self.flat_vars # clear cache
107 | df: pd.DataFrame = self.flat_vars
108 | return create_vector(df.key, df.solution, fill_value=np.nan)
109 |
110 | @cached_property
111 | def dual(self) -> ndarray:
112 | """Vector of dual values of all non-missing constraints."""
113 | if not self._parent.status == "ok":
114 | raise ValueError("Model is not optimized.")
115 | if "dual" not in self.flat_cons:
116 | del self.flat_cons # clear cache
117 | df: pd.DataFrame = self.flat_cons
118 | if "dual" not in df:
119 | raise AttributeError(
120 | "Underlying is optimized but does not have dual values stored."
121 | )
122 | return create_vector(df.key, df.dual, fill_value=np.nan)
123 |
124 | @property
125 | def ub(self) -> ndarray:
126 | """Vector of upper bounds of all non-missing variables."""
127 | df: pd.DataFrame = self.flat_vars
128 | return create_vector(df.key, df.upper)
129 |
130 | @property
131 | def clabels(self) -> ndarray:
132 | """Vector of labels of all non-missing constraints."""
133 | df: pd.DataFrame = self.flat_cons
134 | if df.empty:
135 | return np.array([], dtype=int)
136 | return create_vector(df.key, df.labels, fill_value=-1)
137 |
138 | @property
139 | def A(self) -> csc_matrix | None:
140 | """Constraint matrix of all non-missing constraints and variables."""
141 | m = self._parent
142 | if not len(m.constraints):
143 | return None
144 | A: csc_matrix = m.constraints.to_matrix(filter_missings=False)
145 | return A[self.clabels][:, self.vlabels]
146 |
147 | @property
148 | def sense(self) -> ndarray:
149 | """Vector of senses of all non-missing constraints."""
150 | df: pd.DataFrame = self.flat_cons
151 | return create_vector(df.key, df.sign.astype(np.dtype(" ndarray:
155 | """Vector of right-hand-sides of all non-missing constraints."""
156 | df: pd.DataFrame = self.flat_cons
157 | return create_vector(df.key, df.rhs)
158 |
159 | @property
160 | def c(self) -> ndarray:
161 | """Vector of objective coefficients of all non-missing variables."""
162 | m = self._parent
163 | ds = m.objective.flat
164 | if isinstance(m.objective.expression, expressions.QuadraticExpression):
165 | ds = ds[(ds.vars1 == -1) | (ds.vars2 == -1)]
166 | ds["vars"] = ds.vars1.where(ds.vars1 != -1, ds.vars2)
167 |
168 | vars: pd.Series = ds.vars.map(self.flat_vars.set_index("labels").key)
169 | shape: int = self.flat_vars.key.max() + 1
170 | return create_vector(vars, ds.coeffs, fill_value=0.0, shape=shape)
171 |
172 | @property
173 | def Q(self) -> csc_matrix | None:
174 | """Matrix objective coefficients of quadratic terms of all non-missing variables."""
175 | m = self._parent
176 | expr = m.objective.expression
177 | if not isinstance(expr, expressions.QuadraticExpression):
178 | return None
179 | return expr.to_matrix()[self.vlabels][:, self.vlabels]
180 |
--------------------------------------------------------------------------------
/linopy/monkey_patch_xarray.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from functools import partialmethod, update_wrapper
4 | from typing import Any, Callable
5 |
6 | from xarray import DataArray
7 |
8 | from linopy import expressions, variables
9 | from linopy.types import NotImplementedType
10 |
11 |
12 | def monkey_patch(cls: type[DataArray], pass_unpatched_method: bool = False) -> Callable:
13 | def deco(func: Callable) -> Callable:
14 | func_name = func.__name__
15 | wrapped = getattr(cls, func_name)
16 | update_wrapper(func, wrapped)
17 | if pass_unpatched_method:
18 | func = partialmethod(func, unpatched_method=wrapped) # type: ignore
19 | setattr(cls, func_name, func)
20 | return func
21 |
22 | return deco
23 |
24 |
25 | @monkey_patch(DataArray, pass_unpatched_method=True)
26 | def __mul__(
27 | da: DataArray, other: Any, unpatched_method: Callable
28 | ) -> DataArray | NotImplementedType:
29 | if isinstance(
30 | other,
31 | (
32 | variables.Variable,
33 | expressions.LinearExpression,
34 | expressions.QuadraticExpression,
35 | ),
36 | ):
37 | return NotImplemented
38 | return unpatched_method(da, other)
39 |
--------------------------------------------------------------------------------
/linopy/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/PyPSA/linopy/a956aa4ac74e326d47b5e4105523a007c20f3351/linopy/py.typed
--------------------------------------------------------------------------------
/linopy/testing.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | from xarray.testing import assert_equal
4 |
5 | from linopy.constraints import Constraint, _con_unwrap
6 | from linopy.expressions import LinearExpression, QuadraticExpression, _expr_unwrap
7 | from linopy.model import Model
8 | from linopy.variables import Variable, _var_unwrap
9 |
10 |
11 | def assert_varequal(a: Variable, b: Variable) -> None:
12 | """Assert that two variables are equal."""
13 | return assert_equal(_var_unwrap(a), _var_unwrap(b))
14 |
15 |
16 | def assert_linequal(
17 | a: LinearExpression | QuadraticExpression, b: LinearExpression | QuadraticExpression
18 | ) -> None:
19 | """Assert that two linear expressions are equal."""
20 | assert isinstance(a, LinearExpression)
21 | assert isinstance(b, LinearExpression)
22 | return assert_equal(_expr_unwrap(a), _expr_unwrap(b))
23 |
24 |
25 | def assert_quadequal(
26 | a: LinearExpression | QuadraticExpression, b: LinearExpression | QuadraticExpression
27 | ) -> None:
28 | """Assert that two quadratic or linear expressions are equal."""
29 | return assert_equal(_expr_unwrap(a), _expr_unwrap(b))
30 |
31 |
32 | def assert_conequal(a: Constraint, b: Constraint) -> None:
33 | """Assert that two constraints are equal."""
34 | return assert_equal(_con_unwrap(a), _con_unwrap(b))
35 |
36 |
37 | def assert_model_equal(a: Model, b: Model) -> None:
38 | """Assert that two models are equal."""
39 | for k in a.dataset_attrs:
40 | assert_equal(getattr(a, k), getattr(b, k))
41 |
42 | assert set(a.variables) == set(b.variables)
43 | assert set(a.constraints) == set(b.constraints)
44 |
45 | for v in a.variables:
46 | assert_varequal(a.variables[v], b.variables[v])
47 |
48 | for c in a.constraints:
49 | assert_conequal(a.constraints[c], b.constraints[c])
50 |
51 | assert_linequal(a.objective.expression, b.objective.expression)
52 | assert a.objective.sense == b.objective.sense
53 | assert a.objective.value == b.objective.value
54 |
55 | assert a.status == b.status
56 | assert a.termination_condition == b.termination_condition
57 |
58 | assert a.type == b.type
59 |
--------------------------------------------------------------------------------
/linopy/types.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import sys
4 | from collections.abc import Hashable, Iterable, Mapping, Sequence
5 | from pathlib import Path
6 | from typing import TYPE_CHECKING, Union
7 |
8 | import numpy
9 | import numpy.typing
10 | from pandas import DataFrame, Index, Series
11 | from xarray import DataArray
12 | from xarray.core.coordinates import DataArrayCoordinates, DatasetCoordinates
13 |
14 | if sys.version_info >= (3, 10):
15 | from types import EllipsisType, NotImplementedType
16 | else:
17 | EllipsisType = type(Ellipsis)
18 | NotImplementedType = type(NotImplemented)
19 |
20 | if TYPE_CHECKING:
21 | from linopy.constraints import AnonymousScalarConstraint, Constraint
22 | from linopy.expressions import (
23 | LinearExpression,
24 | QuadraticExpression,
25 | ScalarLinearExpression,
26 | )
27 | from linopy.variables import ScalarVariable, Variable
28 |
29 | # Type aliases using Union for Python 3.9 compatibility
30 | CoordsLike = Union[
31 | Sequence[Union[Sequence, Index, DataArray]],
32 | Mapping,
33 | DataArrayCoordinates,
34 | DatasetCoordinates,
35 | ]
36 | DimsLike = Union[str, Iterable[Hashable]]
37 |
38 | ConstantLike = Union[
39 | int,
40 | float,
41 | numpy.floating,
42 | numpy.integer,
43 | numpy.ndarray,
44 | DataArray,
45 | Series,
46 | DataFrame,
47 | ]
48 | SignLike = Union[str, numpy.ndarray, DataArray, Series, DataFrame]
49 | VariableLike = Union["ScalarVariable", "Variable"]
50 | ExpressionLike = Union[
51 | "ScalarLinearExpression",
52 | "LinearExpression",
53 | "QuadraticExpression",
54 | ]
55 | ConstraintLike = Union["Constraint", "AnonymousScalarConstraint"]
56 | MaskLike = Union[numpy.ndarray, DataArray, Series, DataFrame]
57 | SideLike = Union[ConstantLike, VariableLike, ExpressionLike]
58 | PathLike = Union[str, Path]
59 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [build-system]
2 | requires = ["setuptools>=60", "setuptools-scm>=8.0"]
3 | build-backend = "setuptools.build_meta"
4 |
5 | [project]
6 | name = "linopy"
7 | dynamic = ["version"]
8 | description = "Linear optimization with N-D labeled arrays in Python"
9 | readme = "README.md"
10 | authors = [{ name = "Fabian Hofmann", email = "fabianmarikhofmann@gmail.com" }]
11 | license = { file = "LICENSE" }
12 | classifiers = [
13 | "Programming Language :: Python :: 3.9",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Development Status :: 3 - Alpha",
18 | "Environment :: Console",
19 | "Intended Audience :: Science/Research",
20 | "License :: OSI Approved :: MIT License",
21 | "Natural Language :: English",
22 | "Typing :: Typed",
23 | "Operating System :: OS Independent",
24 | ]
25 |
26 | requires-python = ">=3.9"
27 | dependencies = [
28 | "numpy; python_version > '3.10'",
29 | "numpy<2; python_version <= '3.10'",
30 | "scipy",
31 | "bottleneck",
32 | "toolz",
33 | "numexpr",
34 | "xarray>=2024.2.0",
35 | "dask>=0.18.0",
36 | "polars",
37 | "tqdm",
38 | "deprecation",
39 | ]
40 |
41 | [project.urls]
42 | Homepage = "https://github.com/PyPSA/linopy"
43 | Source = "https://github.com/PyPSA/linopy"
44 |
45 | [project.optional-dependencies]
46 | docs = [
47 | "ipython==8.26.0",
48 | "numpydoc==1.7.0",
49 | "sphinx==7.3.7",
50 | "sphinx_rtd_theme==2.0.0",
51 | "sphinx_book_theme==1.1.3",
52 | "nbsphinx==0.9.4",
53 | "nbsphinx-link==1.3.0",
54 | "gurobipy==11.0.2",
55 | "ipykernel==6.29.5",
56 | "matplotlib==3.9.1",
57 | ]
58 | dev = [
59 | "pytest",
60 | "pytest-cov",
61 | 'mypy',
62 | "pre-commit",
63 | "netcdf4",
64 | "paramiko",
65 | "types-paramiko",
66 | "gurobipy",
67 | "highspy",
68 | ]
69 | solvers = [
70 | "gurobipy",
71 | "highspy>=1.5.0; python_version < '3.12'",
72 | "highspy>=1.7.1; python_version >= '3.12'",
73 | "cplex; platform_system != 'Darwin' and python_version < '3.12'",
74 | "mosek",
75 | "mindoptpy; python_version < '3.12'",
76 | "coptpy!=7.2.1",
77 | "xpress; platform_system != 'Darwin' and python_version < '3.11'",
78 | "pyscipopt; platform_system != 'Darwin'",
79 | ]
80 |
81 | [tool.setuptools.packages.find]
82 | include = ["linopy"]
83 |
84 | [tool.setuptools.package-data]
85 | "linopy" = ["py.typed"]
86 |
87 | [tool.setuptools_scm]
88 | write_to = "linopy/version.py"
89 | version_scheme = "no-guess-dev"
90 |
91 | [tool.coverage.run]
92 | branch = true
93 | source = ["linopy"]
94 | omit = ["test/*"]
95 | [tool.coverage.report]
96 | exclude_also = [
97 | "if TYPE_CHECKING:",
98 | ]
99 |
100 | [tool.mypy]
101 | exclude = ['dev/*', 'examples/*', 'benchmark/*', 'doc/*']
102 | ignore_missing_imports = true
103 | no_implicit_optional = true
104 | warn_unused_ignores = true
105 | show_error_code_links = true
106 | disallow_untyped_defs = true
107 | # disallow_any_generics = true
108 | # warn_return_any = true
109 |
110 | # [[tool.mypy.overrides]]
111 | # module = "linopy.*"
112 | # disallow_untyped_defs = true
113 |
114 | [tool.ruff]
115 | extend-include = ['*.ipynb']
116 |
117 | [tool.ruff.lint]
118 | select = [
119 | 'F', # pyflakes
120 | 'E', # pycodestyle: Error
121 | 'W', # pycodestyle: Warning
122 | 'I', # isort
123 | 'D', # pydocstyle
124 | 'UP', # pyupgrade
125 | 'TID', # flake8-tidy-imports
126 | 'NPY', # numpy
127 | ]
128 |
129 | ignore = [
130 | 'E501', # line too long
131 | 'E741', # ambiguous variable names
132 | 'D105', # Missing docstring in magic method
133 | 'D212', # Multi-line docstring summary should start at the second line
134 | 'D200', # One-line docstring should fit on one line with quotes
135 | 'D401', # First line should be in imperative mood
136 | 'D404', # First word of the docstring should not be "This
137 | 'D413', # Missing blank line after last section
138 |
139 | # pydocstyle ignores, which could be enabled in future when existing
140 | # issues are fixed
141 | 'D100', # Missing docstring in public module
142 | 'D101', # Missing docstring in public class
143 | 'D102', # Missing docstring in public method
144 | 'D103', # Missing docstring in public function
145 | 'D107', # Missing docstring in __init__
146 | 'D202', # No blank lines allowed after function docstring
147 | 'D203', # 1 blank line required before class docstring
148 | 'D205', # 1 blank line required between summary line and description
149 | 'D400', # First line should end with a period
150 | 'D415', # First line should end with a period, question mark, or exclamation point
151 | 'D417', # Missing argument descriptions in the docstring
152 |
153 | ]
154 |
155 | [tool.ruff.lint.per-file-ignores]
156 | "benchmark/notebooks/**.ipynb" = [
157 | "F821", # undefined-name - e.g. "snakemake is undefined"
158 | ]
159 | "benchmark/scripts/**.py" = [
160 | "F821", # undefined-name - e.g. "snakemake is undefined"
161 | ]
162 |
163 | [tool.ruff.lint.flake8-tidy-imports]
164 | # Disallow all relative imports.
165 | ban-relative-imports = "all"
166 |
--------------------------------------------------------------------------------
/test/test_compatible_arithmetrics.py:
--------------------------------------------------------------------------------
1 | from typing import Any
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import pytest
6 | import xarray as xr
7 | from xarray.testing import assert_equal
8 |
9 | from linopy import LESS_EQUAL, Model, Variable
10 | from linopy.testing import assert_linequal, assert_quadequal
11 |
12 |
13 | class SomeOtherDatatype:
14 | """
15 | A class that is not a subclass of xarray.DataArray, but stores data in a compatible way.
16 | It defines all necessary arithmetrics AND __array_ufunc__ to ensure that operations are
17 | performed on the active_data.
18 | """
19 |
20 | def __init__(self, data: xr.DataArray) -> None:
21 | self.data1 = data
22 | self.data2 = data.copy()
23 | self.active = 1
24 |
25 | def activate(self, active: int) -> None:
26 | self.active = active
27 |
28 | @property
29 | def active_data(self) -> xr.DataArray:
30 | return self.data1 if self.active == 1 else self.data2
31 |
32 | def __add__(self, other: Any) -> xr.DataArray:
33 | return self.active_data + other
34 |
35 | def __sub__(self, other: Any) -> xr.DataArray:
36 | return self.active_data - other
37 |
38 | def __mul__(self, other: Any) -> xr.DataArray:
39 | return self.active_data * other
40 |
41 | def __truediv__(self, other: Any) -> xr.DataArray:
42 | return self.active_data / other
43 |
44 | def __radd__(self, other: Any) -> Any:
45 | return other + self.active_data
46 |
47 | def __rsub__(self, other: Any) -> Any:
48 | return other - self.active_data
49 |
50 | def __rmul__(self, other: Any) -> Any:
51 | return other * self.active_data
52 |
53 | def __rtruediv__(self, other: Any) -> Any:
54 | return other / self.active_data
55 |
56 | def __neg__(self) -> xr.DataArray:
57 | return -self.active_data
58 |
59 | def __pos__(self) -> xr.DataArray:
60 | return +self.active_data
61 |
62 | def __abs__(self) -> xr.DataArray:
63 | return abs(self.active_data)
64 |
65 | def __array_ufunc__(self, ufunc, method, *inputs, **kwargs): # type: ignore
66 | # Ensure we always use the active_data when interacting with numpy/xarray operations
67 | new_inputs = [
68 | inp.active_data if isinstance(inp, SomeOtherDatatype) else inp
69 | for inp in inputs
70 | ]
71 | return getattr(ufunc, method)(*new_inputs, **kwargs)
72 |
73 |
74 | @pytest.fixture(
75 | params=[
76 | (pd.RangeIndex(10, name="first"),),
77 | (
78 | pd.Index(range(5), name="first"),
79 | pd.Index(range(3), name="second"),
80 | pd.Index(range(2), name="third"),
81 | ),
82 | ],
83 | ids=["single_dim", "multi_dim"],
84 | )
85 | def m(request) -> Model: # type: ignore
86 | m = Model()
87 | x = m.add_variables(coords=request.param, name="x")
88 | m.add_variables(0, 10, name="z")
89 | m.add_constraints(x, LESS_EQUAL, 0, name="c")
90 | return m
91 |
92 |
93 | def test_arithmetric_operations_variable(m: Model) -> None:
94 | x: Variable = m.variables["x"]
95 | rng = np.random.default_rng()
96 | data = xr.DataArray(rng.random(x.shape), coords=x.coords)
97 | other_datatype = SomeOtherDatatype(data.copy())
98 | assert_linequal(x + data, x + other_datatype)
99 | assert_linequal(x - data, x - other_datatype)
100 | assert_linequal(x * data, x * other_datatype)
101 | assert_linequal(x / data, x / other_datatype) # type: ignore
102 | assert_linequal(data * x, other_datatype * x) # type: ignore
103 | assert x.__add__(object()) is NotImplemented
104 | assert x.__sub__(object()) is NotImplemented
105 | assert x.__mul__(object()) is NotImplemented
106 | assert x.__truediv__(object()) is NotImplemented # type: ignore
107 | assert x.__pow__(object()) is NotImplemented # type: ignore
108 | with pytest.raises(ValueError):
109 | x.__pow__(3)
110 |
111 |
112 | def test_arithmetric_operations_expr(m: Model) -> None:
113 | x = m.variables["x"]
114 | expr = x + 3
115 | rng = np.random.default_rng()
116 | data = xr.DataArray(rng.random(x.shape), coords=x.coords)
117 | other_datatype = SomeOtherDatatype(data.copy())
118 | assert_linequal(expr + data, expr + other_datatype)
119 | assert_linequal(expr - data, expr - other_datatype)
120 | assert_linequal(expr * data, expr * other_datatype)
121 | assert_linequal(expr / data, expr / other_datatype)
122 | assert expr.__add__(object()) is NotImplemented
123 | assert expr.__sub__(object()) is NotImplemented
124 | assert expr.__mul__(object()) is NotImplemented
125 | assert expr.__truediv__(object()) is NotImplemented
126 |
127 |
128 | def test_arithmetric_operations_vars_and_expr(m: Model) -> None:
129 | x = m.variables["x"]
130 | x_expr = x * 1.0
131 |
132 | assert_quadequal(x**2, x_expr**2)
133 | assert_quadequal(x**2 + x, x + x**2)
134 | assert_quadequal(x**2 * 2, x**2 * 2)
135 | with pytest.raises(TypeError):
136 | _ = x**2 * x
137 |
138 |
139 | def test_arithmetric_operations_con(m: Model) -> None:
140 | c = m.constraints["c"]
141 | x = m.variables["x"]
142 | rng = np.random.default_rng()
143 | data = xr.DataArray(rng.random(x.shape), coords=x.coords)
144 | other_datatype = SomeOtherDatatype(data.copy())
145 | assert_linequal(c.lhs + data, c.lhs + other_datatype)
146 | assert_linequal(c.lhs - data, c.lhs - other_datatype)
147 | assert_linequal(c.lhs * data, c.lhs * other_datatype)
148 | assert_linequal(c.lhs / data, c.lhs / other_datatype)
149 |
150 | assert_equal(c.rhs + data, c.rhs + other_datatype)
151 | assert_equal(c.rhs - data, c.rhs - other_datatype)
152 | assert_equal(c.rhs * data, c.rhs * other_datatype)
153 | assert_equal(c.rhs / data, c.rhs / other_datatype)
154 |
--------------------------------------------------------------------------------
/test/test_constraints.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Wed Mar 10 11:23:13 2021.
4 |
5 | @author: fabulous
6 | """
7 |
8 | import dask
9 | import dask.array.core
10 | import numpy as np
11 | import pandas as pd
12 | import pytest
13 | import xarray as xr
14 |
15 | from linopy import EQUAL, GREATER_EQUAL, LESS_EQUAL, Model
16 | from linopy.testing import assert_conequal
17 |
18 | # Test model functions
19 |
20 |
21 | def test_constraint_assignment() -> None:
22 | m: Model = Model()
23 |
24 | lower: xr.DataArray = xr.DataArray(
25 | np.zeros((10, 10)), coords=[range(10), range(10)]
26 | )
27 | upper: xr.DataArray = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
28 | x = m.add_variables(lower, upper, name="x")
29 | y = m.add_variables(name="y")
30 |
31 | con0 = m.add_constraints(1 * x + 10 * y, EQUAL, 0)
32 |
33 | for attr in m.constraints.dataset_attrs:
34 | assert "con0" in getattr(m.constraints, attr)
35 |
36 | assert m.constraints.labels.con0.shape == (10, 10)
37 | assert m.constraints.labels.con0.dtype == int
38 | assert m.constraints.coeffs.con0.dtype in (int, float)
39 | assert m.constraints.vars.con0.dtype in (int, float)
40 | assert m.constraints.rhs.con0.dtype in (int, float)
41 |
42 | assert_conequal(m.constraints.con0, con0)
43 |
44 |
45 | def test_constraints_getattr_formatted() -> None:
46 | m: Model = Model()
47 | x = m.add_variables(0, 10, name="x")
48 | m.add_constraints(1 * x == 0, name="con-0")
49 | assert_conequal(m.constraints.con_0, m.constraints["con-0"])
50 |
51 |
52 | def test_anonymous_constraint_assignment() -> None:
53 | m: Model = Model()
54 |
55 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
56 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
57 | x = m.add_variables(lower, upper, name="x")
58 | y = m.add_variables(name="y")
59 | con = 1 * x + 10 * y == 0
60 | m.add_constraints(con)
61 |
62 | for attr in m.constraints.dataset_attrs:
63 | assert "con0" in getattr(m.constraints, attr)
64 |
65 | assert m.constraints.labels.con0.shape == (10, 10)
66 | assert m.constraints.labels.con0.dtype == int
67 | assert m.constraints.coeffs.con0.dtype in (int, float)
68 | assert m.constraints.vars.con0.dtype in (int, float)
69 | assert m.constraints.rhs.con0.dtype in (int, float)
70 |
71 |
72 | def test_constraint_assignment_with_tuples() -> None:
73 | m: Model = Model()
74 |
75 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
76 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
77 | x = m.add_variables(lower, upper)
78 | y = m.add_variables()
79 |
80 | m.add_constraints([(1, x), (10, y)], EQUAL, 0, name="c")
81 | for attr in m.constraints.dataset_attrs:
82 | assert "c" in getattr(m.constraints, attr)
83 | assert m.constraints.labels.c.shape == (10, 10)
84 |
85 |
86 | def test_constraint_assignment_chunked() -> None:
87 | # setting bounds with one pd.DataFrame and one pd.Series
88 | m: Model = Model(chunk=5)
89 | lower = pd.DataFrame(np.zeros((10, 10)))
90 | upper = pd.Series(np.ones(10))
91 | x = m.add_variables(lower, upper)
92 | m.add_constraints(x, GREATER_EQUAL, 0, name="c")
93 | assert m.constraints.coeffs.c.data.shape == (
94 | 10,
95 | 10,
96 | 1,
97 | )
98 | assert isinstance(m.constraints.coeffs.c.data, dask.array.core.Array)
99 |
100 |
101 | def test_constraint_assignment_with_reindex() -> None:
102 | m: Model = Model()
103 |
104 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
105 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
106 | x = m.add_variables(lower, upper, name="x")
107 | y = m.add_variables(name="y")
108 |
109 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
110 |
111 | shuffled_coords = [2, 1, 3, 4, 6, 5, 7, 9, 8, 0]
112 |
113 | con = x.loc[shuffled_coords] + y >= 10
114 | assert (con.coords["dim_0"].values == shuffled_coords).all()
115 |
116 |
117 | def test_wrong_constraint_assignment_repeated() -> None:
118 | # repeated variable assignment is forbidden
119 | m: Model = Model()
120 | x = m.add_variables()
121 | m.add_constraints(x, LESS_EQUAL, 0, name="con")
122 | with pytest.raises(ValueError):
123 | m.add_constraints(x, LESS_EQUAL, 0, name="con")
124 |
125 |
126 | def test_masked_constraints() -> None:
127 | m: Model = Model()
128 |
129 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
130 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
131 | x = m.add_variables(lower, upper)
132 | y = m.add_variables()
133 |
134 | mask = pd.Series([True] * 5 + [False] * 5)
135 | m.add_constraints(1 * x + 10 * y, EQUAL, 0, mask=mask)
136 | assert (m.constraints.labels.con0[0:5, :] != -1).all()
137 | assert (m.constraints.labels.con0[5:10, :] == -1).all()
138 |
139 |
140 | def test_non_aligned_constraints() -> None:
141 | m: Model = Model()
142 |
143 | lower = xr.DataArray(np.zeros(10), coords=[range(10)])
144 | x = m.add_variables(lower, name="x")
145 |
146 | lower = xr.DataArray(np.zeros(8), coords=[range(8)])
147 | y = m.add_variables(lower, name="y")
148 |
149 | m.add_constraints(x == 0.0)
150 | m.add_constraints(y == 0.0)
151 |
152 | with pytest.warns(UserWarning):
153 | m.constraints.labels
154 |
155 | for dtype in m.constraints.labels.dtypes.values():
156 | assert np.issubdtype(dtype, np.integer)
157 |
158 | for dtype in m.constraints.coeffs.dtypes.values():
159 | assert np.issubdtype(dtype, np.floating)
160 |
161 | for dtype in m.constraints.vars.dtypes.values():
162 | assert np.issubdtype(dtype, np.integer)
163 |
164 | for dtype in m.constraints.rhs.dtypes.values():
165 | assert np.issubdtype(dtype, np.floating)
166 |
167 |
168 | def test_constraints_flat() -> None:
169 | m: Model = Model()
170 |
171 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
172 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
173 | x = m.add_variables(lower, upper)
174 | y = m.add_variables()
175 |
176 | assert isinstance(m.constraints.flat, pd.DataFrame)
177 | assert m.constraints.flat.empty
178 | with pytest.raises(ValueError):
179 | m.constraints.to_matrix()
180 |
181 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
182 | m.add_constraints(1 * x + 10 * y, LESS_EQUAL, 0)
183 | m.add_constraints(1 * x + 10 * y, GREATER_EQUAL, 0)
184 |
185 | assert isinstance(m.constraints.flat, pd.DataFrame)
186 | assert not m.constraints.flat.empty
187 |
188 |
189 | def test_sanitize_infinities() -> None:
190 | m: Model = Model()
191 |
192 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
193 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
194 | x = m.add_variables(lower, upper, name="x")
195 | y = m.add_variables(name="y")
196 |
197 | # Test correct infinities
198 | m.add_constraints(x <= np.inf, name="con_inf")
199 | m.add_constraints(y >= -np.inf, name="con_neg_inf")
200 | m.constraints.sanitize_infinities()
201 | assert (m.constraints["con_inf"].labels == -1).all()
202 | assert (m.constraints["con_neg_inf"].labels == -1).all()
203 |
204 | # Test incorrect infinities
205 | with pytest.raises(ValueError):
206 | m.add_constraints(x >= np.inf, name="con_wrong_inf")
207 | with pytest.raises(ValueError):
208 | m.add_constraints(y <= -np.inf, name="con_wrong_neg_inf")
209 |
--------------------------------------------------------------------------------
/test/test_examples.py:
--------------------------------------------------------------------------------
1 | """
2 | Test module for the examples module.
3 | """
4 |
5 | from linopy import Model
6 | from linopy.examples import (
7 | benchmark_model,
8 | simple_two_array_variables_model,
9 | simple_two_single_variables_model,
10 | )
11 |
12 |
13 | def test_simple_two_single_variables_model() -> None:
14 | """
15 | Test function for the simple_two_single_variables_model.
16 | """
17 | model = simple_two_single_variables_model()
18 | assert isinstance(model, Model)
19 |
20 |
21 | def test_simple_two_array_variables_model() -> None:
22 | """
23 | Test function for the simple_two_array_variables_model.
24 | """
25 | model = simple_two_array_variables_model()
26 | assert isinstance(model, Model)
27 |
28 |
29 | def test_benchmark_model() -> None:
30 | """
31 | Test function for the benchmark_model.
32 | """
33 | model = benchmark_model()
34 | assert isinstance(model, Model)
35 |
36 |
37 | def test_benchmark_model_with_integer_labels() -> None:
38 | """
39 | Test function for the benchmark_model with integer labels.
40 | """
41 | model = benchmark_model(integerlabels=True)
42 | assert isinstance(model, Model)
43 |
--------------------------------------------------------------------------------
/test/test_inconsistency_checks.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Mon Feb 28 15:47:32 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pytest
10 |
11 | from linopy import GREATER_EQUAL, Model
12 |
13 |
14 | def test_nan_in_variable_lower() -> None:
15 | m: Model = Model()
16 |
17 | x = m.add_variables(lower=np.nan, name="x")
18 | y = m.add_variables(name="y")
19 |
20 | m.add_constraints(2 * x + 6 * y, GREATER_EQUAL, 10)
21 | m.add_constraints(4 * x + 2 * y, GREATER_EQUAL, 3)
22 |
23 | m.add_objective(2 * y + x)
24 |
25 | with pytest.raises(ValueError):
26 | m.solve()
27 |
28 |
29 | def test_nan_in_variable_upper() -> None:
30 | m: Model = Model()
31 |
32 | x = m.add_variables(upper=np.nan, name="x")
33 | y = m.add_variables(name="y")
34 |
35 | m.add_constraints(2 * x + 6 * y, GREATER_EQUAL, 10)
36 | m.add_constraints(4 * x + 2 * y, GREATER_EQUAL, 3)
37 |
38 | m.add_objective(2 * y + x)
39 | with pytest.raises(ValueError):
40 | m.solve()
41 |
42 |
43 | def test_nan_in_constraint_sign() -> None:
44 | m: Model = Model()
45 |
46 | x = m.add_variables(name="x")
47 | y = m.add_variables(name="y")
48 |
49 | with pytest.raises(ValueError):
50 | m.add_constraints(2 * x + 6 * y, np.nan, 10)
51 |
52 |
53 | # TODO: this requires a strict propagation of the NaN values across expressions
54 | # def test_nan_in_constraint_rhs():
55 | # m = Model()
56 |
57 | # x = m.add_variables(name="x")
58 | # y = m.add_variables(name="y")
59 |
60 | # m.add_constraints(2 * x + 6 * y, GREATER_EQUAL, np.nan)
61 | # m.add_constraints(4 * x + 2 * y, GREATER_EQUAL, 3)
62 |
63 | # m.add_objective(2 * y + x)
64 |
65 | # with pytest.raises(ValueError):
66 | # m.solve()
67 |
68 |
69 | # TODO: this requires a strict propagation of the NaN values across expressions
70 | # def test_nan_in_objective():
71 | # m = Model()
72 |
73 | # x = m.add_variables(name="x")
74 | # y = m.add_variables(name="y")
75 |
76 | # m.add_constraints(2 * x + 6 * y, GREATER_EQUAL, np.nan)
77 | # m.add_constraints(4 * x + 2 * y, GREATER_EQUAL, 3)
78 |
79 | # m.add_objective(np.nan * y + x)
80 |
81 | # with pytest.raises(ValueError):
82 | # m.solve()
83 |
--------------------------------------------------------------------------------
/test/test_io.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Thu Mar 18 09:03:35 2021.
4 |
5 | @author: fabian
6 | """
7 |
8 | import pickle
9 | from pathlib import Path
10 | from typing import Union
11 |
12 | import pandas as pd
13 | import pytest
14 | import xarray as xr
15 |
16 | from linopy import LESS_EQUAL, Model, available_solvers, read_netcdf
17 | from linopy.testing import assert_model_equal
18 |
19 |
20 | @pytest.fixture
21 | def model() -> Model:
22 | m = Model()
23 |
24 | x = m.add_variables(4, pd.Series([8, 10]), name="x")
25 | y = m.add_variables(0, pd.DataFrame([[1, 2], [3, 4]]), name="y")
26 |
27 | m.add_constraints(x + y, LESS_EQUAL, 10)
28 |
29 | m.add_objective(2 * x + 3 * y)
30 |
31 | m.parameters["param"] = xr.DataArray([1, 2, 3, 4], dims=["x"])
32 |
33 | return m
34 |
35 |
36 | @pytest.fixture
37 | def model_with_dash_names() -> Model:
38 | m = Model()
39 |
40 | x = m.add_variables(4, pd.Series([8, 10]), name="x-var")
41 | x = m.add_variables(4, pd.Series([8, 10]), name="x-var-2")
42 | y = m.add_variables(0, pd.DataFrame([[1, 2], [3, 4]]), name="y-var")
43 |
44 | m.add_constraints(x + y, LESS_EQUAL, 10, name="constraint-1")
45 |
46 | m.add_objective(2 * x + 3 * y)
47 |
48 | return m
49 |
50 |
51 | @pytest.fixture
52 | def model_with_multiindex() -> Model:
53 | m = Model()
54 |
55 | index = pd.MultiIndex.from_tuples(
56 | [(1, "a"), (1, "b"), (2, "a"), (2, "b")], names=["first", "second"]
57 | )
58 | x = m.add_variables(4, pd.Series([8, 10, 12, 14], index=index), name="x-var")
59 | y = m.add_variables(
60 | 0, pd.DataFrame([[1, 2], [3, 4], [5, 6], [7, 8]], index=index), name="y-var"
61 | )
62 |
63 | m.add_constraints(x + y, LESS_EQUAL, 10, name="constraint-1")
64 |
65 | m.add_objective(2 * x + 3 * y)
66 |
67 | return m
68 |
69 |
70 | def test_model_to_netcdf(model: Model, tmp_path: Path) -> None:
71 | m = model
72 | fn = tmp_path / "test.nc"
73 | m.to_netcdf(fn)
74 | p = read_netcdf(fn)
75 |
76 | assert_model_equal(m, p)
77 |
78 |
79 | def test_model_to_netcdf_with_sense(model: Model, tmp_path: Path) -> None:
80 | m = model
81 | m.objective.sense = "max"
82 | fn = tmp_path / "test.nc"
83 | m.to_netcdf(fn)
84 | p = read_netcdf(fn)
85 |
86 | assert_model_equal(m, p)
87 |
88 |
89 | def test_model_to_netcdf_with_dash_names(
90 | model_with_dash_names: Model, tmp_path: Path
91 | ) -> None:
92 | m = model_with_dash_names
93 | fn = tmp_path / "test.nc"
94 | m.to_netcdf(fn)
95 | p = read_netcdf(fn)
96 |
97 | assert_model_equal(m, p)
98 |
99 |
100 | def test_model_to_netcdf_with_status_and_condition(
101 | model_with_dash_names: Model, tmp_path: Path
102 | ) -> None:
103 | m = model_with_dash_names
104 | fn = tmp_path / "test.nc"
105 | m._status = "ok"
106 | m._termination_condition = "optimal"
107 | m.to_netcdf(fn)
108 | p = read_netcdf(fn)
109 |
110 | assert_model_equal(m, p)
111 |
112 |
113 | def test_pickle_model(model_with_dash_names: Model, tmp_path: Path) -> None:
114 | m = model_with_dash_names
115 | fn = tmp_path / "test.nc"
116 | m._status = "ok"
117 | m._termination_condition = "optimal"
118 |
119 | with open(fn, "wb") as f:
120 | pickle.dump(m, f)
121 |
122 | with open(fn, "rb") as f:
123 | p = pickle.load(f)
124 |
125 | assert_model_equal(m, p)
126 |
127 |
128 | # skip it xarray version is 2024.01.0 due to issue https://github.com/pydata/xarray/issues/8628
129 | @pytest.mark.skipif(
130 | xr.__version__ in ["2024.1.0", "2024.1.1"],
131 | reason="xarray version 2024.1.0 has a bug with MultiIndex deserialize",
132 | )
133 | def test_model_to_netcdf_with_multiindex(
134 | model_with_multiindex: Model, tmp_path: Path
135 | ) -> None:
136 | m = model_with_multiindex
137 | fn = tmp_path / "test.nc"
138 | m.to_netcdf(fn)
139 | p = read_netcdf(fn)
140 |
141 | assert_model_equal(m, p)
142 |
143 |
144 | @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
145 | def test_to_file_lp(model: Model, tmp_path: Path) -> None:
146 | import gurobipy
147 |
148 | fn = tmp_path / "test.lp"
149 | model.to_file(fn)
150 |
151 | gurobipy.read(str(fn))
152 |
153 |
154 | @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
155 | def test_to_file_lp_explicit_coordinate_names(model: Model, tmp_path: Path) -> None:
156 | import gurobipy
157 |
158 | fn = tmp_path / "test.lp"
159 | model.to_file(fn, io_api="lp", explicit_coordinate_names=True)
160 |
161 | gurobipy.read(str(fn))
162 |
163 |
164 | @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
165 | def test_to_file_lp_None(model: Model) -> None:
166 | import gurobipy
167 |
168 | fn: Union[str, None] = None
169 | model.to_file(fn)
170 |
171 | fn_path = model.get_problem_file()
172 | gurobipy.read(str(fn_path))
173 |
174 |
175 | @pytest.mark.skipif(
176 | not {"gurobi", "highs"}.issubset(available_solvers),
177 | reason="Gurobipy of highspy not installed",
178 | )
179 | def test_to_file_mps(model: Model, tmp_path: Path) -> None:
180 | import gurobipy
181 |
182 | fn = tmp_path / "test.mps"
183 | model.to_file(fn)
184 |
185 | gurobipy.read(str(fn))
186 |
187 |
188 | @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
189 | def test_to_file_invalid(model: Model, tmp_path: Path) -> None:
190 | with pytest.raises(ValueError):
191 | fn = tmp_path / "test.failedtype"
192 | model.to_file(fn)
193 |
194 |
195 | @pytest.mark.skipif("gurobi" not in available_solvers, reason="Gurobipy not installed")
196 | def test_to_gurobipy(model: Model) -> None:
197 | model.to_gurobipy()
198 |
199 |
200 | @pytest.mark.skipif("highs" not in available_solvers, reason="Highspy not installed")
201 | def test_to_highspy(model: Model) -> None:
202 | model.to_highspy()
203 |
204 |
205 | def test_to_blocks(tmp_path: Path) -> None:
206 | m: Model = Model()
207 |
208 | lower: pd.Series = pd.Series(range(20))
209 | upper: pd.Series = pd.Series(range(30, 50))
210 | x = m.add_variables(lower, upper)
211 | y = m.add_variables(lower, upper)
212 |
213 | m.add_constraints(x + y, LESS_EQUAL, 10)
214 |
215 | m.add_objective(2 * x + 3 * y)
216 |
217 | m.blocks = xr.DataArray([1] * 10 + [2] * 10)
218 |
219 | with pytest.raises(NotImplementedError):
220 | m.to_block_files(tmp_path)
221 |
--------------------------------------------------------------------------------
/test/test_matrices.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Mon Oct 10 14:21:23 2022.
4 |
5 | @author: fabian
6 | """
7 |
8 | import numpy as np
9 | import pandas as pd
10 | import xarray as xr
11 |
12 | from linopy import EQUAL, GREATER_EQUAL, Model
13 |
14 |
15 | def test_basic_matrices() -> None:
16 | m = Model()
17 |
18 | lower = xr.DataArray(np.zeros((10, 10)), coords=[range(10), range(10)])
19 | upper = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
20 | x = m.add_variables(lower, upper, name="x")
21 | y = m.add_variables(name="y")
22 |
23 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
24 |
25 | obj = (10 * x + 5 * y).sum()
26 | m.add_objective(obj)
27 |
28 | assert m.matrices.A is not None
29 | assert m.matrices.A.shape == (*m.matrices.clabels.shape, *m.matrices.vlabels.shape)
30 | assert m.matrices.clabels.shape == m.matrices.sense.shape
31 | assert m.matrices.vlabels.shape == m.matrices.ub.shape
32 | assert m.matrices.vlabels.shape == m.matrices.lb.shape
33 |
34 |
35 | def test_basic_matrices_masked() -> None:
36 | m = Model()
37 |
38 | lower = pd.Series(0, range(10))
39 | x = m.add_variables(lower, name="x")
40 | mask = pd.Series([True] * 8 + [False, False])
41 | y = m.add_variables(lower, name="y", mask=mask)
42 |
43 | m.add_constraints(x + y, GREATER_EQUAL, 10)
44 |
45 | m.add_constraints(y, GREATER_EQUAL, 0)
46 |
47 | m.add_objective(2 * x + y)
48 |
49 | assert m.matrices.A is not None
50 | assert m.matrices.A.shape == (*m.matrices.clabels.shape, *m.matrices.vlabels.shape)
51 | assert m.matrices.clabels.shape == m.matrices.sense.shape
52 | assert m.matrices.vlabels.shape == m.matrices.ub.shape
53 | assert m.matrices.vlabels.shape == m.matrices.lb.shape
54 |
55 |
56 | def test_matrices_duplicated_variables() -> None:
57 | m = Model()
58 |
59 | x = m.add_variables(pd.Series([0, 0]), 1, name="x")
60 | y = m.add_variables(4, pd.Series([8, 10]), name="y")
61 | z = m.add_variables(0, pd.DataFrame([[1, 2], [3, 4], [5, 6]]).T, name="z")
62 | m.add_constraints(x + x + y + y + z + z == 0)
63 |
64 | assert m.matrices.A is not None
65 |
66 | A = m.matrices.A.todense()
67 | assert A[0, 0] == 2
68 | assert np.isin(np.unique(np.array(A)), [0.0, 2.0]).all()
69 |
70 |
71 | def test_matrices_float_c() -> None:
72 | # https://github.com/PyPSA/linopy/issues/200
73 | m = Model()
74 |
75 | x = m.add_variables(pd.Series([0, 0]), 1, name="x")
76 | m.add_objective(x * 1.5)
77 |
78 | c = m.matrices.c
79 | assert np.all(c == np.array([1.5, 1.5]))
80 |
--------------------------------------------------------------------------------
/test/test_model.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Test function defined in the Model class.
4 | """
5 |
6 | from __future__ import annotations
7 |
8 | from pathlib import Path
9 | from tempfile import gettempdir
10 |
11 | import numpy as np
12 | import pytest
13 | import xarray as xr
14 |
15 | from linopy import EQUAL, Model
16 | from linopy.testing import assert_model_equal
17 |
18 | target_shape: tuple[int, int] = (10, 10)
19 |
20 |
21 | def test_model_repr() -> None:
22 | m: Model = Model()
23 | m.__repr__()
24 |
25 |
26 | def test_model_force_dims_names() -> None:
27 | m: Model = Model(force_dim_names=True)
28 | with pytest.raises(ValueError):
29 | m.add_variables([-5], [10])
30 |
31 |
32 | def test_model_solver_dir() -> None:
33 | d: str = gettempdir()
34 | m: Model = Model(solver_dir=d)
35 | assert m.solver_dir == Path(d)
36 |
37 |
38 | def test_model_variable_getitem() -> None:
39 | m = Model()
40 | x = m.add_variables(name="x")
41 | assert m["x"].labels == x.labels
42 |
43 |
44 | def test_coefficient_range() -> None:
45 | m: Model = Model()
46 |
47 | lower: xr.DataArray = xr.DataArray(
48 | np.zeros((10, 10)), coords=[range(10), range(10)]
49 | )
50 | upper: xr.DataArray = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
51 | x = m.add_variables(lower, upper)
52 | y = m.add_variables()
53 |
54 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
55 | assert m.coefficientrange["min"].con0 == 1
56 | assert m.coefficientrange["max"].con0 == 10
57 |
58 |
59 | def test_objective() -> None:
60 | m: Model = Model()
61 |
62 | lower: xr.DataArray = xr.DataArray(
63 | np.zeros((10, 10)), coords=[range(10), range(10)]
64 | )
65 | upper: xr.DataArray = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
66 | x = m.add_variables(lower, upper, name="x")
67 | y = m.add_variables(lower, upper, name="y")
68 |
69 | obj1 = (10 * x + 5 * y).sum()
70 | m.add_objective(obj1)
71 | assert m.objective.vars.size == 200
72 |
73 | # test overwriting
74 | obj2 = (2 * x).sum()
75 | m.add_objective(obj2, overwrite=True)
76 |
77 | # test Tuple
78 | obj3 = [(2, x)]
79 | m.add_objective(obj3, overwrite=True)
80 |
81 | # test objective range
82 | assert m.objectiverange.min() == 2
83 | assert m.objectiverange.max() == 2
84 |
85 | # test objective with constant which is not supported
86 | with pytest.raises(ValueError):
87 | m.objective = m.objective + 3
88 |
89 |
90 | def test_remove_variable() -> None:
91 | m: Model = Model()
92 |
93 | lower: xr.DataArray = xr.DataArray(
94 | np.zeros((10, 10)), coords=[range(10), range(10)]
95 | )
96 | upper: xr.DataArray = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
97 | x = m.add_variables(lower, upper, name="x")
98 | y = m.add_variables(name="y")
99 |
100 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
101 |
102 | obj = (10 * x + 5 * y).sum()
103 | m.add_objective(obj)
104 |
105 | assert "x" in m.variables
106 |
107 | m.remove_variables("x")
108 | assert "x" not in m.variables
109 |
110 | assert not m.constraints.con0.vars.isin(x.labels).any()
111 |
112 | assert not m.objective.vars.isin(x.labels).any()
113 |
114 |
115 | def test_remove_constraint() -> None:
116 | m: Model = Model()
117 |
118 | x = m.add_variables()
119 | m.add_constraints(x, EQUAL, 0, name="x")
120 | m.remove_constraints("x")
121 | assert not len(m.constraints.labels)
122 |
123 |
124 | def test_remove_constraints_with_list() -> None:
125 | m: Model = Model()
126 |
127 | x = m.add_variables()
128 | y = m.add_variables()
129 | m.add_constraints(x, EQUAL, 0, name="constraint_x")
130 | m.add_constraints(y, EQUAL, 0, name="constraint_y")
131 | m.remove_constraints(["constraint_x", "constraint_y"])
132 | assert "constraint_x" not in m.constraints.labels
133 | assert "constraint_y" not in m.constraints.labels
134 | assert not len(m.constraints.labels)
135 |
136 |
137 | def test_remove_objective() -> None:
138 | m: Model = Model()
139 |
140 | lower: xr.DataArray = xr.DataArray(np.zeros((2, 2)), coords=[range(2), range(2)])
141 | upper: xr.DataArray = xr.DataArray(np.ones((2, 2)), coords=[range(2), range(2)])
142 | x = m.add_variables(lower, upper, name="x")
143 | y = m.add_variables(lower, upper, name="y")
144 | obj = (10 * x + 5 * y).sum()
145 | m.add_objective(obj)
146 | m.remove_objective()
147 | assert not len(m.objective.vars)
148 |
149 |
150 | def test_assert_model_equal() -> None:
151 | m: Model = Model()
152 |
153 | lower: xr.DataArray = xr.DataArray(
154 | np.zeros((10, 10)), coords=[range(10), range(10)]
155 | )
156 | upper: xr.DataArray = xr.DataArray(np.ones((10, 10)), coords=[range(10), range(10)])
157 | x = m.add_variables(lower, upper, name="x")
158 | y = m.add_variables(name="y")
159 |
160 | m.add_constraints(1 * x + 10 * y, EQUAL, 0)
161 |
162 | obj = (10 * x + 5 * y).sum()
163 | m.add_objective(obj)
164 |
165 | assert_model_equal(m, m)
166 |
--------------------------------------------------------------------------------
/test/test_objective.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import pytest
3 | import xarray as xr
4 | import xarray.core.indexes
5 | import xarray.core.utils
6 | from scipy.sparse import csc_matrix
7 |
8 | from linopy import Model
9 | from linopy.expressions import LinearExpression, QuadraticExpression
10 | from linopy.objective import Objective
11 |
12 |
13 | @pytest.fixture
14 | def linear_objective() -> Objective:
15 | m = Model()
16 | v = m.add_variables(coords=[[1, 2, 3]])
17 | m.objective = Objective(1 * v, m, sense="min")
18 | return m.objective
19 |
20 |
21 | @pytest.fixture
22 | def quadratic_objective() -> Objective:
23 | m = Model()
24 | v = m.add_variables(coords=[[1, 2, 3]])
25 | m.objective = Objective(v * v, m, sense="max")
26 | return m.objective
27 |
28 |
29 | def test_model(linear_objective: Objective, quadratic_objective: Objective) -> None:
30 | assert isinstance(linear_objective.model, Model)
31 | assert isinstance(quadratic_objective.model, Model)
32 |
33 |
34 | def test_sense(linear_objective: Objective, quadratic_objective: Objective) -> None:
35 | assert linear_objective.sense == "min"
36 | assert quadratic_objective.sense == "max"
37 |
38 | assert linear_objective.model.sense == "min"
39 | assert quadratic_objective.model.sense == "max"
40 |
41 |
42 | def test_set_sense(linear_objective: Objective, quadratic_objective: Objective) -> None:
43 | linear_objective.sense = "max"
44 | quadratic_objective.sense = "min"
45 |
46 | assert linear_objective.sense == "max"
47 | assert quadratic_objective.sense == "min"
48 |
49 | assert linear_objective.model.sense == "max"
50 | assert quadratic_objective.model.sense == "min"
51 |
52 |
53 | def test_set_sense_via_model(
54 | linear_objective: Objective, quadratic_objective: Objective
55 | ) -> None:
56 | linear_objective.model.sense = "max"
57 | quadratic_objective.model.sense = "min"
58 |
59 | assert linear_objective.sense == "max"
60 | assert quadratic_objective.sense == "min"
61 |
62 |
63 | def test_sense_setter_error(linear_objective: Objective) -> None:
64 | with pytest.raises(ValueError):
65 | linear_objective.sense = "not min or max"
66 |
67 |
68 | def test_variables_inherited_properties(linear_objective: Objective) -> None:
69 | assert isinstance(linear_objective.attrs, dict)
70 | assert isinstance(linear_objective.coords, xr.Coordinates)
71 | assert isinstance(linear_objective.indexes, xarray.core.indexes.Indexes)
72 | assert isinstance(linear_objective.sizes, xarray.core.utils.Frozen)
73 |
74 | assert isinstance(linear_objective.flat, pd.DataFrame)
75 | assert isinstance(linear_objective.vars, xr.DataArray)
76 | assert isinstance(linear_objective.coeffs, xr.DataArray)
77 | assert isinstance(linear_objective.nterm, int)
78 |
79 |
80 | def test_expression(
81 | linear_objective: Objective, quadratic_objective: Objective
82 | ) -> None:
83 | assert isinstance(linear_objective.expression, LinearExpression)
84 | assert isinstance(quadratic_objective.expression, QuadraticExpression)
85 |
86 |
87 | def test_value(linear_objective: Objective, quadratic_objective: Objective) -> None:
88 | assert linear_objective.value is None
89 | assert quadratic_objective.value is None
90 |
91 |
92 | def test_set_value(linear_objective: Objective, quadratic_objective: Objective) -> None:
93 | linear_objective.set_value(1)
94 | quadratic_objective.set_value(2)
95 | assert linear_objective.value == 1
96 | assert quadratic_objective.value == 2
97 |
98 |
99 | def test_set_value_error(linear_objective: Objective) -> None:
100 | with pytest.raises(ValueError):
101 | linear_objective.set_value("not a number") # type: ignore
102 |
103 |
104 | def test_assign(linear_objective: Objective) -> None:
105 | assert isinstance(linear_objective.assign(one=1), Objective)
106 |
107 |
108 | def test_sel(linear_objective: Objective) -> None:
109 | assert isinstance(linear_objective.sel(_term=[]), Objective)
110 |
111 |
112 | def test_is_linear(linear_objective: Objective, quadratic_objective: Objective) -> None:
113 | assert linear_objective.is_linear is True
114 | assert quadratic_objective.is_linear is False
115 |
116 |
117 | def test_is_quadratic(
118 | linear_objective: Objective, quadratic_objective: Objective
119 | ) -> None:
120 | assert linear_objective.is_quadratic is False
121 | assert quadratic_objective.is_quadratic is True
122 |
123 |
124 | def test_to_matrix(linear_objective: Objective, quadratic_objective: Objective) -> None:
125 | with pytest.raises(ValueError):
126 | linear_objective.to_matrix()
127 | assert isinstance(quadratic_objective.to_matrix(), csc_matrix)
128 |
129 |
130 | def test_add(linear_objective: Objective, quadratic_objective: Objective) -> None:
131 | obj = linear_objective + quadratic_objective
132 | assert isinstance(obj, Objective)
133 | assert isinstance(obj.expression, QuadraticExpression)
134 |
135 |
136 | def test_add_expr(linear_objective: Objective, quadratic_objective: Objective) -> None:
137 | obj = linear_objective + quadratic_objective.expression
138 | assert isinstance(obj, Objective)
139 | assert isinstance(obj.expression, QuadraticExpression)
140 |
141 |
142 | def test_sub(linear_objective: Objective, quadratic_objective: Objective) -> None:
143 | obj = quadratic_objective - linear_objective
144 | assert isinstance(obj, Objective)
145 | assert isinstance(obj.expression, QuadraticExpression)
146 |
147 |
148 | def test_sub_epxr(linear_objective: Objective, quadratic_objective: Objective) -> None:
149 | obj = quadratic_objective - linear_objective.expression
150 | assert isinstance(obj, Objective)
151 | assert isinstance(obj.expression, QuadraticExpression)
152 |
153 |
154 | def test_mul(quadratic_objective: Objective) -> None:
155 | obj = quadratic_objective * 2
156 | assert isinstance(obj, Objective)
157 | assert isinstance(obj.expression, QuadraticExpression)
158 |
159 |
160 | def test_neg(quadratic_objective: Objective) -> None:
161 | obj = -quadratic_objective
162 | assert isinstance(obj, Objective)
163 | assert isinstance(obj.expression, QuadraticExpression)
164 |
165 |
166 | def test_truediv(quadratic_objective: Objective) -> None:
167 | obj = quadratic_objective / 2
168 | assert isinstance(obj, Objective)
169 | assert isinstance(obj.expression, QuadraticExpression)
170 |
171 |
172 | def test_truediv_false(quadratic_objective: Objective) -> None:
173 | with pytest.raises(ValueError):
174 | quadratic_objective / quadratic_objective
175 |
176 |
177 | def test_repr(linear_objective: Objective, quadratic_objective: Objective) -> None:
178 | assert isinstance(linear_objective.__repr__(), str)
179 | assert isinstance(quadratic_objective.__repr__(), str)
180 |
181 | assert "Linear" in linear_objective.__repr__()
182 | assert "Quadratic" in quadratic_objective.__repr__()
183 |
184 |
185 | def test_objective_constant() -> None:
186 | m = Model()
187 | linear_expr = LinearExpression(None, m) + 1
188 | with pytest.raises(ValueError):
189 | m.objective = Objective(linear_expr, m)
190 |
--------------------------------------------------------------------------------
/test/test_options.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 |
4 | import pytest
5 |
6 | from linopy.config import OptionSettings
7 |
8 |
9 | @pytest.fixture
10 | def options() -> OptionSettings:
11 | return OptionSettings(a=1, b=2, c=3)
12 |
13 |
14 | def test_set_value(options: OptionSettings) -> None:
15 | options.set_value(a=10)
16 | assert options._current_values == {"a": 10, "b": 2, "c": 3}
17 |
18 | with pytest.raises(KeyError, match="d is not a valid setting."):
19 | options.set_value(d=20)
20 |
21 |
22 | def test_get_value(options: OptionSettings) -> None:
23 | assert options.get_value("a") == 1
24 |
25 | with pytest.raises(KeyError, match="d is not a valid setting."):
26 | options.get_value("d")
27 |
28 |
29 | def test_call(options: OptionSettings) -> None:
30 | options(a=10)
31 | assert options._current_values == {"a": 10, "b": 2, "c": 3}
32 |
33 | with pytest.raises(KeyError, match="d is not a valid setting."):
34 | options(d=20)
35 |
36 |
37 | def test_getitem(options: OptionSettings) -> None:
38 | assert options["a"] == 1
39 |
40 | with pytest.raises(KeyError, match="d is not a valid setting."):
41 | options["d"]
42 |
43 |
44 | def test_setitem(options: OptionSettings) -> None:
45 | options["a"] = 10
46 | assert options._current_values == {"a": 10, "b": 2, "c": 3}
47 |
48 | with pytest.raises(KeyError, match="d is not a valid setting."):
49 | options["d"] = 20
50 |
51 |
52 | def test_repr(options: OptionSettings) -> None:
53 | repr(options)
54 |
55 |
56 | def test_with_statement(options: OptionSettings) -> None:
57 | with options as o:
58 | o.set_value(a=3)
59 | assert o.get_value("a") == 3
60 | assert options.get_value("a") == 1
61 |
62 |
63 | def test_reset(options: OptionSettings) -> None:
64 | options(a=10)
65 | options.reset()
66 | assert options._current_values == {"a": 1, "b": 2, "c": 3}
67 |
--------------------------------------------------------------------------------
/test/test_repr.py:
--------------------------------------------------------------------------------
1 | from __future__ import annotations
2 |
3 | import numpy as np
4 | import pandas as pd
5 | import pytest
6 | import xarray as xr
7 |
8 | from linopy import Model, options
9 | from linopy.constraints import Constraint
10 | from linopy.expressions import LinearExpression
11 | from linopy.variables import Variable
12 |
13 | m = Model()
14 |
15 | lower = pd.Series(0, range(10))
16 | upper = pd.DataFrame(np.arange(10, 110).reshape(10, 10), range(10), range(10))
17 | types = pd.Index(list("abcdefgh"), name="types")
18 |
19 | u = m.add_variables(0, upper, name="u")
20 | v = m.add_variables(lower, upper, name="v")
21 | x = m.add_variables(lower, 10, coords=[lower.index], name="x")
22 | y = m.add_variables(0, 10, name="y")
23 | z = m.add_variables(name="z", binary=True)
24 | a = m.add_variables(coords=[lower.index], name="a", binary=True)
25 | b = m.add_variables(coords=[lower.index], name="b", integer=True)
26 | c_mask = xr.DataArray(False, coords=upper.axes)
27 | c_mask[:, 5:] = True
28 | c = m.add_variables(lower, upper, name="c", mask=c_mask)
29 | d = m.add_variables(0, 10, coords=[types], name="d")
30 |
31 | # new behavior in v0.2, variable with dimension name and other
32 | # coordinates are added without a warning
33 | e = m.add_variables(0, upper[5:], name="e")
34 |
35 | f_mask = np.full_like(upper[:5], True, dtype=bool)
36 | f_mask[:3] = False
37 | f = m.add_variables(0, upper[5:], name="f", mask=f_mask)
38 |
39 |
40 | multiindex = pd.MultiIndex.from_product(
41 | [list("asdfhjkg"), list("asdfghj")], names=["level_0", "level_1"]
42 | )
43 | g = m.add_variables(coords=[multiindex], name="g")
44 |
45 | # create linear expression for each variable
46 | lu = 1 * u
47 | lv = 1 * v
48 | lx = 1 * x
49 | ly = 1 * y
50 | lz = 1 * z
51 | la = 1 * a
52 | lb = 1 * b
53 | lc = 1 * c
54 | ld = 1 * d
55 | lav = 1 * a + 1 * v
56 | luc = 1 * v + 10
57 | lq = x * x
58 | lq2 = x * x + 1 * x
59 | lq3 = x * x + 1 * x + 1 + 1 * y + 1 * z
60 | lg = 1 * g
61 |
62 | # create anonymous constraint for linear expression
63 | cu_ = lu >= 0
64 | cv_ = lv >= 0
65 | cx_ = lx >= 0
66 | cy_ = ly >= 0
67 | cz_ = lz >= 0
68 | ca_ = la >= 0
69 | cb_ = lb >= 0
70 | cc_ = lc >= 0
71 | cd_ = ld >= 0
72 | cav_ = lav >= 0
73 | cuc_ = luc >= 0
74 | cg_ = lg >= 0
75 |
76 | # add constraint for each variable
77 | cu = m.add_constraints(cu_, name="cu")
78 | cv = m.add_constraints(cv_, name="cv")
79 | cx = m.add_constraints(cx_, name="cx")
80 | cy = m.add_constraints(cy_, name="cy")
81 | cz = m.add_constraints(cz_, name="cz")
82 | ca = m.add_constraints(ca_, name="ca")
83 | cb = m.add_constraints(cb_, name="cb")
84 | cc = m.add_constraints(cc_, name="cc")
85 | cd = m.add_constraints(cd_, name="cd")
86 | cav = m.add_constraints(cav_, name="cav")
87 | cuc = m.add_constraints(cuc_, name="cuc")
88 | cu_masked = m.add_constraints(cu_, name="cu_masked", mask=xr.full_like(u.labels, False))
89 | cg = m.add_constraints(cg_, name="cg")
90 |
91 | variables = [u, v, x, y, z, a, b, c, d, e, f, g]
92 | expressions = [lu, lv, lx, ly, lz, la, lb, lc, ld, lav, luc, lq, lq2, lq3, lg]
93 | anonymous_constraints = [cu_, cv_, cx_, cy_, cz_, ca_, cb_, cc_, cd_, cav_, cuc_, cg_]
94 | constraints = [cu, cv, cx, cy, cz, ca, cb, cc, cd, cav, cuc, cu_masked, cg]
95 |
96 |
97 | @pytest.mark.parametrize("var", variables)
98 | def test_variable_repr(var: Variable) -> None:
99 | repr(var)
100 |
101 |
102 | @pytest.mark.parametrize("var", variables)
103 | def test_scalar_variable_repr(var: Variable) -> None:
104 | coord = tuple(var.indexes[c][0] for c in var.dims)
105 | repr(var.at[coord])
106 |
107 |
108 | @pytest.mark.parametrize("var", variables)
109 | def test_single_variable_repr(var: Variable) -> None:
110 | coord = tuple(var.indexes[c][0] for c in var.dims)
111 | repr(var.loc[coord])
112 |
113 |
114 | @pytest.mark.parametrize("expr", expressions)
115 | def test_linear_expression_repr(expr: LinearExpression) -> None:
116 | repr(expr)
117 |
118 |
119 | def test_linear_expression_long() -> None:
120 | repr(x.sum())
121 |
122 |
123 | @pytest.mark.parametrize("var", variables)
124 | def test_scalar_linear_expression_repr(var: Variable) -> None:
125 | coord = tuple(var.indexes[c][0] for c in var.dims)
126 | repr(1 * var.at[coord])
127 |
128 |
129 | @pytest.mark.parametrize("var", variables)
130 | def test_single_linear_repr(var: Variable) -> None:
131 | coord = tuple(var.indexes[c][0] for c in var.dims)
132 | repr(1 * var.loc[coord])
133 |
134 |
135 | @pytest.mark.parametrize("var", variables)
136 | def test_single_array_linear_repr(var: Variable) -> None:
137 | coord = {c: [var.indexes[c][0]] for c in var.dims}
138 | repr(1 * var.sel(coord))
139 |
140 |
141 | @pytest.mark.parametrize("con", anonymous_constraints)
142 | def test_anonymous_constraint_repr(con: Constraint) -> None:
143 | repr(con)
144 |
145 |
146 | def test_scalar_constraint_repr() -> None:
147 | repr(u.at[0, 0] >= 0)
148 |
149 |
150 | @pytest.mark.parametrize("var", variables)
151 | def test_single_constraint_repr(var: Variable) -> None:
152 | coord = tuple(var.indexes[c][0] for c in var.dims)
153 | repr(var.loc[coord] == 0)
154 | repr(1 * var.loc[coord] - var.loc[coord] == 0)
155 |
156 |
157 | @pytest.mark.parametrize("var", variables)
158 | def test_single_array_constraint_repr(var: Variable) -> None:
159 | coord = {c: [var.indexes[c][0]] for c in var.dims}
160 | repr(var.sel(coord) == 0)
161 | repr(1 * var.sel(coord) - var.sel(coord) == 0)
162 |
163 |
164 | @pytest.mark.parametrize("con", constraints)
165 | def test_constraint_repr(con: Constraint) -> None:
166 | repr(con)
167 |
168 |
169 | def test_empty_repr() -> None:
170 | repr(u.loc[[]])
171 | repr(lu.sel(dim_0=[]))
172 | repr(lu.sel(dim_0=[]) >= 0)
173 |
174 |
175 | @pytest.mark.parametrize("obj", [v, lv, cv_, cv])
176 | def test_print_options(obj: Variable | LinearExpression | Constraint) -> None:
177 | default_repr = repr(obj)
178 | with options as opts:
179 | opts.set_value(display_max_rows=20)
180 | longer_repr = repr(obj)
181 | assert len(default_repr) < len(longer_repr)
182 |
183 | obj.print(display_max_rows=20)
184 |
185 |
186 | def test_print_labels() -> None:
187 | m.variables.print_labels([1, 2, 3])
188 | m.constraints.print_labels([1, 2, 3])
189 | m.constraints.print_labels([1, 2, 3], display_max_terms=10)
190 |
191 |
192 | def test_label_position_too_high() -> None:
193 | with pytest.raises(ValueError):
194 | m.variables.print_labels([1000])
195 |
196 |
197 | def test_model_repr_empty() -> None:
198 | repr(Model())
199 |
--------------------------------------------------------------------------------
/test/test_scalar_constraint.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 |
3 |
4 | import pandas as pd
5 | import pytest
6 |
7 | import linopy
8 | from linopy import GREATER_EQUAL, Model, Variable
9 | from linopy.constraints import AnonymousScalarConstraint, Constraint
10 |
11 |
12 | @pytest.fixture
13 | def m() -> Model:
14 | m = Model()
15 | m.add_variables(coords=[pd.RangeIndex(10, name="first")], name="x")
16 | return m
17 |
18 |
19 | @pytest.fixture
20 | def x(m: Model) -> Variable:
21 | return m.variables["x"]
22 |
23 |
24 | def test_scalar_constraint_repr(x: Variable) -> None:
25 | c: AnonymousScalarConstraint = x.at[0] >= 0
26 | c.__repr__()
27 |
28 |
29 | def test_anonymous_scalar_constraint_type(x: Variable) -> None:
30 | c: AnonymousScalarConstraint = x.at[0] >= 0
31 | assert isinstance(c, linopy.constraints.AnonymousScalarConstraint)
32 |
33 |
34 | def test_simple_constraint_type(m: Model, x: Variable) -> None:
35 | c: Constraint = m.add_constraints(x.at[0] >= 0)
36 | assert isinstance(c, linopy.constraints.Constraint)
37 |
38 |
39 | def test_compound_constraint_type(m: Model, x: Variable) -> None:
40 | c: Constraint = m.add_constraints(x.at[0] + x.at[1] >= 0)
41 | assert isinstance(c, linopy.constraints.Constraint)
42 |
43 |
44 | def test_explicit_simple_constraint_type(m: Model, x: Variable) -> None:
45 | c: Constraint = m.add_constraints(x.at[0], GREATER_EQUAL, 0)
46 | assert isinstance(c, linopy.constraints.Constraint)
47 |
48 |
49 | def test_explicit_compound_constraint_type(m: Model, x: Variable) -> None:
50 | c: Constraint = m.add_constraints(x.at[0] + x.at[1], GREATER_EQUAL, 0)
51 | assert isinstance(c, linopy.constraints.Constraint)
52 |
--------------------------------------------------------------------------------
/test/test_scalar_linear_expression.py:
--------------------------------------------------------------------------------
1 | import pandas as pd
2 | import pytest
3 |
4 | from linopy import Model, Variable
5 | from linopy.expressions import ScalarLinearExpression
6 |
7 |
8 | @pytest.fixture
9 | def m() -> Model:
10 | m = Model()
11 |
12 | m.add_variables(pd.Series([0, 0]), 1, name="x")
13 | m.add_variables(4, pd.Series([8, 10]), name="y")
14 | m.add_variables(0, pd.DataFrame([[1, 2], [3, 4], [5, 6]]).T, name="z")
15 | return m
16 |
17 |
18 | @pytest.fixture
19 | def x(m: Model) -> Variable:
20 | return m.variables["x"]
21 |
22 |
23 | @pytest.fixture
24 | def y(m: Model) -> Variable:
25 | return m.variables["y"]
26 |
27 |
28 | @pytest.fixture
29 | def z(m: Model) -> Variable:
30 | return m.variables["z"]
31 |
32 |
33 | def test_scalar_expression_initialization(
34 | x: Variable, y: Variable, z: Variable
35 | ) -> None:
36 | expr: ScalarLinearExpression = 10 * x.at[0]
37 | assert isinstance(expr, ScalarLinearExpression)
38 |
39 | expr2: ScalarLinearExpression = 10 * x.at[0] + y.at[1] + z.at[1, 1]
40 | assert isinstance(expr2, ScalarLinearExpression)
41 |
42 |
43 | def test_scalar_expression_multiplication(x: Variable) -> None:
44 | expr: ScalarLinearExpression = 10 * x.at[0]
45 | expr2: ScalarLinearExpression = 2 * expr
46 | assert isinstance(expr2, ScalarLinearExpression)
47 | assert expr2.coeffs == (20,)
48 |
49 |
50 | def test_scalar_expression_division(x: Variable) -> None:
51 | expr: ScalarLinearExpression = 10 * x.at[0]
52 | expr2: ScalarLinearExpression = expr / 2
53 | assert isinstance(expr2, ScalarLinearExpression)
54 | assert expr2.coeffs == (5,)
55 |
56 | expr3: ScalarLinearExpression = expr / 2.0
57 | assert isinstance(expr3, ScalarLinearExpression)
58 | assert expr3.coeffs == (5,)
59 |
60 |
61 | def test_scalar_expression_negation(x: Variable) -> None:
62 | expr: ScalarLinearExpression = 10 * x.at[0]
63 | expr3: ScalarLinearExpression = -expr
64 | assert isinstance(expr3, ScalarLinearExpression)
65 | assert expr3.coeffs == (-10,)
66 |
67 |
68 | def test_scalar_expression_multiplication_raises_type_error(x: Variable) -> None:
69 | with pytest.raises(TypeError):
70 | x.at[1] * x.at[1] # type: ignore
71 |
72 |
73 | def test_scalar_expression_division_raises_type_error(x: Variable) -> None:
74 | with pytest.raises(TypeError):
75 | x.at[1] / x.at[1] # type: ignore
76 |
77 |
78 | def test_scalar_expression_sum(x: Variable, y: Variable, z: Variable) -> None:
79 | target: ScalarLinearExpression = 10 * x.at[0] + y.at[1] + z.at[1, 1]
80 | expr: ScalarLinearExpression = sum((10 * x.at[0], y.at[1], z.at[1, 1])) # type: ignore
81 | assert isinstance(expr, ScalarLinearExpression)
82 | assert expr.vars == target.vars
83 | assert expr.coeffs == target.coeffs
84 |
85 |
86 | def test_scalar_expression_sum_from_variables(x: Variable, y: Variable) -> None:
87 | target: ScalarLinearExpression = x.at[0] + y.at[0]
88 | expr: ScalarLinearExpression = sum((x.at[0], y.at[0])) # type: ignore
89 | assert isinstance(expr, ScalarLinearExpression)
90 | assert expr.vars == target.vars
91 | assert expr.coeffs == target.coeffs
92 |
--------------------------------------------------------------------------------
/test/test_solvers.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | Created on Tue Jan 28 09:03:35 2025.
4 |
5 | @author: sid
6 | """
7 |
8 | from pathlib import Path
9 |
10 | import pytest
11 |
12 | from linopy import solvers
13 |
14 | free_mps_problem = """NAME sample_mip
15 | ROWS
16 | N obj
17 | G c1
18 | L c2
19 | E c3
20 | COLUMNS
21 | col1 obj 5
22 | col1 c1 2
23 | col1 c2 4
24 | col1 c3 1
25 | MARK0000 'MARKER' 'INTORG'
26 | colu2 obj 3
27 | colu2 c1 3
28 | colu2 c2 2
29 | colu2 c3 1
30 | col3 obj 7
31 | col3 c1 4
32 | col3 c2 3
33 | col3 c3 1
34 | MARK0001 'MARKER' 'INTEND'
35 | RHS
36 | RHS_V c1 12
37 | RHS_V c2 15
38 | RHS_V c3 6
39 | BOUNDS
40 | UP BOUND col1 4
41 | UI BOUND colu2 3
42 | UI BOUND col3 5
43 | ENDATA
44 | """
45 |
46 |
47 | @pytest.mark.parametrize("solver", set(solvers.available_solvers))
48 | def test_free_mps_solution_parsing(solver: str, tmp_path: Path) -> None:
49 | try:
50 | solver_enum = solvers.SolverName(solver.lower())
51 | solver_class = getattr(solvers, solver_enum.name)
52 | except ValueError:
53 | raise ValueError(f"Solver '{solver}' is not recognized")
54 |
55 | # Write the MPS file to the temporary directory
56 | mps_file = tmp_path / "problem.mps"
57 | mps_file.write_text(free_mps_problem)
58 |
59 | # Create a solution file path in the temporary directory
60 | sol_file = tmp_path / "solution.sol"
61 |
62 | s = solver_class()
63 | result = s.solve_problem(problem_fn=mps_file, solution_fn=sol_file)
64 |
65 | assert result.status.is_ok
66 | assert result.solution.objective == 30.0
67 |
--------------------------------------------------------------------------------
/test/test_typing.py:
--------------------------------------------------------------------------------
1 | import xarray as xr
2 |
3 | import linopy
4 |
5 |
6 | def test_operations_with_data_arrays_are_typed_correctly() -> None:
7 | m = linopy.Model()
8 |
9 | a: xr.DataArray = xr.DataArray([1, 2, 3])
10 |
11 | v: linopy.Variable = m.add_variables(lower=0.0, name="v")
12 | e: linopy.LinearExpression = v * 1.0
13 | q = v * v
14 |
15 | _ = a * v
16 | _ = v * a
17 | _ = v + a
18 |
19 | _ = a * e
20 | _ = e * a
21 | _ = e + a
22 |
23 | _ = a * q
24 | _ = q * a
25 | _ = q + a
26 |
--------------------------------------------------------------------------------
/test/test_variables.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | """
3 | This module aims at testing the correct behavior of the Variables class.
4 | """
5 |
6 | import numpy as np
7 | import pandas as pd
8 | import pytest
9 | import xarray as xr
10 | import xarray.core.indexes
11 | import xarray.core.utils
12 |
13 | import linopy
14 | from linopy import Model
15 | from linopy.testing import assert_varequal
16 | from linopy.variables import ScalarVariable
17 |
18 |
19 | @pytest.fixture
20 | def m() -> Model:
21 | m = Model()
22 | m.add_variables(coords=[pd.RangeIndex(10, name="first")], name="x")
23 | m.add_variables(coords=[pd.Index([1, 2, 3], name="second")], name="y")
24 | m.add_variables(0, 10, name="z")
25 | return m
26 |
27 |
28 | def test_variables_repr(m: Model) -> None:
29 | m.variables.__repr__()
30 |
31 |
32 | def test_variables_inherited_properties(m: Model) -> None:
33 | assert isinstance(m.variables.attrs, dict)
34 | assert isinstance(m.variables.coords, xr.Coordinates)
35 | assert isinstance(m.variables.indexes, xarray.core.indexes.Indexes)
36 | assert isinstance(m.variables.sizes, xarray.core.utils.Frozen)
37 |
38 |
39 | def test_variables_getattr_formatted() -> None:
40 | m = Model()
41 | m.add_variables(name="y-0")
42 | assert_varequal(m.variables.y_0, m.variables["y-0"])
43 |
44 |
45 | def test_variables_assignment_with_merge() -> None:
46 | """
47 | Test the merger of a variables with same dimension name but with different
48 | lengths.
49 |
50 | New coordinates are aligned to the existing ones. Thus this should
51 | raise a warning.
52 | """
53 | m = Model()
54 |
55 | upper = pd.Series(np.ones(10))
56 | var0 = m.add_variables(upper)
57 |
58 | upper = pd.Series(np.ones(12))
59 | var1 = m.add_variables(upper)
60 |
61 | with pytest.warns(UserWarning):
62 | assert m.variables.labels.var0[-1].item() == -1
63 |
64 | assert_varequal(var0, m.variables.var0)
65 | assert_varequal(var1, m.variables.var1)
66 |
67 |
68 | def test_variables_assignment_with_reindex(m: Model) -> None:
69 | shuffled_coords = [pd.Index([2, 1, 3, 4, 6, 5, 7, 9, 8, 0], name="first")]
70 | m.add_variables(coords=shuffled_coords, name="a")
71 |
72 | with pytest.warns(UserWarning):
73 | m.variables.labels
74 |
75 | for dtype in m.variables.labels.dtypes.values():
76 | assert np.issubdtype(dtype, np.integer)
77 |
78 | for dtype in m.variables.lower.dtypes.values():
79 | assert np.issubdtype(dtype, np.floating)
80 |
81 | for dtype in m.variables.upper.dtypes.values():
82 | assert np.issubdtype(dtype, np.floating)
83 |
84 |
85 | def test_scalar_variables_name_counter() -> None:
86 | m = Model()
87 | m.add_variables()
88 | m.add_variables()
89 | assert "var0" in m.variables
90 | assert "var1" in m.variables
91 |
92 |
93 | def test_variables_binaries(m: Model) -> None:
94 | assert isinstance(m.binaries, linopy.variables.Variables)
95 |
96 |
97 | def test_variables_integers(m: Model) -> None:
98 | assert isinstance(m.integers, linopy.variables.Variables)
99 |
100 |
101 | def test_variables_nvars(m: Model) -> None:
102 | assert m.variables.nvars == 14
103 |
104 | idx = pd.RangeIndex(10, name="first")
105 | mask = pd.Series([True] * 5 + [False] * 5, idx)
106 | m.add_variables(coords=[idx], mask=mask)
107 | assert m.variables.nvars == 19
108 |
109 |
110 | def test_variables_get_name_by_label(m: Model) -> None:
111 | assert m.variables.get_name_by_label(4) == "x"
112 | assert m.variables.get_name_by_label(12) == "y"
113 |
114 | with pytest.raises(ValueError):
115 | m.variables.get_name_by_label(30)
116 |
117 | with pytest.raises(ValueError):
118 | m.variables.get_name_by_label("anystring") # type: ignore
119 |
120 |
121 | def test_scalar_variable(m: Model) -> None:
122 | x = ScalarVariable(label=0, model=m)
123 | assert isinstance(x, ScalarVariable)
124 | assert x.__rmul__(x) is NotImplemented # type: ignore
125 |
--------------------------------------------------------------------------------