Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .flake8

This file was deleted.

34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,11 @@ jobs:
ruff check src/ tests/
continue-on-error: true

- name: Type check with ty (non-blocking)
run: |
ty check src/
continue-on-error: true

security:
name: Security Scan
runs-on: ubuntu-latest
Expand Down Expand Up @@ -229,3 +234,32 @@ jobs:
sarif_file: bandit-report.sarif
category: bandit
continue-on-error: true

docs-reference-sync:
name: API Reference In Sync
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0

- name: Set up uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
cache-dependency-glob: "pyproject.toml"

- name: Regenerate the quartodoc API reference
run: |
uv run python docs/quartodoc_build.py
uv run quartodoc interlinks

- name: Fail if committed docs/reference is stale
run: |
if ! git diff --exit-code -- docs/reference/; then
echo "::error::docs/reference is out of sync with quartodoc. Run 'uv run python docs/quartodoc_build.py && uv run quartodoc interlinks' locally and commit the result."
exit 1
fi
12 changes: 3 additions & 9 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,9 @@ repos:
- id: ruff
args: [--fix]

# Type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies:
- types-all
args: [--no-error-summary]
exclude: ^(tests/|docs/)
# Type checking now runs in CI via Astral `ty` (non-blocking); the heavy
# mypy + types-all pre-commit hook was removed during the tooling-baseline
# convergence (roadmap #6).

# YAML validation
- repo: https://github.com/pre-commit/pre-commit-hooks
Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## [19.2.0-rc.1](https://github.com/sequential-parameter-optimization/spotforecast2-safe/compare/v19.1.0...v19.2.0-rc.1) (2026-06-08)


### Features

* **weather:** population-weighted multi-city index + degree-hours/apparent-temperature/dew-point features ([1942c7b](https://github.com/sequential-parameter-optimization/spotforecast2-safe/commit/1942c7bfe417624bae308c45f7023923fbcd8ea6))

## [19.1.0](https://github.com/sequential-parameter-optimization/spotforecast2-safe/compare/v19.0.0...v19.1.0) (2026-06-07)


Expand Down
8 changes: 4 additions & 4 deletions MODEL_CARD.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ This card describes what spotforecast2-safe is, how to use it safely, the condit
| Field | Value |
| --- | --- |
| Name | spotforecast2-safe |
| Version | 19.1.0 |
| Version | 19.2.0-rc.1 |
| Type | Deterministic Python library for time series feature engineering and recursive multi-step forecasting. It performs no training of its own. |
| Developed by | Thomas Bartz-Beielstein, ORCID [0000-0002-5938-5158](https://orcid.org/0000-0002-5938-5158) |
| Distributed by | the `sequential-parameter-optimization` GitHub organization |
Expand All @@ -18,7 +18,7 @@ This card describes what spotforecast2-safe is, how to use it safely, the condit

The library depends only on numpy, pandas, scikit-learn, lightgbm, numba, pyarrow, requests, feature-engine, holidays, astral, and tqdm. It deliberately excludes plotly, matplotlib, spotoptim, optuna, torch, and tensorflow, so no plotting or automated-tuning code ships in this package.

Two Common Platform Enumeration (CPE) identifiers let vulnerability-tracking and software bill of materials (SBOM) tools recognize the package. The wildcard identifier `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:*:*:*:*:*:*:*:*` matches any release; the current release is `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:19.1.0:*:*:*:*:*:*:*`.
Two Common Platform Enumeration (CPE) identifiers let vulnerability-tracking and software bill of materials (SBOM) tools recognize the package. The wildcard identifier `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:*:*:*:*:*:*:*:*` matches any release; the current release is `cpe:2.3:a:sequential_parameter_optimization:spotforecast2_safe:19.2.0-rc.1:*:*:*:*:*:*:*`.

The library itself is a low-risk component: it is deterministic, its source is fully inspectable, and it fails safe on invalid input. It is built to support high-risk AI systems in the sense of the EU AI Act, but it is not itself such a system. When it is embedded in a high-risk deployment, the duties that attach to that system fall on the integrator, not on the library.

Expand All @@ -30,7 +30,7 @@ Responsibilities are divided as follows.
| Distribution | sequential-parameter-optimization on GitHub | repository issue tracker |
| Deployment, operation, and audit | the system integrator | defined per deployment |

The current release is 19.1.0, with a stable public interface pinned in `spotforecast2_safe.__init__.__all__`. The full version history, including release dates, is recorded in `CHANGELOG.md` and on the GitHub Releases page; it is maintained automatically by the release pipeline and is not repeated here.
The current release is 19.2.0-rc.1, with a stable public interface pinned in `spotforecast2_safe.__init__.__all__`. The full version history, including release dates, is recorded in `CHANGELOG.md` and on the GitHub Releases page; it is maintained automatically by the release pipeline and is not repeated here.

## 2. Intended Use and Scope

Expand Down Expand Up @@ -216,7 +216,7 @@ Maintainer: Thomas Bartz-Beielstein, ORCID [0000-0002-5938-5158](https://orcid.o
}
```

Or as a formatted reference: Bartz-Beielstein, T. (2026). *spotforecast2-safe: Safety-critical subset of spotforecast2* (Version 19.1.0) [Computer software]. https://github.com/sequential-parameter-optimization/spotforecast2-safe
Or as a formatted reference: Bartz-Beielstein, T. (2026). *spotforecast2-safe: Safety-critical subset of spotforecast2* (Version 19.2.0-rc.1) [Computer software]. https://github.com/sequential-parameter-optimization/spotforecast2-safe

The technical report (`bart26h/index.qmd`) is the long-form reference for design rationale, compliance mapping, and evaluation protocol.

Expand Down
2 changes: 1 addition & 1 deletion REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ SPDX-PackageSupplier = "bartzbeielstein <32470350+bartzbeielstein@users.noreply.
SPDX-PackageDownloadLocation = "https://github.com/sequential-parameter-optimization/spotforecast2-safe"

[[annotations]]
path = [".flake8", ".gitignore", ".python-version", ".releaserc.json", ".semantic-release-init", "mkdocs.yml", "pyproject.toml", "uv.lock", "junit.xml"]
path = [".gitignore", ".python-version", ".releaserc.json", ".semantic-release-init", "mkdocs.yml", "pyproject.toml", "uv.lock", "junit.xml"]
precedence = "aggregate"
SPDX-FileCopyrightText = "2026 bartzbeielstein"
SPDX-License-Identifier = "AGPL-3.0-or-later"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"hash": "275de27a383338eb229b788a42e8ac50",
"result": {
"engine": "jupyter",
"markdown": "---\ntitle: manager.demo_metrics.calculate_metrics\n---\n\n\n\n```python\nmanager.demo_metrics.calculate_metrics(actual, predicted)\n```\n\nCalculate MAE and MSE for numeric evaluation.\n\nComputes Mean Absolute Error (MAE) and Mean Squared Error (MSE) between\nactual and predicted values. These metrics are essential for evaluating\nforecasting model performance in safety-critical applications.\n\n## Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|-----------|------------------------------------------|---------------------------------------------------------------|------------|\n| actual | [pd](`pandas`).[Series](`pandas.Series`) | Series of actual observed values. | _required_ |\n| predicted | [pd](`pandas`).[Series](`pandas.Series`) | Series of predicted values (must have same length as actual). | _required_ |\n\n## Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|---------------------------------------------------------|---------------------------------------------------------------------------------------------------|\n| | [Dict](`typing.Dict`)\\[[str](`str`), [float](`float`)\\] | Dict[str, float]: Dictionary containing: - 'MAE': Mean Absolute Error - 'MSE': Mean Squared Error |\n\n## Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------|---------------------------------------------------------|\n| | [ValueError](`ValueError`) | If series have different lengths or contain NaN values. |\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#51aa8de6 .cell execution_count=1}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Perfect predictions: both MAE and MSE should be zero\nactual = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])\npredicted = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])\nmetrics = calculate_metrics(actual, predicted)\nprint(f\"MAE: {metrics['MAE']:.4f}\")\nprint(f\"MSE: {metrics['MSE']:.4f}\")\nassert metrics[\"MAE\"] == 0.0\nassert metrics[\"MSE\"] == 0.0\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMAE: 0.0000\nMSE: 0.0000\n```\n:::\n:::\n\n\n::: {#9a1b7136 .cell execution_count=2}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Small symmetric errors: MAE == MSE == 1.0\nactual = pd.Series([10.0, 20.0, 30.0, 40.0])\npredicted = pd.Series([11.0, 19.0, 31.0, 39.0])\nmetrics = calculate_metrics(actual, predicted)\nprint(f\"MAE: {metrics['MAE']:.4f}\")\nprint(f\"MSE: {metrics['MSE']:.4f}\")\nassert metrics[\"MAE\"] == 1.0\nassert metrics[\"MSE\"] == 1.0\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMAE: 1.0000\nMSE: 1.0000\n```\n:::\n:::\n\n\n::: {#8439531d .cell execution_count=3}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Larger asymmetric errors\nactual = pd.Series([100.0, 200.0, 300.0])\npredicted = pd.Series([95.0, 210.0, 290.0])\nmetrics = calculate_metrics(actual, predicted)\nprint(f\"MAE: {metrics['MAE']:.4f}\")\nprint(f\"MSE: {metrics['MSE']:.4f}\")\nassert abs(metrics[\"MAE\"] - 25 / 3) < 1e-9\nassert abs(metrics[\"MSE\"] - 75.0) < 1e-9\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMAE: 8.3333\nMSE: 75.0000\n```\n:::\n:::\n\n\n::: {#4271bd79 .cell execution_count=4}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Safety-critical: validate metrics stay within acceptable bounds\nactual = pd.Series([50.0, 55.0, 60.0, 65.0, 70.0])\npredicted = pd.Series([51.0, 54.0, 61.0, 64.0, 71.0])\nmetrics = calculate_metrics(actual, predicted)\nassert metrics[\"MAE\"] < 2.0, \"MAE exceeds safety threshold\"\nassert metrics[\"MSE\"] < 5.0, \"MSE exceeds safety threshold\"\nprint(\"Safety validation passed\")\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nSafety validation passed\n```\n:::\n:::\n\n\n::: {#cd27106b .cell execution_count=5}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Time series with a datetime index\ndates = pd.date_range(\"2024-01-01\", periods=5, freq=\"D\")\nactual = pd.Series([10.5, 11.2, 10.8, 11.5, 12.0], index=dates)\npredicted = pd.Series([10.3, 11.4, 10.9, 11.3, 12.1], index=dates)\nmetrics = calculate_metrics(actual, predicted)\nprint(f\"MAE: {metrics['MAE']:.4f}\")\nprint(f\"MSE: {metrics['MSE']:.4f}\")\nassert abs(metrics[\"MAE\"] - 0.16) < 1e-9\nassert abs(metrics[\"MSE\"] - 0.028) < 1e-9\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nMAE: 0.1600\nMSE: 0.0280\n```\n:::\n:::\n\n\n::: {#b5316029 .cell execution_count=6}\n``` {.python .cell-code}\nimport pandas as pd\nfrom spotforecast2_safe.manager.demo_metrics import calculate_metrics\n\n# Compare two models: lower MAE wins\nactual = pd.Series([1.0, 2.0, 3.0, 4.0, 5.0])\npred_model_a = pd.Series([1.1, 2.1, 2.9, 4.2, 4.8])\npred_model_b = pd.Series([1.5, 2.5, 3.5, 4.5, 5.5])\nmetrics_a = calculate_metrics(actual, pred_model_a)\nmetrics_b = calculate_metrics(actual, pred_model_b)\nwinner = \"A\" if metrics_a[\"MAE\"] < metrics_b[\"MAE\"] else \"B\"\nprint(f\"Model A MAE: {metrics_a['MAE']:.4f}\")\nprint(f\"Model B MAE: {metrics_b['MAE']:.4f}\")\nprint(f\"Model {winner} has better MAE\")\nassert winner == \"A\"\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nModel A MAE: 0.1400\nModel B MAE: 0.5000\nModel A has better MAE\n```\n:::\n:::\n\n\n",
"supporting": [
"manager.demo_metrics.calculate_metrics_files"
],
"filters": [],
"includes": {}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"hash": "58c242eba1427e390e9e505d5a3555db",
"result": {
"engine": "jupyter",
"markdown": "---\ntitle: manager.predictor.get_model_prediction\n---\n\n\n\n```python\nmanager.predictor.get_model_prediction(\n model_name,\n model_dir=None,\n predict_size=None,\n)\n```\n\nGet the prediction package from the latest trained model.\n\nThis function retrieves the latest iteration of a specified model from the\ncache and calls its `package_prediction` method to obtain a comprehensive\nset of predictions and metrics.\n\n## Parameters {.doc-section .doc-section-parameters}\n\n| Name | Type | Description | Default |\n|--------------|--------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------|------------|\n| model_name | [str](`str`) | Name of the model to use (e.g., 'lgbm', 'xgb'). | _required_ |\n| model_dir | [Optional](`typing.Optional`)\\[[Union](`typing.Union`)\\[[str](`str`), [Path](`pathlib.Path`)\\]\\] | Directory where models are stored. If None, defaults to the library's cache home. | `None` |\n| predict_size | [Optional](`typing.Optional`)\\[[int](`int`)\\] | Optional override for the prediction horizon. | `None` |\n\n## Returns {.doc-section .doc-section-returns}\n\n| Name | Type | Description |\n|--------|------------------------------------------------------------|-------------------------------------------------------------|\n| | [Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\] | A dictionary containing predictions and metrics produced by |\n| | [Dict](`typing.Dict`)\\[[str](`str`), [Any](`typing.Any`)\\] | `package_prediction()`. |\n\n## Raises {.doc-section .doc-section-raises}\n\n| Name | Type | Description |\n|--------|----------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------|\n| | [FileNotFoundError](`FileNotFoundError`) | If no trained model is found for `model_name` in `model_dir`. |\n| | [AttributeError](`AttributeError`) | If the loaded model does not implement a `package_prediction` method. |\n| | [PredictionPackageError](`PredictionPackageError`) | If `package_prediction()` itself fails — see `forecaster.wrappers.model.ForecasterRecursiveModel.package_prediction`. |\n| | [OSError](`OSError`) | If the on-disk model file exists but cannot be deserialised (corrupt joblib). |\n\n## Notes {.doc-section .doc-section-notes}\n\n`predict_size` is accepted by `get_model_prediction()` but only has effect if the concrete model's `package_prediction()` accepts it.\nThe original `ForecasterRecursiveModel.package_prediction()` does not — so this parameter is currently forward-looking API design, not yet wired end-to-end.\n\n## Examples {.doc-section .doc-section-examples}\n\n\n::: {#bffee86b .cell execution_count=1}\n``` {.python .cell-code}\nimport tempfile\nfrom spotforecast2_safe.manager.predictor import get_model_prediction\n\n# When no model has been trained, get_model_prediction raises FileNotFoundError.\nwith tempfile.TemporaryDirectory() as tmpdir:\n try:\n get_model_prediction(\"lgbm\", model_dir=tmpdir)\n except FileNotFoundError as e:\n print(type(e).__name__)\n assert \"lgbm\" in str(e)\n```\n\n::: {.cell-output .cell-output-stdout}\n```\nFileNotFoundError\n```\n:::\n:::\n\n\n",
"supporting": [
"manager.predictor.get_model_prediction_files"
],
"filters": [],
"includes": {}
}
}
Loading