diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 3b1a32fb2..db0989a14 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -29,9 +29,7 @@ body: import numpy as np import flixopt as fx - fx.CONFIG.Logging.console = True - fx.CONFIG.Logging.level = 'DEBUG' - fx.CONFIG.apply() + fx.CONFIG.Logging.enable_console('DEBUG') flow_system = fx.FlowSystem(pd.date_range('2020-01-01', periods=3, freq='h')) flow_system.add_elements( diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..31af80dc9 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,14 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + commit-message: + prefix: "ci" + - package-ecosystem: pip + directory: / + schedule: + interval: weekly + commit-message: + prefix: "chore" diff --git a/.github/workflows/dependabot-auto-merge.yaml b/.github/workflows/dependabot-auto-merge.yaml new file mode 100644 index 000000000..d1198cb3b --- /dev/null +++ b/.github/workflows/dependabot-auto-merge.yaml @@ -0,0 +1,39 @@ +name: Dependabot auto-merge + +on: + pull_request_target: + +permissions: + contents: write + pull-requests: write + +jobs: + auto-merge: + name: Auto-merge minor/patch + if: github.actor == 'dependabot[bot]' + runs-on: ubuntu-24.04 + steps: + - uses: dependabot/fetch-metadata@v3 + id: metadata + + - name: Generate token for Release Bot + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + + - name: Approve PR + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: gh pr review "$PR" --approve + env: + PR: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.generate-token.outputs.token }} + + - name: Enable auto-merge + if: steps.metadata.outputs.update-type != 'version-update:semver-major' + run: gh pr merge "$PR" --auto --squash + env: + PR: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 000000000..6addc2745 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,197 @@ +name: Docs + +on: + push: + branches: [main] + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'flixopt/**' + - '.github/workflows/**' + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'flixopt/**' # notebooks import flixopt; catch library changes that break docs + - '.github/workflows/**' + workflow_dispatch: + inputs: + deploy: + description: 'Deploy docs to GitHub Pages' + type: boolean + default: false + version: + description: 'Version to deploy (e.g., v6.0.0)' + type: string + required: false + workflow_call: + inputs: + deploy: + type: boolean + default: false + version: + type: string + required: false + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.11" + MPLBACKEND: Agg + PLOTLY_RENDERER: notebook_connected + +jobs: + build: + name: Build documentation + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Extract changelog + run: | + cp CHANGELOG.md docs/changelog.md + python scripts/format_changelog.py + + - name: Install dependencies + run: uv pip install --system ".[docs,full]" + + - name: Get notebook cache key + id: notebook-cache-key + run: | + set -eo pipefail + # Hash notebooks + flixopt source code using null-delimited find for safety + HASH=$({ find docs/notebooks -name '*.ipynb' -print0; find flixopt -name '*.py' -print0; } | sort -z | xargs -0 tar -cf - 2>/dev/null | sha256sum | cut -d' ' -f1) + echo "hash=$HASH" >> $GITHUB_OUTPUT + + - name: Cache executed notebooks + uses: actions/cache@v6 + id: notebook-cache + with: + path: docs/notebooks/**/*.ipynb + key: notebooks-${{ steps.notebook-cache-key.outputs.hash }} + + - name: Execute fast notebooks + if: steps.notebook-cache.outputs.cache-hit != 'true' + run: | + set -eo pipefail + # Execute fast notebooks in parallel (4 at a time), excluding slow ones + cd docs/notebooks && find . -name '*.ipynb' | \ + grep -vFf slow_notebooks.txt | \ + xargs -P 4 -I {} sh -c 'jupyter execute --inplace "$1" || exit 255' _ {} + + - name: Execute slow notebooks + if: steps.notebook-cache.outputs.cache-hit != 'true' && github.event.pull_request.draft != 'true' + run: | + set -eo pipefail + # Execute slow notebooks in parallel (skip on draft PRs) + cd docs/notebooks && cat slow_notebooks.txt | \ + xargs -P 4 -I {} sh -c 'jupyter execute --inplace "$1" || exit 255' _ {} + + - name: Build docs + env: + MKDOCS_JUPYTER_EXECUTE: "false" + run: mkdocs build --strict + + - uses: actions/upload-artifact@v4 + with: + name: docs + path: site/ + retention-days: 7 + + deploy: + name: Deploy documentation + needs: build + if: ${{ inputs.deploy == true && inputs.version != '' }} + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Extract changelog + run: | + cp CHANGELOG.md docs/changelog.md + python scripts/format_changelog.py + + - name: Install dependencies + run: uv pip install --system ".[docs,full]" + + - name: Get notebook cache key + id: notebook-cache-key + run: | + set -eo pipefail + # Hash notebooks + flixopt source code using null-delimited find for safety + HASH=$({ find docs/notebooks -name '*.ipynb' -print0; find flixopt -name '*.py' -print0; } | sort -z | xargs -0 tar -cf - 2>/dev/null | sha256sum | cut -d' ' -f1) + echo "hash=$HASH" >> $GITHUB_OUTPUT + + - name: Cache executed notebooks + uses: actions/cache@v6 + id: notebook-cache + with: + path: docs/notebooks/**/*.ipynb + key: notebooks-${{ steps.notebook-cache-key.outputs.hash }} + + - name: Execute fast notebooks + if: steps.notebook-cache.outputs.cache-hit != 'true' + run: | + set -eo pipefail + # Execute fast notebooks in parallel (4 at a time), excluding slow ones + cd docs/notebooks && find . -name '*.ipynb' | \ + grep -vFf slow_notebooks.txt | \ + xargs -P 4 -I {} sh -c 'jupyter execute --inplace "$1" || exit 255' _ {} + + - name: Execute slow notebooks + if: steps.notebook-cache.outputs.cache-hit != 'true' + run: | + set -eo pipefail + # Execute slow notebooks (only on release) + cd docs/notebooks && cat slow_notebooks.txt | \ + xargs -I {} sh -c 'jupyter execute --inplace "$1" || exit 255' _ {} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Deploy docs + env: + MKDOCS_JUPYTER_EXECUTE: "false" + run: | + VERSION=${{ inputs.version }} + VERSION=${VERSION#v} + + # Check if this is a pre-release (alpha, beta, rc) + if [[ "$VERSION" =~ (alpha|beta|rc) ]]; then + # Pre-release: deploy version only, don't update "latest" + mike deploy --push $VERSION + else + # Stable release: deploy and update "latest" alias + mike deploy --push --update-aliases $VERSION latest + mike set-default --push latest + fi diff --git a/.github/workflows/pr-title.yaml b/.github/workflows/pr-title.yaml new file mode 100644 index 000000000..3a358dc31 --- /dev/null +++ b/.github/workflows/pr-title.yaml @@ -0,0 +1,32 @@ +name: PR Title + +on: + push: + branches: ["release-please--**"] + pull_request: + types: [opened, edited, reopened] + +jobs: + validate: + name: Validate conventional commit + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + permissions: + pull-requests: read + steps: + - uses: amannn/action-semantic-pull-request@v6 + with: + types: | + feat + fix + refactor + test + docs + chore + ci + build + perf + revert + style + env: + GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..e7943097a --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,109 @@ +name: Publish + +on: + push: + tags: ["v*"] + +env: + PYTHON_VERSION: "3.11" + +jobs: + build: + name: Build package + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ github.ref_name }} + fetch-depth: 0 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Build package + run: uv build + + - uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 7 + + publish-pypi: + name: Publish to PyPI + needs: build + runs-on: ubuntu-24.04 + environment: + name: pypi + url: https://pypi.org/p/flixopt + permissions: + id-token: write + steps: + - uses: actions/download-artifact@v4 + with: + name: dist + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + skip-existing: true + + verify-pypi: + name: Verify PyPI installation + needs: publish-pypi + runs-on: ubuntu-24.04 + steps: + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Verify installation + run: | + VERSION="${TAG#v}" + + for delay in 30 60 90 120 180 300; do + sleep $delay + echo "Attempting installation (waited ${delay}s)..." + + if uv pip install --system --index-url https://pypi.org/simple/ "flixopt==$VERSION" && \ + python -c "from importlib.metadata import version; assert version('flixopt') == '$VERSION'"; then + echo "PyPI installation successful!" + exit 0 + fi + done + + echo "Failed to verify PyPI installation" + exit 1 + env: + TAG: ${{ github.ref_name }} + + github-release: + name: Create GitHub release + needs: verify-pypi + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + + - name: Create GitHub release + run: | + if [[ "$TAG" =~ (rc|alpha|beta) ]]; then + gh release create "$TAG" --generate-notes --prerelease + else + gh release create "$TAG" --generate-notes + fi + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.ref_name }} diff --git a/.github/workflows/python-app.yaml b/.github/workflows/python-app.yaml deleted file mode 100644 index 164f9059a..000000000 --- a/.github/workflows/python-app.yaml +++ /dev/null @@ -1,417 +0,0 @@ -name: Python Package CI/CD - -on: - push: - branches: [main] # Only main branch - tags: ['v*.*.*'] - pull_request: - branches: [main, 'dev*', 'dev/**', 'feature/**'] - types: [opened, synchronize, reopened] - paths-ignore: - - 'docs/**' - - '*.md' - - 'README*' - workflow_dispatch: # Allow manual triggering - -# Set permissions for security -permissions: - contents: read - -# Cancel previous runs on new push -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - PYTHON_VERSION: "3.11" - MPLBACKEND: Agg # Non-interactive matplotlib backend for CI/testing - PLOTLY_RENDERER: json # Headless plotly renderer for CI/testing - FLIXOPT_CI: false # Disable interactive plotting for CI/testing - -jobs: - lint: - runs-on: ubuntu-24.04 - steps: - - name: Check out code - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install Ruff - run: | - uvx ruff --version - - - name: Run Ruff Linting - run: | - echo "::group::Ruff Linting" - uvx ruff check . --output-format=github - echo "::endgroup::" - - - name: Run Ruff Formatting Check - run: | - echo "::group::Ruff Formatting" - uvx ruff format --check --diff . - echo "::endgroup::" - - test: - runs-on: ubuntu-24.04 - timeout-minutes: 30 - needs: lint # Run tests only after linting passes - strategy: - fail-fast: false # Continue testing other Python versions if one fails - matrix: - python-version: ['3.10', '3.11', '3.12', '3.13'] - - steps: - - name: Check out code - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 - with: - python-version: ${{ matrix.python-version }} - - - name: Install dependencies - run: | - uv pip install --system .[dev] - - - name: Run tests - run: pytest -v --numprocesses=auto - - test-examples: - runs-on: ubuntu-24.04 - timeout-minutes: 45 - needs: lint - # Only run examples on releases (tags) - if: startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'push' && github.ref == 'refs/heads/main') - - steps: - - name: Check out code - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python ${{ env.PYTHON_VERSION }} - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - uv pip install --system .[dev] - - - name: Run example tests - run: pytest -v -m examples --numprocesses=auto - - security: - name: Security Scan - runs-on: ubuntu-24.04 - needs: lint - steps: - - name: Check out code - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Run Bandit security scan - run: | - # Gate on HIGH severity & MEDIUM confidence; produce JSON artifact - uvx bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q --severity-level high --confidence-level medium - # Human-readable output without affecting job status - uvx bandit -r flixopt/ -c pyproject.toml -q --exit-zero - - - name: Upload security reports - uses: actions/upload-artifact@v4 - if: always() - with: - name: security-report - path: bandit-report.json - retention-days: 30 - - create-release: - name: Create GitHub Release - runs-on: ubuntu-24.04 - permissions: - contents: write - needs: [lint, test, test-examples, security] - if: startsWith(github.ref, 'refs/tags/v') - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Extract release notes - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "Extracting release notes for version: $VERSION" - python scripts/extract_release_notes.py $VERSION > current_release_notes.md - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - body_path: current_release_notes.md - draft: false - prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') || contains(github.ref, 'rc') }} - generate_release_notes: true - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - publish-testpypi: - name: Publish to TestPyPI - runs-on: ubuntu-24.04 - needs: [test, test-examples, create-release] # Run after tests and release creation - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push - environment: - name: testpypi - url: https://test.pypi.org/p/flixopt - env: - SKIP_TESTPYPI_UPLOAD: "false" - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - uv pip install --system twine - - - name: Build the distribution - run: | - uv build - - - name: Upload to TestPyPI - run: | - twine upload --repository-url https://test.pypi.org/legacy/ dist/* --verbose --skip-existing - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.TEST_PYPI_API_TOKEN }} - TWINE_NON_INTERACTIVE: "1" - - - name: Test install from TestPyPI - if: env.SKIP_TESTPYPI_UPLOAD != 'true' - run: | - set -Eeuo pipefail - # Create a temporary environment to test installation - uv venv test_env - source test_env/bin/activate - - # Get project name from pyproject.toml (PEP 621) - PACKAGE_NAME=$(python - <<'PY' - import sys, tomllib, pathlib - data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) - print(data["project"]["name"]) - PY - ) - - # Extract version from git tag - VERSION=${GITHUB_REF#refs/tags/v} - - # Wait and retry while TestPyPI indexes the package - INSTALL_SUCCESS=false - for d in 10 20 40 80 120; do - sleep "$d" - echo "Attempting to install $PACKAGE_NAME==$VERSION from TestPyPI (retry after ${d}s)..." - - # Install specific version and verify it matches - if uv pip install --index-strategy unsafe-best-match --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ - python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then - INSTALL_SUCCESS=true - break - fi - done - - # Check if installation succeeded - if [ "$INSTALL_SUCCESS" = "false" ]; then - echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from TestPyPI after all retries" - echo "This could indicate:" - echo " - TestPyPI indexing issues" - echo " - Package upload problems" - echo " - Version mismatch between tag and package" - exit 1 - fi - - # Final success confirmation - python -c "import flixopt; print('TestPyPI installation successful!')" - - publish-pypi: - name: Publish to PyPI - runs-on: ubuntu-24.04 - needs: [publish-testpypi] # Only run after TestPyPI publish succeeds - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') # Only on tag push - environment: - name: pypi - url: https://pypi.org/p/flixopt - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Install dependencies - run: | - uv pip install --system twine - - - name: Build the distribution - run: | - uv build - - - name: Upload to PyPI - run: | - twine upload dist/* --verbose --skip-existing - env: - TWINE_USERNAME: __token__ - TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} - TWINE_NON_INTERACTIVE: "1" - - - name: Verify PyPI installation - run: | - set -Eeuo pipefail - # Create a temporary environment to test installation - uv venv prod_test_env - source prod_test_env/bin/activate - - # Get project name from pyproject.toml (PEP 621) - PACKAGE_NAME=$(python - <<'PY' - import sys, tomllib, pathlib - data = tomllib.loads(pathlib.Path("pyproject.toml").read_text(encoding="utf-8")) - print(data["project"]["name"]) - PY - ) - - # Extract version from git tag - VERSION=${GITHUB_REF#refs/tags/v} - - # Wait and retry while PyPI indexes the package - INSTALL_SUCCESS=false - for d in 10 20 40 60 90 120 180 300 480 600; do # Total: up to ~30 minutes - sleep "$d" - echo "Attempting to install $PACKAGE_NAME==$VERSION from PyPI (retry after ${d}s)..." - # Install directly from pypi, potentially mitigatiing caches - if uv pip install --index-url https://pypi.org/simple/ "$PACKAGE_NAME==$VERSION" && \ - python -c "from importlib.metadata import version; installed = version('$PACKAGE_NAME'); print(f'Installed: {installed}'); assert '$VERSION' == installed"; then - INSTALL_SUCCESS=true - break - fi - done - - if [ "$INSTALL_SUCCESS" = "false" ]; then - echo "ERROR: Failed to install $PACKAGE_NAME==$VERSION from PyPI after all retries" - echo "Check: https://pypi.org/project/$PACKAGE_NAME/$VERSION/" - exit 1 - fi - - # Final success confirmation - python -c "import flixopt; print('PyPI installation successful!')" - - deploy-docs: - name: Deploy Documentation - runs-on: ubuntu-24.04 - permissions: - contents: write - needs: [publish-pypi] # Deploy docs after successful PyPI publishing - if: startsWith(github.ref, 'refs/tags/v') && !contains(github.ref, 'alpha') && !contains(github.ref, 'beta') && !contains(github.ref, 'rc') - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 # Fetch all history for proper versioning - - - name: Set up uv - uses: astral-sh/setup-uv@v6 - with: - version: "0.9.7" - enable-cache: true - - - name: Set up Python - uses: actions/setup-python@v6 - with: - python-version: ${{ env.PYTHON_VERSION }} - - - name: Extract changelog to docs - run: | - # Install packaging dependency for changelog extraction - uv pip install --system packaging - - # Extract individual release files - python scripts/extract_changelog.py - - echo "✅ Extracted changelog to docs/changelog/" - - - name: Install documentation dependencies - run: | - uv pip install --system ".[docs]" - - - name: Configure Git Credentials - run: | - git config user.name github-actions[bot] - git config user.email 41898282+github-actions[bot]@users.noreply.github.com - - - name: Deploy docs - run: | - VERSION=${GITHUB_REF#refs/tags/v} - echo "Deploying docs after successful PyPI publish: $VERSION" - mike deploy --push --update-aliases $VERSION latest - mike set-default --push latest diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 000000000..b1d06d42f --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,70 @@ +name: Release + +on: + push: + branches: [main] + +permissions: + contents: write + pull-requests: write + +jobs: + release-please: + name: Release Please + runs-on: ubuntu-24.04 + outputs: + release_created: ${{ steps.release.outputs.release_created }} + tag_name: ${{ steps.release.outputs.tag_name }} + steps: + - name: Generate token for Release Bot + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + + - uses: googleapis/release-please-action@v5 + id: release + with: + token: ${{ steps.generate-token.outputs.token }} + config-file: .release-please-config.json + manifest-file: .release-please-manifest.json + + update-citation-date: + name: Update CITATION.cff date + needs: release-please + if: needs.release-please.outputs.release_created + runs-on: ubuntu-24.04 + steps: + - name: Generate token for Release Bot + id: generate-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_BOT_APP_ID }} + private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }} + + - uses: actions/checkout@v7 + with: + ref: main + token: ${{ steps.generate-token.outputs.token }} + + - name: Update date-released + run: | + DATE=$(date +%Y-%m-%d) + sed -i "s/^date-released: .*/date-released: $DATE/" CITATION.cff + + - name: Commit and push + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add CITATION.cff + git diff --staged --quiet || (git commit -m "chore: update CITATION.cff date-released" && git push origin main) + + deploy-docs: + name: Deploy documentation + needs: release-please + if: needs.release-please.outputs.release_created + uses: ./.github/workflows/docs.yaml + with: + deploy: true + version: ${{ needs.release-please.outputs.tag_name }} diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 000000000..fddad0374 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,173 @@ +name: Tests + +on: + push: + branches: [main] + paths: + - 'flixopt/**' + - 'tests/**' + - 'pyproject.toml' + - '.github/workflows/**' + # No paths filter here: lint and test are required checks, and a workflow that + # never triggers never reports, leaving such PRs permanently blocked. The filter + # lives in the `changes` job below, which skips the expensive steps instead. + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + branches: ["**"] + workflow_dispatch: + +permissions: + contents: read + pull-requests: read # `changes` reads the PR's file list via gh + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PYTHON_VERSION: "3.11" + RUFF_VERSION: "0.15.21" # Keep in sync with the ruff pin in pyproject.toml [dev] + MPLBACKEND: Agg + PLOTLY_RENDERER: json + FLIXOPT_CI: false + +jobs: + changes: + name: Detect source changes + runs-on: ubuntu-24.04 + outputs: + source: ${{ steps.detect.outputs.source }} + steps: + - name: Detect source changes + id: detect + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + if [[ "${{ github.event_name }}" != 'pull_request' ]]; then + echo 'source=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + # Treat a failed, empty or truncated listing as a source change, so a + # detection failure costs a test run rather than skipping one. The guard + # is required: steps run under `bash -e`, where a failing command + # substitution aborts the step instead of falling through. + if ! paths=$(gh pr view "$PR_NUMBER" --repo "$REPO" --json files --jq '.files[].path'); then + echo 'source=true' >> "$GITHUB_OUTPUT" + exit 0 + fi + if [[ -z "$paths" ]] || grep -qE '^(flixopt/|tests/|pyproject\.toml$|\.github/workflows/)' <<< "$paths"; then + echo 'source=true' >> "$GITHUB_OUTPUT" + else + echo 'source=false' >> "$GITHUB_OUTPUT" + fi + + lint: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run Ruff + run: | + uvx ruff@${{ env.RUFF_VERSION }} check . --output-format=github + uvx ruff@${{ env.RUFF_VERSION }} format --check --diff . + + test: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + needs: [lint, changes] + strategy: + fail-fast: false + matrix: + python-version: ['3.11', '3.12', '3.13', '3.14'] + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + if: needs.changes.outputs.source == 'true' + run: uv pip install --system .[dev] + + - name: Run tests + if: needs.changes.outputs.source == 'true' + run: | + if [[ "${{ github.event.pull_request.draft }}" == "true" ]]; then + # Draft PR: skip examples, slow, and deprecated_api + pytest -v --numprocesses=auto -m "not examples and not slow and not deprecated_api" + else + # Ready PR & main push: examples excluded via addopts + pytest -v --numprocesses=auto + fi + + - name: Report skipped tests + if: needs.changes.outputs.source != 'true' + run: echo 'No changes under flixopt/, tests/, pyproject.toml or .github/workflows/ - tests skipped.' + + test-examples: + runs-on: ubuntu-24.04 + timeout-minutes: 45 + needs: lint + # Only run on main branch or when called by release workflow (not on PRs) + if: github.event_name != 'pull_request' + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Install dependencies + run: uv pip install --system .[dev] + + - name: Run example tests + run: pytest -v -m examples --numprocesses=auto + + security: + runs-on: ubuntu-24.04 + needs: lint + steps: + - uses: actions/checkout@v7 + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Run Bandit + run: | + uvx bandit -r flixopt/ -c pyproject.toml -f json -o bandit-report.json -q --severity-level high --confidence-level medium + uvx bandit -r flixopt/ -c pyproject.toml -q --exit-zero + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: security-report + path: bandit-report.json + retention-days: 30 diff --git a/.github/workflows/tutorial-data.yaml b/.github/workflows/tutorial-data.yaml new file mode 100644 index 000000000..3a28f5703 --- /dev/null +++ b/.github/workflows/tutorial-data.yaml @@ -0,0 +1,74 @@ +name: Tutorial data + +# Builds the pre-built example FlowSystems and uploads them (plus registry.txt) as +# assets to the GitHub release that `flixopt.tutorials.load_example` downloads from. +# Run manually whenever the example systems change. The release tag must match +# `flixopt.tutorials._examples.DATA_RELEASE` (default: tutorial-data-v1). + +on: + workflow_dispatch: + inputs: + release_tag: + description: "Release tag to (re)upload the data assets to (must match DATA_RELEASE)." + required: true + default: "tutorial-data-v1" + +env: + PYTHON_VERSION: "3.11" + +jobs: + build-and-upload: + name: Build and upload example systems + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: astral-sh/setup-uv@v7 + with: + version: "0.10.9" + enable-cache: true + + - uses: actions/setup-python@v6 + with: + python-version: ${{ env.PYTHON_VERSION }} + + # docs extra provides demandlib/pvlib/holidays used by the example generators. + - name: Install build dependencies + run: uv pip install --system -e ".[docs,full]" + + - name: Verify DATA_RELEASE matches the requested tag + run: | + EXPECTED=$(python -c "from flixopt.tutorials._examples import DATA_RELEASE; print(DATA_RELEASE)") + if [[ "$EXPECTED" != "$TAG" ]]; then + echo "::error::DATA_RELEASE is '$EXPECTED' but the workflow was asked to upload to '$TAG'." + echo "Update flixopt/tutorials/_examples.py or pass the matching tag." + exit 1 + fi + env: + TAG: ${{ inputs.release_tag }} + + - name: Build example systems + run: python scripts/build_tutorial_datasets.py --out-dir dist/tutorial_datasets + + - name: Create the data release if it does not exist + run: | + if ! gh release view "$TAG" >/dev/null 2>&1; then + gh release create "$TAG" \ + --title "Tutorial data ($TAG)" \ + --notes "Pre-built example FlowSystems downloaded by flixopt.tutorials.load_example." \ + --prerelease + fi + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.release_tag }} + + - name: Upload data assets + run: gh release upload "$TAG" dist/tutorial_datasets/* --clobber + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.release_tag }} diff --git a/.gitignore b/.gitignore index cc2179b07..49fa19800 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ *.pyc *.log +*.nc4 +*.nc results/ .idea/ .venv/ @@ -8,3 +10,7 @@ venv/ .DS_Store lib/ temp-plot.html +.cache +site/ +*.egg-info +uv.lock diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e39033067..18b1eb4be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,10 +7,17 @@ repos: - id: check-yaml exclude: ^mkdocs\.yml$ # Skip mkdocs.yml - id: check-added-large-files + exclude: (.*Zeitreihen2020\.csv$|docs/notebooks/data/raw/.*) - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.12.4 hooks: - - id: ruff-check - args: [ --fix ] - - id: ruff-format + - id: ruff-check + args: [ --fix ] + - id: ruff-format + + - repo: https://github.com/kynan/nbstripout + rev: 0.8.2 + hooks: + - id: nbstripout + files: ^docs/.*\.ipynb$ diff --git a/.release-please-config.json b/.release-please-config.json new file mode 100644 index 000000000..ed357ff3a --- /dev/null +++ b/.release-please-config.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", + "include-component-in-tag": false, + "packages": { + ".": { + "release-type": "simple", + "package-name": "flixopt", + "changelog-path": "CHANGELOG.md", + "extra-files": [ + "CITATION.cff" + ] + } + } +} diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 000000000..c2430e7ad --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "8.0.1" +} diff --git a/CHANGELOG.md b/CHANGELOG.md index 7953efb5d..22d2fc38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,85 +1,1432 @@ # Changelog This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Gitmoji](https://gitmoji.dev). +Formatting is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) & [Conventional Commits](https://www.conventionalcommits.org/). For more details regarding the individual PRs and contributors, please refer to our [GitHub releases](https://github.com/flixOpt/flixopt/releases). !!! tip - If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + If upgrading from v5.x, see the [Migration Guide v6](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v6/). + If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide v3](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). --- - +## [3.6.1] - 2025-11-17 + +**Summary**: Documentation improvements and dependency updates. + +If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). + +### Dependencies +- Updated `astral-sh/uv` to v0.9.8 +- Updated `mkdocs-git-revision-date-localized-plugin` to v1.5.0 + +### Documentation +- Improved type specifications in `flixopt/types.py` for better documentation generation +- Fixed minor mkdocs warnings in `flixopt/io.py` and `mkdocs.yml` + +--- ## [3.6.0] - 2025-11-15 @@ -87,7 +1434,7 @@ Until here --> If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ✨ Added +### Added - **New type system** (`flixopt/types.py`): - Introduced dimension-aware type aliases using suffix notation (`_TPS`, `_PS`, `_S`) to clearly indicate which dimensions data can have - Added `Numeric_TPS`, `Numeric_PS`, `Numeric_S` for numeric data with Time/Period/Scenario dimensions @@ -99,24 +1446,24 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Lazy logging evaluation - expensive log operations only execute when log level is active - `CONFIG.Logging.verbose_tracebacks` option for detailed debugging with variable values -### 💥 Breaking Changes +### Breaking Changes - **Logging framework**: Migrated to [loguru](https://loguru.readthedocs.io/) - Removed `CONFIG.Logging` parameters: `rich`, `Colors`, `date_format`, `format`, `console_width`, `show_path`, `show_logger_name` - For advanced formatting, use loguru's API directly after `CONFIG.apply()` -### ♻️ Changed +### Changed - **Code structure**: Removed `commons.py` module and moved all imports directly to `__init__.py` for cleaner code organization (no public API changes) - **Type handling improvements**: Updated internal data handling to work seamlessly with the new type system -### 🐛 Fixed +### Bug Fixes - Fixed `ShareAllocationModel` inconsistency where None/inf conversion happened in `__init__` instead of during modeling, which could cause issues with parameter validation - Fixed numerous type hint inconsistencies across the codebase -### 📦 Dependencies +### Dependencies - Updated `mkdocs-material` to v9.6.23 - Replaced `rich >= 13.0.0` with `loguru >= 0.7.0` for logging -### 📝 Docs +### Documentation - Enhanced documentation in `flixopt/types.py` with comprehensive examples and dimension explanation table - Clarified Effect type docstrings - Effect types are dicts, but single numeric values work through union types - Added clarifying comments in `effects.py` explaining parameter handling and transformation @@ -124,7 +1471,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Updated getting-started guide with loguru examples - Updated `config.py` docstrings for loguru integration -### 👷 Development +### Development - Added test for FlowSystem resampling --- @@ -135,12 +1482,12 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ✨ Added +### Added - Added options to resample and select subsets of flowsystems without converting to and from Dataset each time. Use the new methods `FlowSystem.__dataset_resample()`, `FlowSystem.__dataset_sel()` and `FlowSystem.__dataset_isel()`. All of them expect and return a dataset. -### 💥 Breaking Changes +### Breaking Changes -### ♻️ Changed +### Changed - Truncate repr of FlowSystem and CalculationResults to only show the first 10 items of each category - Greatly sped up the resampling of a FlowSystem again @@ -152,7 +1499,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ♻️ Changed +### Changed - Greatly sped up the resampling of a FlowSystem (x20 - x40) by converting to dataarray internally --- @@ -163,7 +1510,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ✨ Added +### Added **Solver configuration:** - **New `CONFIG.Solving` configuration section** for centralized solver parameter management: @@ -175,16 +1522,16 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Solver parameters can still be explicitly overridden when creating solver instances - New `log_to_console` parameter in all Solver classes -### ♻️ Changed +### Changed - Individual solver output is now hidden in **SegmentedCalculation**. To return to the prior behaviour, set `show_individual_solves=True` in `do_modeling_and_solve()`. -### 🐛 Fixed +### Bug Fixes - New compacted list representation for periods and scenarios also in results log and console print -### 📝 Docs +### Documentation - Unified contributing guides in docs and on github -### 👷 Development +### Development - Added type hints for submodel in all Interface classes --- @@ -195,13 +1542,13 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ♻️ Changed +### Changed - Improved `summary.yaml` to use a compacted list representation for periods and scenarios -### 🐛 Fixed +### Bug Fixes - Using `switch_on_total_max` with periods or scenarios failed -### 📝 Docs +### Documentation - Add more comprehensive `CONTRIBUTE.md` - Improve logical structure in User Guide @@ -213,7 +1560,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ♻️ Changed +### Changed **Improved repr methods:** - **Results classes** (`ComponentResults`, `BusResults`, `FlowResults`, `EffectResults`) now show concise header with key metadata followed by xarray Dataset repr - **Element classes** (`Component`, `Bus`, `Flow`, `Effect`, `Storage`) now show one-line summaries with essential information (connections, sizes, capacities, constraints) @@ -223,7 +1570,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Use `flow_system['element_label']`, `flow_system.keys()`, `flow_system.values()`, and `flow_system.items()` for unified element access - Specialized containers (`components`, `buses`, `effects`, `flows`) offer type-specific access with helpful error messages -### 🗑️ Deprecated +### Deprecated - **`FlowSystem.all_elements`** property is deprecated in favor of dict-like interface (`flow_system['label']`, `.keys()`, `.values()`, `.items()`). Will be removed in v4.0.0. --- @@ -234,10 +1581,10 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### 🐛 Fixed +### Bug Fixes - Fixed resampling of FlowSystem to reset `hours_of_last_timestep` and `hours_of_previous_timesteps` properly -### 👷 Development +### Development - Improved issue templates --- @@ -248,7 +1595,7 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### ✨ Added +### Added **Color management:** - **`setup_colors()` method** for `CalculationResults` and `SegmentedCalculationResults` to configure consistent colors across all plots @@ -273,24 +1620,24 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp - Enhanced NetCDF handling with consistent engine usage - Better numeric formatting in YAML exports -### ♻️ Changed +### Changed - **Default colorscale**: Changed from 'viridis' to 'turbo' for better perceptual uniformity - **Color terminology**: Standardized from "colormap" to "colorscale" throughout for Plotly consistency - **Plotting internals**: Now use `xr.Dataset` as primary data type (DataFrames automatically converted) - **NetCDF engine**: Switched back to netcdf4 engine following xarray updates and performance benchmarks -### 🔥 Removed +### Removed - Removed unused `plotting.pie_with_plotly()` method -### 🐛 Fixed +### Bug Fixes - Improved error messages when using `engine='matplotlib'` with multidimensional data - Better dimension validation in `results.plot_heatmap()` -### 📝 Docs +### Documentation - Enhanced examples demonstrating `setup_colors()` usage - Updated terminology from "colormap" to "colorscale" in docstrings -### 👷 Development +### Development - Fixed concurrency issue in CI - Centralized color processing logic into dedicated module - Refactored to function-based color handling for simpler API @@ -298,14 +1645,14 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp --- ## [3.1.1] - 2025-10-20 -**Summary**: Fixed a bug when acessing the `effects_per_component` dataset in results without periodic effects. +**Summary**: Fixed a bug when accessing the `effects_per_component` dataset in results without periodic effects. If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0) and [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/). -### 🐛 Fixed +### Bug Fixes - Fixed ValueError in effects_per_component when all periodic effects are scalars/NaN by explicitly creating mode-specific templates (via _create_template_for_mode) with correct dimensions -### 👷 Development +### Development - Converted all remaining numpy style docstrings to google style --- @@ -316,31 +1663,31 @@ If upgrading from v2.x, see the [v3.0.0 release notes](https://github.com/flixOp If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). -### ✨ Added +### Added - **Faceting and animation for multidimensional plots**: All plotting methods now support `facet_by` and `animate_by` parameters to create subplot grids and animations from multidimensional data (scenarios, periods, etc.). *Plotly only.* - **Flexible data selection with `select` parameter**: Select data using single values, lists, slices, or index arrays for precise control over what gets plotted - **Heatmap fill control**: New `fill` parameter in heatmap methods controls how missing values are filled after reshaping (`'ffill'` or `'bfill'`) - **Smart line styling for mixed variables**: Area plots now automatically style variables containing both positive and negative values with dashed lines, while stacking purely positive or negative variables -### ♻️ Changed +### Changed - **Breaking: Selection behavior**: Plotting methods no longer automatically select the first value for non-time dimensions. Use the `select` parameter for explicit selection of scenarios, periods, or other dimensions - **Better error messages**: Enhanced error messages when using Matplotlib with multidimensional data, with clearer guidance on dimension requirements and suggestions to use Plotly - **Improved examples**: Enhanced `scenario_example.py` with better demonstration of new features - **Robust validation**: Improved dimension validation in `plot_heatmap()` with clearer error messages -### 🗑️ Deprecated +### Deprecated - **`indexer` parameter**: Use the new `select` parameter instead. The `indexer` parameter will be removed in v4.0.0 - **`heatmap_timeframes` and `heatmap_timesteps_per_frame` parameters**: Use the new `reshape_time=(timeframes, timesteps_per_frame)` parameter instead in heatmap plotting methods - **`color_map` parameter**: Use the new `colors` parameter instead in heatmap plotting methods -### 🐛 Fixed +### Bug Fixes - Fixed cryptic errors when working with empty buses by adding proper validation - Added early validation for non-existent periods when using linked periods with tuples -### 📝 Documentation +### Documentation - **Redesigned documentation website** with custom css -### 👷 Development +### Development - Renamed internal `_apply_indexer_to_data()` to `_apply_selection_to_data()` for consistency with new API naming --- @@ -350,11 +1697,11 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix **Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). -### 🐛 Fixed +### Bug Fixes - Reverted breaking change from v3.0.0: continue to use `mode parameter in plotting instead of new `style` - Renamed new `mode` parameter in plotting methods to `unit_type` -### 📝 Docs +### Documentation - Updated Migration Guide and added missing entries. - Improved Changelog of v3.0.0 @@ -365,7 +1712,7 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix **Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). -### 📝 Docs +### Documentation - Update the Readme - Add a project roadmap to the docs - Change Development status to "Production/Stable" @@ -378,11 +1725,11 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix **Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). -### 📝 Docs +### Documentation - Fixed deployed docs - Added Migration Guide for flixopt 3 -### 👷 Development +### Development - Added missing type hints --- @@ -392,7 +1739,7 @@ If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flix **Note**: If upgrading from v2.x, see the [Migration Guide](https://flixopt.github.io/flixopt/latest/user-guide/migration-guide-v3/) and [v3.0.0 release notes](https://github.com/flixOpt/flixOpt/releases/tag/v3.0.0). -### ✨ Added +### Added **New model dimensions:** @@ -428,7 +1775,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Improved filter methods in results - Example for 2-stage investment decisions leveraging FlowSystem resampling -### 💥 Breaking Changes +### Breaking Changes **API and Behavior Changes:** @@ -465,7 +1812,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - `relative_minimum_charge_state` and `relative_maximum_charge_state` don't have an extra timestep anymore. Use the new `relative_minimum_final_charge_state` and `relative_maximum_final_charge_state` parameters for final state control -### ♻️ Changed +### Changed - Type system overhaul - added clear separation between temporal and non-temporal data throughout codebase for better clarity - Enhanced FlowSystem interface with improved `__repr__()` and `__str__()` methods @@ -477,7 +1824,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Enhanced console output to support both `stdout` and `stderr` stream selection - Added `show_logger_name` parameter to `CONFIG.Logging` for displaying logger names in messages -### 🗑️ Deprecated +### Deprecated - The `agg_group` and `agg_weight` parameters of `TimeSeriesData` are deprecated and will be removed in a future version. Use `aggregation_group` and `aggregation_weight` instead. - The `active_timesteps` parameter of `Calculation` is deprecated and will be removed in a future version. Use the new `sel(time=...)` method on the FlowSystem instead. @@ -500,20 +1847,20 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - `SourceAndSink.sink` → `SourceAndSink.inputs` - `SourceAndSink.prevent_simultaneous_sink_and_source` → `SourceAndSink.prevent_simultaneous_flow_rates` -### 🔥 Removed +### Removed - **Effect share parameters**: The old `specific_share_to_other_effects_*` parameters were replaced WITHOUT DEPRECATION - `specific_share_to_other_effects_operation` → `share_from_temporal` (with inverted direction) - `specific_share_to_other_effects_invest` → `share_from_periodic` (with inverted direction) -### 🐛 Fixed +### Bug Fixes - Enhanced NetCDF I/O with proper attribute preservation for DataArrays - Improved error handling and validation in serialization processes - Better type consistency across all framework components - Added extra validation in `config.py` to improve error handling -### 📝 Docs +### Documentation - Reorganized mathematical notation docs: moved to lowercase `mathematical-notation/` with subdirectories (`elements/`, `features/`, `modeling-patterns/`) - Added comprehensive documentation pages: `dimensions.md` (time/period/scenario), `effects-penalty-objective.md`, modeling patterns @@ -523,11 +1870,11 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Tightened docstrings in core modules with better cross-referencing - Added recipes section to docs -### 🚧 Known Issues +### Known Issues - IO for single Interfaces/Elements to Datasets might not work properly if the Interface/Element is not part of a fully transformed and connected FlowSystem. This arises from Numeric Data not being stored as xr.DataArray by the user. To avoid this, always use the `to_dataset()` on Elements inside a FlowSystem that's connected and transformed. -### 👷 Development +### Development - **Centralized deprecation pattern**: Added `_handle_deprecated_kwarg()` helper method to `Interface` base class that provides reusable deprecation handling with consistent warnings, conflict detection, and optional value transformation. Applied across 5 classes (InvestParameters, Source, Sink, SourceAndSink, Effect) reducing deprecation boilerplate by 72%. - FlowSystem data management simplified - removed `time_series_collection` pattern in favor of direct timestep properties @@ -553,7 +1900,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ## [2.2.0] - 2025-10-11 **Summary:** This release is a Configuration and Logging management release. -### ✨ Added +### Added - Added `CONFIG.reset()` method to restore configuration to default values - Added configurable log file rotation settings: `CONFIG.Logging.max_file_size` and `CONFIG.Logging.backup_count` - Added configurable log format settings: `CONFIG.Logging.date_format` and `CONFIG.Logging.format` @@ -562,21 +1909,21 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - All examples now enable console logging to demonstrate proper logging usage - Console logging now outputs to `sys.stdout` instead of `sys.stderr` for better compatibility with output redirection -### 💥 Breaking Changes +### Breaking Changes - Console logging is now disabled by default (`CONFIG.Logging.console = False`). Enable it explicitly in your scripts with `CONFIG.Logging.console = True` and `CONFIG.apply()` - File logging is now disabled by default (`CONFIG.Logging.file = None`). Set a file path to enable file logging -### ♻️ Changed +### Changed - Logging and Configuration management changed - Improved default logging colors: DEBUG is now gray (`\033[90m`) for de-emphasized messages, INFO uses terminal default color (`\033[0m`) for clean output -### 🗑️ Deprecated +### Deprecated - `change_logging_level()` function is now deprecated in favor of `CONFIG.Logging.level` and `CONFIG.apply()`. Will be removed in version 3.0.0. -### 🔥 Removed +### Removed - Removed unused `config.merge_configs` function from configuration module -### 👷 Development +### Development - Greatly expanded test coverage for `config.py` module - Added `@pytest.mark.xdist_group` to `TestConfigModule` tests to prevent global config interference @@ -585,13 +1932,13 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ## [2.1.11] - 2025-10-05 **Summary:** Important bugfix in `Storage` leading to wrong results due to incorrect discharge losses. -### ♻️ Changed +### Changed - Using `h5netcdf` instead of `netCDF4` for dataset I/O operations. This follows the update in `xarray==2025.09.01` -### 🐛 Fixed +### Bug Fixes - Fix `charge_state` Constraint in `Storage` leading to incorrect losses in discharge and therefore incorrect charge states and discharge values. -### 📦 Dependencies +### Dependencies - Updated `renovate.config` to treat CalVer packages (xarray and dask) with more care - Updated packaging configuration @@ -600,11 +1947,11 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ## [2.1.10] - 2025-09-29 **Summary:** This release is a Documentation and Development release. -### 📝 Docs +### Documentation - Improved CHANGELOG.md formatting by adding better categories and formating by Gitmoji. - Added a script to extract the release notes from the CHANGELOG.md file for better organized documentation. -### 👷 Development +### Development - Improved `renovate.config` - Sped up CI by not running examples in every run and using `pytest-xdist` @@ -614,7 +1961,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Summary:** Small bugfix release addressing network visualization error handling. -### 🐛 Fixed +### Bug Fixes - Fix error handling in network visualization if `networkx` is not installed --- @@ -623,12 +1970,12 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Summary:** Code quality improvements, enhanced documentation, and bug fixes for heat pump components and visualization features. -### ✨ Added +### Added - Extra Check for HeatPumpWithSource.COP to be strictly > 1 to avoid division by zero - Apply deterministic color assignment by using sorted() in `plotting.py` - Add missing args in docstrings in `plotting.py`, `solvers.py`, and `core.py`. -### ♻️ Changed +### Changed - Greatly improved docstrings and documentation of all public classes - Make path handling to be gentle about missing .html suffix in `plotting.py` - Default for `relative_losses` in `Transmission` is now 0 instead of None @@ -636,7 +1983,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Fix some docstrings in plotting.py - Change assertions to raise Exceptions in `plotting.py` -### 🐛 Fixed +### Bug Fixes **Core Components:** - Fix COP getter and setter of `HeatPumpWithSource` returning and setting wrong conversion factors @@ -646,11 +1993,11 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Visualization:** - Fix color scheme selection in network_app; color pickers now update when a scheme is selected -### 📝 Docs +### Documentation - Fix broken links in docs - Fix some docstrings in plotting.py -### 👷 Development +### Development - Pin dev dependencies to specific versions - Improve CI workflows to run faster and smarter @@ -660,10 +2007,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Summary:** Maintenance release to improve Code Quality, CI and update the dependencies. There are no changes or new features. -### ✨ Added +### Added - Added `__version__` to flixopt -### 👷 Development +### Development - ruff format the whole Codebase - Added renovate config - Added pre-commit @@ -678,82 +2025,82 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Summary:** Enhanced Sink/Source components with multi-flow support and new interactive network visualization. -### ✨ Added +### Added - **Network Visualization**: Added `FlowSystem.start_network_app()` and `FlowSystem.stop_network_app()` to easily visualize the network structure of a flow system in an interactive Dash web app - *Note: This is still experimental and might change in the future* -### ♻️ Changed +### Changed - **Multi-Flow Support**: `Sink`, `Source`, and `SourceAndSink` now accept multiple `flows` as `inputs` and `outputs` instead of just one. This enables modeling more use cases with these classes - **Flow Control**: Both `Sink` and `Source` now have a `prevent_simultaneous_flow_rates` argument to prevent simultaneous flow rates of more than one of their flows -### 🗑️ Deprecated +### Deprecated - For the classes `Sink`, `Source` and `SourceAndSink`: `.sink`, `.source` and `.prevent_simultaneous_sink_and_source` are deprecated in favor of the new arguments `inputs`, `outputs` and `prevent_simultaneous_flow_rates` -### 🐛 Fixed +### Bug Fixes - Fixed testing issue with new `linopy` version 0.5.6 -### 👷 Development +### Development - Added dependency "nbformat>=4.2.0" to dev dependencies to resolve issue with plotly CI --- ## [2.1.5] - 2025-07-08 -### 🐛 Fixed +### Bug Fixes - Fixed Docs deployment --- ## [2.1.4] - 2025-07-08 -### 🐛 Fixed +### Bug Fixes - Fixing release notes of 2.1.3, as well as documentation build. --- ## [2.1.3] - 2025-07-08 -### 🐛 Fixed +### Bug Fixes - Using `Effect.maximum_operation_per_hour` raised an error, needing an extra timestep. This has been fixed thanks to @PRse4. --- ## [2.1.2] - 2025-06-14 -### 🐛 Fixed +### Bug Fixes - Storage losses per hour were not calculated correctly, as mentioned by @brokenwings01. This might have led to issues when modeling large losses and long timesteps. - Old implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) \cdot \Delta \text{t}_{i}$ - Correct implementation: $c(\text{t}_{i}) \cdot (1-\dot{\text{c}}_\text{rel,loss}(\text{t}_i)) ^{\Delta \text{t}_{i}}$ -### 🚧 Known Issues +### Known Issues - Just to mention: Plotly >= 6 may raise errors if "nbformat" is not installed. We pinned plotly to <6, but this may be fixed in the future. --- ## [2.1.1] - 2025-05-08 -### ♻️ Changed +### Changed - Improved docstring and tests -### 🐛 Fixed +### Bug Fixes - Fixed bug in the `_ElementResults.constraints` not returning the constraints but rather the variables --- ## [2.1.0] - 2025-04-11 -### ✨ Added +### Added - Python 3.13 support added - Logger warning if relative_minimum is used without on_off_parameters in Flow - Greatly improved internal testing infrastructure by leveraging linopy's testing framework -### 💥 Breaking Changes +### Breaking Changes - Restructured the modeling of the On/Off state of Flows or Components - Variable renaming: `...|consecutive_on_hours` → `...|ConsecutiveOn|hours` - Variable renaming: `...|consecutive_off_hours` → `...|ConsecutiveOff|hours` - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - Similar pattern for all consecutive on/off constraints -### 🐛 Fixed +### Bug Fixes - Fixed the lower bound of `flow_rate` when using optional investments without OnOffParameters - Fixed bug that prevented divest effects from working - Added lower bounds of 0 to two unbounded vars (numerical improvement) @@ -762,10 +2109,10 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir ## [2.0.1] - 2025-04-10 -### ✨ Added +### Added - Logger warning if relative_minimum is used without on_off_parameters in Flow -### 🐛 Fixed +### Bug Fixes - Replace "|" with "__" in filenames when saving figures (Windows compatibility) - Fixed bug that prevented the load factor from working without InvestmentParameters @@ -773,7 +2120,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir **Summary:** 💥 **MAJOR RELEASE** - Complete framework migration from Pyomo to Linopy with redesigned architecture. -### ✨ Added +### Added **Model Capabilities:** - Full model serialization support - save and restore unsolved Models @@ -787,7 +2134,7 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - `to_netcdf/from_netcdf` methods for FlowSystem and core components - xarray integration for TimeSeries with improved datatypes support -### 💥 Breaking Changes +### Breaking Changes **Framework Migration:** - **Optimization Engine**: Complete migration from Pyomo to Linopy optimization framework @@ -802,14 +2149,14 @@ This replaces `specific_share_to_other_effects_*` parameters and inverts the dir - Constraint renaming: `...|consecutive_on_hours_con1` → `...|ConsecutiveOn|con1` - Similar pattern for all consecutive on/off constraints -### 🔥 Removed +### Removed - **Pyomo dependency** (replaced by linopy) - **Period concepts** in time management (simplified to timesteps) -### 🐛 Fixed +### Bug Fixes - Improved infeasible model detection and reporting - Enhanced time series management and serialization - Reduced file size through improved compression -### 📝 Docs +### Documentation - Google Style Docstrings throughout the codebase diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 000000000..831096c6c --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,71 @@ +cff-version: 1.2.0 +message: "If you use this software, please cite it as below and consider citing the related publication." +type: software +title: "flixopt" +version: 8.0.1 # x-release-please-version +date-released: 2026-07-27 +url: "https://github.com/flixOpt/flixopt" +repository-code: "https://github.com/flixOpt/flixopt" +license: MIT +abstract: "FlixOpt (Flexible, Low-entry, Investment, X-sector OPTimization) is a comprehensive framework for modeling and optimizing energy and material flow systems in Python. It enables optimization of diverse applications including district heating networks, industrial production lines, renewable energy portfolios, and supply chain logistics. Built on modern scientific Python stack (linopy and xarray), it provides a progressive enhancement approach allowing users to start with simple models and incrementally add complexity such as multi-period investments, stochastic scenarios, and custom constraints. The framework simplifies the creation of global constraints and switching objectives through its 'effect' concept. The framework is designed for researchers and engineers in energy systems, industrial process optimization, and operations research." +keywords: + - optimization + - energy systems + - energy flow modeling + - linear programming + - mixed-integer programming + - MILP + - operations research + - python + - district heating + - renewable energy + - multi-period optimization + - investment optimization + - capacity planning + - energy modeling + - sector coupling + - energy transition + - industrial processes + - stochastic optimization + - linopy + - xarray +authors: + - family-names: Bumann + given-names: Felix + email: felixbumann387@gmail.com + affiliation: "SachsenEnergie AG" + orcid: "https://orcid.org/0009-0006-0765-4789" + - family-names: Panitz + given-names: Felix + email: baumbude@googlemail.com + affiliation: "Fraunhofer Research Institution for Energy Infrastructures and Geotechnologies IEG" + orcid: "https://orcid.org/0009-0007-7030-6987" + - family-names: Stange + given-names: Peter + email: peter.stange@tu-dresden.de + affiliation: "Chair of Building Energy Systems and Heat Supply, TU Dresden" + orcid: "https://orcid.org/0009-0001-6407-1495" +identifiers: + - type: doi + value: "10.18086/eurosun.2022.04.07" + description: "Software-supported Investment Optimization for District Heating Supply Systems" + - type: url + value: "https://flixopt.github.io/flixopt/latest/" + description: "Documentation" + - type: url + value: "https://pypi.org/project/flixopt/" + description: "PyPI package" +references: + - type: conference-paper + authors: + - family-names: Panitz + given-names: Felix + - family-names: Behrends + given-names: Tim + - family-names: Stange + given-names: Peter + title: "Software-supported Investment Optimization for District Heating Supply Systems" + year: 2022 + conference: + name: "EuroSun 2022" + doi: "10.18086/eurosun.2022.04.07" diff --git a/.github/CONTRIBUTING.md b/CONTRIBUTING.md similarity index 100% rename from .github/CONTRIBUTING.md rename to CONTRIBUTING.md diff --git a/MANIFEST.in b/MANIFEST.in index 383cbef76..6b55b8523 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,7 +11,6 @@ recursive-include flixopt *.py global-exclude *.pyc *.pyo __pycache__ prune .github prune docs -prune examples prune tests prune pics prune scripts @@ -21,6 +20,5 @@ prune .venv prune venv exclude .gitignore exclude .pre-commit-config.yaml -exclude renovate.json exclude mkdocs.yml exclude test_package.sh diff --git a/README.md b/README.md index 0a90dcb33..dfa8216a5 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -# FlixOpt: Energy and Material Flow Optimization Framework +# FlixOpt: Progressive Flow System Optimization + +

+ Flexible  •  Low-entry  •  Investment  •  X-sector  •  OPTimization +

+ +

+ Model more than costs · Easy to prototype · Based on dispatch · Sector coupling · Mathematical optimization +

[![Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg)](https://flixopt.github.io/flixopt/latest/) [![Build Status](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml/badge.svg)](https://github.com/flixOpt/flixopt/actions/workflows/python-app.yaml) @@ -9,18 +17,18 @@ [![PyPI downloads](https://img.shields.io/pypi/dm/flixopt)](https://pypi.org/project/flixopt/) [![GitHub last commit](https://img.shields.io/github/last-commit/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/commits/main) [![GitHub issues](https://img.shields.io/github/issues/flixOpt/flixopt)](https://github.com/flixOpt/flixopt/issues) -[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/flixOpt/flixopt/main.svg)](https://results.pre-commit.ci/latest/github/flixOpt/flixopt/main) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Powered by linopy](https://img.shields.io/badge/powered%20by-linopy-blue)](https://github.com/PyPSA/linopy/) [![Powered by xarray](https://img.shields.io/badge/powered%20by-xarray-blue)](https://xarray.dev/) +[![DOI](https://zenodo.org/badge/540378857.svg)](https://doi.org/10.5281/zenodo.17448623) [![DOI](https://img.shields.io/badge/DOI-10.18086%2Feurosun.2022.04.07-blue)](https://doi.org/10.18086/eurosun.2022.04.07) [![GitHub stars](https://img.shields.io/github/stars/flixOpt/flixopt?style=social)](https://github.com/flixOpt/flixopt/stargazers) --- -**FlixOpt is a Python framework for optimizing energy and material flow systems** - from district heating networks to industrial production lines, from renewable energy portfolios to supply chain logistics. +**FlixOpt is a Python framework for progressive flow system optimization** - from district heating networks to industrial production lines, from renewable energy portfolios to supply chain logistics. -**Start simple, scale complex:** Build a working optimization model in minutes, then progressively add detail - multi-period investments, stochastic scenarios, custom constraints - without rewriting your code. +Build simple models quickly, then incrementally add investment decision, multi-period planning, stochastic scenarios, and custom constraints without refactoring. --- @@ -41,12 +49,12 @@ import flixopt as fx flow_system = fx.FlowSystem(timesteps) flow_system.add_elements(buses, components, effects) -# 2. Create and solve -calculation = fx.FullCalculation("MyModel", flow_system) -calculation.solve() +# 2. Optimize +flow_system.optimize(fx.solvers.HighsSolver()) # 3. Analyze results -calculation.results.solution +flow_system.solution # Raw xarray Dataset +flow_system.stats # Convenient analysis accessor ``` **Get started with real examples:** @@ -64,7 +72,7 @@ calculation.results.solution ```python # Basic single-period model flow_system = fx.FlowSystem(timesteps) -boiler = fx.Boiler("Boiler", eta=0.9, ...) +boiler = fx.linear_converters.Boiler("Boiler", eta=0.9, ...) ``` **Add complexity incrementally:** @@ -88,10 +96,10 @@ boiler = fx.Boiler("Boiler", eta=0.9, ...) ### Key Features **Multi-criteria optimization:** Model costs, emissions, resource use - any custom metric. Optimize single objectives or use weighted combinations and ε-constraints. -→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-penalty-objective/) +→ [Effects documentation](https://flixopt.github.io/flixopt/latest/user-guide/mathematical-notation/effects-and-dimensions/) -**Performance at any scale:** Choose calculation modes without changing your model - Full, Segmented, or Aggregated (using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam)). -→ [Calculation modes](https://flixopt.github.io/flixopt/latest/api-reference/calculation/) +**Performance at any scale:** Choose optimization modes without changing your model - full optimization, rolling horizon, or clustering (using [TSAM](https://github.com/FZJ-IEK3-VSA/tsam)). +→ [Scaling notebooks](https://flixopt.github.io/flixopt/latest/notebooks/08a-aggregation/) **Built for reproducibility:** Self-contained NetCDF result files with complete model information. Load results months later - everything is preserved. → [Results documentation](https://flixopt.github.io/flixopt/latest/api-reference/results/) @@ -185,6 +193,9 @@ If FlixOpt supports your research or project, please cite: - **Main Citation:** [DOI:10.18086/eurosun.2022.04.07](https://doi.org/10.18086/eurosun.2022.04.07) - **Short Overview:** [DOI:10.13140/RG.2.2.14948.24969](https://doi.org/10.13140/RG.2.2.14948.24969) +To pinpoint which version you used in your work, please reference one of these doi's here: +- [![DOI](https://zenodo.org/badge/540378857.svg)](https://doi.org/10.5281/zenodo.17448623) + --- ## 📄 License diff --git a/benchmarks/benchmark_io_performance.py b/benchmarks/benchmark_io_performance.py new file mode 100644 index 000000000..de02285fe --- /dev/null +++ b/benchmarks/benchmark_io_performance.py @@ -0,0 +1,195 @@ +"""Benchmark script for FlowSystem IO performance. + +Tests to_dataset() and from_dataset() performance with large FlowSystems. +Run this to compare performance before/after optimizations. + +Usage: + python benchmarks/benchmark_io_performance.py +""" + +import tempfile +import time +from typing import NamedTuple + +import numpy as np +import pandas as pd + +import flixopt as fx + + +class BenchmarkResult(NamedTuple): + """Results from a benchmark run.""" + + name: str + mean_ms: float + std_ms: float + iterations: int + + +def create_large_flow_system( + n_timesteps: int = 2190, + n_periods: int = 12, + n_components: int = 125, +) -> fx.FlowSystem: + """Create a large FlowSystem for benchmarking. + + Args: + n_timesteps: Number of timesteps (default 2190 = ~1 year at 4h resolution). + n_periods: Number of periods (default 12). + n_components: Number of sink/source pairs (default 125). + + Returns: + Configured FlowSystem. + """ + timesteps = pd.date_range('2024-01-01', periods=n_timesteps, freq='4h') + periods = pd.Index([2028 + i * 2 for i in range(n_periods)], name='period') + + fs = fx.FlowSystem(timesteps=timesteps, periods=periods) + fs.add_elements(fx.Effect('Cost', '€', is_objective=True)) + + n_buses = 10 + buses = [fx.Bus(f'Bus_{i}') for i in range(n_buses)] + fs.add_elements(*buses) + + # Create demand profile with daily pattern + base_demand = 100 + 50 * np.sin(2 * np.pi * np.arange(n_timesteps) / 24) + + for i in range(n_components): + bus = buses[i % n_buses] + # Add noise to create unique profiles + profile = base_demand + np.random.normal(0, 10, n_timesteps) + profile = np.clip(profile / profile.max(), 0.1, 1.0) + + fs.add_elements( + fx.Sink( + f'D_{i}', + inputs=[fx.Flow(f'Q_{i}', bus=bus.label, size=100, fixed_relative_profile=profile)], + ) + ) + fs.add_elements( + fx.Source( + f'S_{i}', + outputs=[fx.Flow(f'P_{i}', bus=bus.label, size=500, effects_per_flow_hour={'Cost': 20 + i})], + ) + ) + + return fs + + +def benchmark_function(func, iterations: int = 5, warmup: int = 1) -> BenchmarkResult: + """Benchmark a function with multiple iterations. + + Args: + func: Function to benchmark (callable with no arguments). + iterations: Number of timed iterations. + warmup: Number of warmup iterations (not timed). + + Returns: + BenchmarkResult with timing statistics. + """ + # Warmup + for _ in range(warmup): + func() + + # Timed runs + times = [] + for _ in range(iterations): + start = time.perf_counter() + func() + elapsed = time.perf_counter() - start + times.append(elapsed) + + return BenchmarkResult( + name=func.__name__ if hasattr(func, '__name__') else str(func), + mean_ms=np.mean(times) * 1000, + std_ms=np.std(times) * 1000, + iterations=iterations, + ) + + +def run_io_benchmarks( + n_timesteps: int = 2190, + n_periods: int = 12, + n_components: int = 125, + iterations: int = 5, +) -> dict[str, BenchmarkResult]: + """Run IO performance benchmarks. + + Args: + n_timesteps: Number of timesteps for the FlowSystem. + n_periods: Number of periods. + n_components: Number of components (sink/source pairs). + iterations: Number of benchmark iterations. + + Returns: + Dictionary mapping benchmark names to results. + """ + print('=' * 70) + print('FlowSystem IO Performance Benchmark') + print('=' * 70) + print('\nConfiguration:') + print(f' Timesteps: {n_timesteps}') + print(f' Periods: {n_periods}') + print(f' Components: {n_components}') + print(f' Iterations: {iterations}') + + # Create FlowSystem + print('\n1. Creating FlowSystem...') + fs = create_large_flow_system(n_timesteps, n_periods, n_components) + print(f' Components: {len(fs.components)}') + + # Create dataset + print('\n2. Creating dataset...') + ds = fs.to_dataset() + print(f' Variables: {len(ds.data_vars)}') + print(f' Size: {ds.nbytes / 1e6:.1f} MB') + + results = {} + + # Benchmark to_dataset + print('\n3. Benchmarking to_dataset()...') + result = benchmark_function(lambda: fs.to_dataset(), iterations=iterations) + results['to_dataset'] = result + print(f' Mean: {result.mean_ms:.1f}ms (std: {result.std_ms:.1f}ms)') + + # Benchmark from_dataset + print('\n4. Benchmarking from_dataset()...') + result = benchmark_function(lambda: fx.FlowSystem.from_dataset(ds), iterations=iterations) + results['from_dataset'] = result + print(f' Mean: {result.mean_ms:.1f}ms (std: {result.std_ms:.1f}ms)') + + # Benchmark NetCDF round-trip + print('\n5. Benchmarking NetCDF round-trip...') + with tempfile.NamedTemporaryFile(suffix='.nc', delete=False) as f: + tmp_path = f.name + + def netcdf_roundtrip(): + fs.to_netcdf(tmp_path, overwrite=True) + return fx.FlowSystem.from_netcdf(tmp_path) + + result = benchmark_function(netcdf_roundtrip, iterations=iterations) + results['netcdf_roundtrip'] = result + print(f' Mean: {result.mean_ms:.1f}ms (std: {result.std_ms:.1f}ms)') + + # Verify restoration + print('\n6. Verification...') + fs_restored = fx.FlowSystem.from_dataset(ds) + print(f' Components restored: {len(fs_restored.components)}') + print(f' Timesteps restored: {len(fs_restored.timesteps)}') + print(f' Periods restored: {len(fs_restored.periods)}') + + # Summary + print('\n' + '=' * 70) + print('Summary') + print('=' * 70) + for name, res in results.items(): + print(f' {name}: {res.mean_ms:.1f}ms (+/- {res.std_ms:.1f}ms)') + + total_ms = results['to_dataset'].mean_ms + results['from_dataset'].mean_ms + print(f'\n Total (to + from): {total_ms:.1f}ms') + + return results + + +if __name__ == '__main__': + run_io_benchmarks() diff --git a/docs/contribute.md b/docs/contribute.md index 557d72a03..a380fa01e 100644 --- a/docs/contribute.md +++ b/docs/contribute.md @@ -1 +1 @@ -{! ../.github/CONTRIBUTING.md !} +{! ../CONTRIBUTING.md !} diff --git a/docs/examples/00-Minimal Example.md b/docs/examples/00-Minimal Example.md deleted file mode 100644 index a568cd9c9..000000000 --- a/docs/examples/00-Minimal Example.md +++ /dev/null @@ -1,5 +0,0 @@ -# Minimal Example - -```python -{! ../examples/00_Minmal/minimal_example.py !} -``` diff --git a/docs/examples/01-Basic Example.md b/docs/examples/01-Basic Example.md deleted file mode 100644 index 6c6bfbee3..000000000 --- a/docs/examples/01-Basic Example.md +++ /dev/null @@ -1,5 +0,0 @@ -# Simple example - -```python -{! ../examples/01_Simple/simple_example.py !} -``` diff --git a/docs/examples/02-Complex Example.md b/docs/examples/02-Complex Example.md deleted file mode 100644 index 48868cdb0..000000000 --- a/docs/examples/02-Complex Example.md +++ /dev/null @@ -1,10 +0,0 @@ -# Complex example -This saves the results of a calculation to file and reloads them to analyze the results -## Build the Model -```python -{! ../examples/02_Complex/complex_example.py !} -``` -## Load the Results from file -```python -{! ../examples/02_Complex/complex_example_results.py !} -``` diff --git a/docs/examples/03-Calculation Modes.md b/docs/examples/03-Calculation Modes.md deleted file mode 100644 index dd0321d43..000000000 --- a/docs/examples/03-Calculation Modes.md +++ /dev/null @@ -1,5 +0,0 @@ -# Calculation Mode comparison -**Note:** This example relies on time series data. You can find it in the `examples` folder of the FlixOpt repository. -```python -{! ../examples/03_Calculation_types/example_calculation_types.py !} -``` diff --git a/docs/examples/index.md b/docs/examples/index.md deleted file mode 100644 index 1df12dc28..000000000 --- a/docs/examples/index.md +++ /dev/null @@ -1,5 +0,0 @@ -# Examples - -Here you can find a collection of examples that demonstrate how to use FlixOpt. - -We work on improving this gallery. If you have something to share, please contact us! diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 5841de3a4..000000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,60 +0,0 @@ -# Getting Started with FlixOpt - -This guide will help you install FlixOpt, understand its basic concepts, and run your first optimization model. - -## Installation - -### Basic Installation - -Install FlixOpt directly into your environment using pip: - -```bash -pip install flixopt -``` - -This provides the core functionality with the HiGHS solver included. - -### Full Installation - -For all features including interactive network visualizations and time series aggregation: - -```bash -pip install "flixopt[full]" -``` - -## Logging - -FlixOpt uses [loguru](https://loguru.readthedocs.io/) for logging. Logging is silent by default but can be easily configured. For beginners, use our internal convenience methods. Experts can use loguru directly. - -```python -from flixopt import CONFIG - -# Enable console logging -CONFIG.Logging.console = True -CONFIG.Logging.level = 'INFO' -CONFIG.apply() - -# Or use a preset configuration for exploring -CONFIG.exploring() -``` - -For more details on logging configuration, see the [`CONFIG.Logging`][flixopt.config.CONFIG.Logging] documentation. - -## Basic Workflow - -Working with FlixOpt follows a general pattern: - -1. **Create a [`FlowSystem`][flixopt.flow_system.FlowSystem]** with a time series -2. **Define [`Effects`][flixopt.effects.Effect]** (costs, emissions, etc.) -3. **Define [`Buses`][flixopt.elements.Bus]** as connection points in your system -4. **Add [`Components`][flixopt.components]** like converters, storage, sources/sinks with their Flows -5. **Run [`Calculations`][flixopt.calculation]** to optimize your system -6. **Analyze [`Results`][flixopt.results]** using built-in or external visualization tools - -## Next Steps - -Now that you've installed FlixOpt and understand the basic workflow, you can: - -- Learn about the [core concepts of flixopt](user-guide/core-concepts.md) -- Explore some [examples](examples/index.md) -- Check the [API reference](api-reference/index.md) for detailed documentation diff --git a/docs/home/citing.md b/docs/home/citing.md new file mode 100644 index 000000000..a4f900d18 --- /dev/null +++ b/docs/home/citing.md @@ -0,0 +1,29 @@ +# Citing flixOpt + +If you use flixOpt in your research, please cite it. + +## Citation + +When referencing flixOpt in academic publications, please use look here: [flixopt citation](https://zenodo.org/records/17756895) + +## Publications + +If you've published research using flixOpt, please let us know! We'd love to feature it here. + +### List of Publications + +Coming soon: A list of academic publications that have used flixOpt. + +## Contributing Back + +If flixOpt helped your research: + +- Share your model as an example +- Report issues or contribute code +- Improve documentation + +See the [Contributing Guide](../contribute.md). + +## License + +flixOpt is released under the MIT License. See [License](license.md) for details. diff --git a/docs/home/installation.md b/docs/home/installation.md new file mode 100644 index 000000000..d61022074 --- /dev/null +++ b/docs/home/installation.md @@ -0,0 +1,91 @@ +# Installation + +This guide covers installing flixOpt and its dependencies. + + +## Basic Installation + +Install flixOpt directly into your environment using pip: + +```bash +pip install flixopt +``` + +This provides the core functionality with the HiGHS solver included. + +## Full Installation + +For all features including interactive network visualizations and time series aggregation: + +```bash +pip install "flixopt[full]" +``` + +## Development Installation + +If you want to contribute to flixOpt or work with the latest development version: + +```bash +git clone https://github.com/flixOpt/flixopt.git +cd flixopt +pip install -e ".[full,dev,docs]" +``` + +## Solver Installation + +### HiGHS (Included) + +The HiGHS solver is included with flixOpt and works out of the box. No additional installation is required. + +### Gurobi (Optional) + +For academic use, Gurobi offers free licenses: + +1. Register for an academic license at [gurobi.com](https://www.gurobi.com/academia/) +2. Install Gurobi: + ```bash + pip install gurobipy + ``` +3. Activate your license following Gurobi's instructions + +## Verification + +Verify your installation by running: + +```python +import flixopt +print(flixopt.__version__) +``` + +## Logging Configuration + +flixOpt uses Python's standard logging module with optional colored output via [colorlog](https://github.com/borntyping/python-colorlog). Logging is silent by default but can be easily configured: + +```python +from flixopt import CONFIG + +# Enable colored console logging +CONFIG.Logging.enable_console('INFO') + +# Or use a preset configuration for exploring +CONFIG.exploring() +``` + +Since flixOpt uses Python's standard logging, you can also configure it directly: + +```python +import logging + +# Get the flixopt logger and configure it +logger = logging.getLogger('flixopt') +logger.setLevel(logging.DEBUG) +logger.addHandler(logging.StreamHandler()) +``` + +For more details on logging configuration, see the [`CONFIG.Logging`][flixopt.config.CONFIG.Logging] documentation. + +## Next Steps + +- Follow the [Quick Start](quick-start.md) guide +- Explore the [Minimal Example](../notebooks/01-quickstart.ipynb) +- Read about [Core Concepts](../user-guide/core-concepts.md) diff --git a/docs/home/license.md b/docs/home/license.md new file mode 100644 index 000000000..e0b0266a4 --- /dev/null +++ b/docs/home/license.md @@ -0,0 +1,43 @@ +# License + +flixOpt is released under the MIT License. + +## MIT License + +```text +MIT License + +Copyright (c) 2022 Chair of Building Energy Systems and Heat Supply - TU Dresden + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. +``` + +## What This Means + +The MIT License is a permissive open-source license that allows you to: + +✅ **Use** flixOpt for any purpose, including commercial applications +✅ **Modify** the source code to fit your needs +✅ **Distribute** copies of flixOpt +✅ **Sublicense** under different terms +✅ **Use privately** without making your modifications public + +## Contributing + +By contributing to flixOpt, you agree that your contributions will be licensed under the MIT License. See our [Contributing Guide](../contribute.md) for more information. diff --git a/docs/home/quick-start.md b/docs/home/quick-start.md new file mode 100644 index 000000000..7bbc88172 --- /dev/null +++ b/docs/home/quick-start.md @@ -0,0 +1,156 @@ +# Quick Start + +Get up and running with flixOpt in 5 minutes! This guide walks you through creating and solving your first energy system optimization. + +## Installation + +First, install flixOpt: + +```bash +pip install "flixopt[full]" +``` + +## Your First Model + +Let's create a simple energy system with a generator, demand, and battery storage. + +### 1. Import flixOpt + +```python +import flixopt as fx +import numpy as np +import pandas as pd +``` + +### 2. Define your time horizon + +```python +# 24h period with hourly timesteps +timesteps = pd.date_range('2024-01-01', periods=24, freq='h') +``` + +### 2. Set Up the Flow System + +```python +# Create the flow system +flow_system = fx.FlowSystem(timesteps) + +# Define an effect to minimize (costs) +costs = fx.Effect('costs', 'EUR', 'Minimize total system costs', is_objective=True) +flow_system.add_elements(costs) +``` + +### 4. Add Components + +```python +# Electricity bus +electricity_bus = fx.Bus('electricity') + +# Solar generator with time-varying output +solar_profile = np.array([0, 0, 0, 0, 0, 0, 0.2, 0.5, 0.8, 1.0, + 1.0, 0.9, 0.8, 0.7, 0.5, 0.3, 0.1, 0, + 0, 0, 0, 0, 0, 0]) + +solar = fx.Source( + 'solar', + outputs=[fx.Flow( + 'power', + bus='electricity', + size=100, # 100 kW capacity + relative_maximum=solar_profile + ) +]) + +# Demand +demand_profile = np.array([30, 25, 20, 20, 25, 35, 50, 70, 80, 75, + 70, 65, 60, 65, 70, 80, 90, 95, 85, 70, + 60, 50, 40, 35]) + +demand = fx.Sink('demand', inputs=[ + fx.Flow('consumption', + bus='electricity', + size=1, + fixed_relative_profile=demand_profile) +]) + +# Battery storage +battery = fx.Storage( + 'battery', + charging=fx.Flow('charge', bus='electricity', size=50), + discharging=fx.Flow('discharge', bus='electricity', size=50), + capacity_in_flow_hours=100, # 100 kWh capacity + initial_charge_state=50, # Start at 50% + eta_charge=0.95, + eta_discharge=0.95, +) + +# Add all components to system +flow_system.add_elements(solar, demand, battery, electricity_bus) +``` + +### 5. Visualize and Run Optimization + +```python +# Optional: visualize your system structure +flow_system.topology.plot(path='system.html') + +# Run optimization +flow_system.optimize(fx.solvers.HighsSolver()) +``` + +### 6. Access and Visualize Results + +```python +# Access raw solution data +print(flow_system.solution) + +# Use statistics for aggregated data +print(flow_system.statistics.flow_hours) + +# Access component-specific results +print(flow_system.components['battery'].solution) + +# Visualize results +flow_system.statistics.plot.balance('electricity') +flow_system.statistics.plot.storage('battery') +``` + +### 7. Save Results (Optional) + +```python +# Save the flow system (includes inputs and solution) +flow_system.to_netcdf('results/solar_battery.nc') + +# Load it back later +loaded_fs = fx.FlowSystem.from_netcdf('results/solar_battery.nc') +``` + +## What's Next? + +Now that you've created your first model, you can: + +- **Learn the concepts** - Read the [Core Concepts](../user-guide/core-concepts.md) guide +- **Explore examples** - Check out more [Examples](../notebooks/index.md) +- **Deep dive** - Study the [Mathematical Formulation](../user-guide/mathematical-notation/index.md) +- **Build complex models** - Use [Recipes](../user-guide/recipes/index.md) for common patterns + +## Common Workflow + +Most flixOpt projects follow this pattern: + +1. **Define time series** - Set up the temporal resolution +2. **Create flow system** - Initialize with time series and effects +3. **Add buses** - Define connection points +4. **Add components** - Create generators, storage, converters, loads +5. **Verify structure** - Use `flow_system.topology.plot()` to visualize +6. **Run optimization** - Call `flow_system.optimize(solver)` +7. **Analyze results** - Via `flow_system.statistics` and `.solution` +8. **Visualize** - Use `flow_system.statistics.plot.*` methods + +## Tips + +- Start simple and add complexity incrementally +- Use meaningful names for components and flows +- Check solver status before analyzing results +- Enable logging during development for debugging +- Visualize results to verify model behavior diff --git a/docs/home/users.md b/docs/home/users.md new file mode 100644 index 000000000..d27f99576 --- /dev/null +++ b/docs/home/users.md @@ -0,0 +1,27 @@ +# Who Uses flixOpt? + +flixOpt is developed and used primarily in academic research for energy system optimization. + +## Primary Users + +- **Researchers** - Energy system modeling and optimization studies +- **Students** - Master's and PhD thesis projects +- **Engineers** - Feasibility studies and system planning + +## Typical Applications + +- Dispatch optimization with renewable integration +- Capacity expansion planning +- Battery and thermal storage sizing +- District heating network optimization +- Combined heat and power (CHP) systems +- Multi-energy systems and sector coupling + +## Get Involved + +Using flixOpt in your research? Consider: + +- [Citing flixOpt](citing.md) in your publications +- Sharing your model as an example +- Contributing to the codebase +- Joining [discussions](https://github.com/flixOpt/flixopt/discussions) diff --git a/docs/index.md b/docs/index.md index c9b01f284..330a33fca 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,21 +1,20 @@ --- title: Home -hide: - - navigation - - toc ---

flixOpt

-

Energy and Material Flow Optimization Framework

+

Flexible · Low-entry · Investment · X-sector · OPTimization

+ +

Model more than costs · Easy to prototype · Based on dispatch · Sector coupling · Mathematical optimization

Model, optimize, and analyze complex energy systems with a powerful Python framework designed for flexibility and performance.

- 🚀 Get Started - 💡 View Examples + 🚀 Get Started + 💡 View Examples ⭐ GitHub

@@ -23,36 +22,44 @@ hide: ## :material-map-marker-path: Quick Navigation -