├── .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 | [![PyPI](https://img.shields.io/pypi/v/linopy)](https://pypi.org/project/linopy/) 4 | [![License](https://img.shields.io/pypi/l/linopy.svg)](LICENSE.txt) 5 | [![Tests](https://github.com/PyPSA/linopy/actions/workflows/test.yml/badge.svg)](https://github.com/PyPSA/linopy/actions/workflows/test.yml) 6 | [![doc](https://readthedocs.org/projects/linopy/badge/?version=latest)](https://linopy.readthedocs.io/en/latest/) 7 | [![codecov](https://codecov.io/gh/PyPSA/linopy/branch/master/graph/badge.svg?token=TT4EYFCCZX)](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 | ![Performance Benchmark](doc/benchmark_resource-overhead.png) 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 | ![Resources benchmark](benchmark_resource-overhead.png) 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 | --------------------------------------------------------------------------------