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
4 changes: 4 additions & 0 deletions .github/dco.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# DCO GitHub App: require a sign-off on every commit of a pull request.
# Members of the organisation are checked too (no exemption).
require:
members: true
1 change: 1 addition & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Fill one row per line you add to `CHANGELOG.md`. "Not needed" is an answer only
- [ ] No frozen public API change: existing parameters keep their name, order and default (`tests/test_api_contracts.py`); new ones are keyword arguments with a default.
- [ ] No `print()` in new code.
- [ ] Data used by tests and examples is public.
- [ ] Every commit is signed off (`git commit -s`, see CONTRIBUTING.md).
- [ ] Every added file keeps its `SPDX-FileCopyrightText` and `SPDX-License-Identifier` header; its author is named as `SPDX-FileContributor`.

## Pre-push verdict
Expand Down
32 changes: 32 additions & 0 deletions .github/workflows/reuse.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: REUSE

on:
push:
pull_request:
workflow_dispatch:

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

concurrency:
group: reuse-${{ github.ref }}
cancel-in-progress: true

jobs:
reuse:
name: REUSE lint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install reuse
run: pip install reuse

- name: Check licences and copyright on every file
run: reuse lint
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
> **[0.0.6]** corresponds to the legacy `weakl` package available on PyPI.

---
## [Unreleased]

- The `DCO` sign-off is required on every commit of a pull request (`DCO`, `CONTRIBUTING.md`, `docs/contributing/dco_howto.md`); a `REUSE` workflow runs `reuse lint` on each push and pull request.
- The `pid()` page of `architecture/core/06_the_spectrum_api.md` says when `d_pen` acts and that the derivative is computed along the frame given to `predict()`; three tests pin `pid` behaviour.

## [1.4.0] - 2026-10-06

First release on PyPI and Zenodo since 1.3.1; it also carries everything of 1.3.2 to 1.3.4, which were tags only. **`AdaptiveTAM`, `KalmanTAM` and `NeuralTAM` [EXP] forecasts change** (see 1.3.3, 1.3.4 and the sections below); `StaticTAM` changes only for `te()` with GCV (-2%), a mixture mean (-0.6%) and a prediction without the group column. To get the 1.3.1 behaviour: `pip install tam-ml==1.3.1`.
Expand Down
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,15 @@ A body of work too large for one pull request (a new model family, a research co
6. **Submit your PR:** Provide a clear description of the problem solved, the mathematical approach taken, and the performance implications.

---
*By contributing to TAM, you agree that your contributions will be licensed under the project's LGPL license.*

## 6. Licence of Contributions and Sign-off (DCO)

By contributing, you agree that your contribution is licensed under the licence of the file it changes (LGPL-3.0-or-later for the code, see `LICENSES/` and `REUSE.toml` for the data and documents).

Every commit must carry a sign-off line, which certifies the [Developer Certificate of Origin 1.1](DCO):

```
Signed-off-by: Your Name <your.email@example.com>
```

`git commit -s -m "short phrase"` adds it. The `DCO` check of a pull request fails when a commit lacks it. How to fix a missing sign-off: [docs/contributing/dco_howto.md](docs/contributing/dco_howto.md).
34 changes: 34 additions & 0 deletions DCO
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.


Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or

(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or

(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.

(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
34 changes: 34 additions & 0 deletions LICENSES/LicenseRef-DCO-1.1.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Developer Certificate of Origin
Version 1.1

Copyright (C) 2004, 2006 The Linux Foundation and its contributors.

Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.


Developer's Certificate of Origin 1.1

By making a contribution to this project, I certify that:

(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or

(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or

(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.

(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.
7 changes: 7 additions & 0 deletions REUSE.toml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,10 @@ path = "src/tam/data/airpassengers.csv"
precedence = "closest"
SPDX-FileCopyrightText = "Box, Jenkins and Reinsel, Time Series Analysis: Forecasting and Control (monthly international airline passengers, 1949-1960)"
SPDX-License-Identifier = "CC0-1.0"

# The Developer Certificate of Origin 1.1, copied verbatim (changing it is not allowed by its own text).
[[annotations]]
path = "DCO"
precedence = "closest"
SPDX-FileCopyrightText = "2004, 2006 The Linux Foundation and its contributors"
SPDX-License-Identifier = "LicenseRef-DCO-1.1"
4 changes: 4 additions & 0 deletions architecture/core/06_the_spectrum_api.md
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,10 @@ Constructs a $3 \times 3$ diagonal stiffness matrix. Uniquely applies an artific
:end-before: "#: </penalty_matrix>"
```

**Reading the parameters.** `d_pen` multiplies the ridge penalty of the derivative term only, relative to the penalty `ap` of the effect. With the default penalty (1e-9) that ridge is already negligible, so `d_pen` changes nothing; it acts with an explicit penalty (`ap=-2`), where a large `d_pen` penalises the derivative out. `auto_fit` tunes that penalty, and on a formula that already holds the lag's difference it often shrinks the whole effect, which leaves `d_pen` nothing to scale. The tests `test_d_pen_*` in `tests/spectrum/test_pid.py` pin this behaviour.

**Frame dependence.** The derivative and the rolling mean are computed along the rows of the frame given to `predict()`: the first `w` rows of each group of a frame get values that training never saw. Predict a frame that holds the history (the training rows, then the new rows) and slice the result; a short frame alone gives wrong PID features. With a trending lag that leaves the trained range, the P and I terms extrapolate the raw lag, and `extrapolate='linear'` can be much worse than the default.

### Control Diagnostics (Bode Stability)

**Core Interface:** `bode.py`
Expand Down
31 changes: 31 additions & 0 deletions docs/contributing/dco_howto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Signing off your commits (DCO)

The sign-off says you wrote the change, or have the right to submit it, under the licence of the project ([DCO](../../DCO)). It is a line at the end of the commit message.

## New commits

```bash
git commit -s -m "fix: short phrase"
```

Use the name and email set in `git config user.name` and `git config user.email`; they must match your commits.

## The last commit lacks it

```bash
git commit --amend -s --no-edit
git push --force-with-lease
```

## Several commits of the branch lack it

```bash
git rebase --signoff main
git push --force-with-lease
```

Replace `main` by the branch your pull request targets. Only force-push your own feature branch.

## The check still fails

Open the `DCO` check of the pull request: it lists the commits without a sign-off and gives the commands above for them.
40 changes: 40 additions & 0 deletions tests/spectrum/test_pid.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,43 @@ def test_pid_zero_window_uses_full_cumulative_integral(normalized):
def test_pid_negative_window_raises():
with pytest.raises(ValueError, match="non-negative"):
PIDEffect("x", window=-1, lambda_p=1.0, d_penalty_multiplier=1.0, extrapolate="continue")


def _derivative_signal(n=1500, n_train=1000):
"""A stationary AR(1) ``x`` whose derivative carries the information: ``y = x + 5 dx + noise``."""
import numpy as np
import pandas as pd

rng = np.random.default_rng(0)
x = np.zeros(n)
for t in range(1, n):
x[t] = 0.8 * x[t - 1] + rng.normal()
y = x + 5 * np.diff(x, prepend=x[0]) + rng.normal(0, 0.3, n)
df = pd.DataFrame({"timestamp": pd.date_range("2022-01-01", periods=n, freq="h"), "grp": "G", "x": x, "y": y})
return df, n_train


def _test_rmse(formula):
import numpy as np

df, n_train = _derivative_signal()
model = tam.StaticTAM(formula=formula, group_col="grp", date_col="timestamp")
model.fit(df.iloc[:n_train])
pred = model.predict(df)["Estimatedy"].to_numpy()[n_train:]
return float(np.sqrt(np.mean((pred - df["y"].to_numpy()[n_train:]) ** 2)))


def test_pid_beats_a_linear_term_when_the_derivative_carries_the_signal():
assert _test_rmse("y ~ pid(x, w=6, d_pen=0)") < 0.2 * _test_rmse("y ~ l(x)")


def test_d_pen_is_hidden_by_the_default_penalty():
"""With the default (negligible) penalty, d_pen multiplies a ridge that is already ~0: the forecasts agree."""
low, high = _test_rmse("y ~ pid(x, w=6, d_pen=0)"), _test_rmse("y ~ pid(x, w=6, d_pen=100)")
assert abs(low - high) < 0.01 * low


def test_d_pen_acts_when_the_penalty_is_explicit():
"""With an explicit penalty (ap=-2) a large d_pen penalises the derivative out and the forecast gets worse."""
low, high = _test_rmse("y ~ pid(x, w=6, d_pen=0, ap=-2)"), _test_rmse("y ~ pid(x, w=6, d_pen=100, ap=-2)")
assert high > 1.1 * low
10 changes: 9 additions & 1 deletion tests/test_spdx_headers.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def test_every_empty_python_file_is_annotated_in_reuse_toml():

def test_every_licence_used_has_its_text_in_the_licenses_folder():
used = set(re.findall(r'SPDX-License-Identifier = "([^"]+)"', (ROOT / "REUSE.toml").read_text(encoding="utf-8")))
used |= {m for p in FILES for m in re.findall(r"SPDX-License-Identifier: (\S+)", "\n".join(_header(p)[1]))}
used |= {m for p in FILES for m in re.findall(r"SPDX-License-" r"Identifier: (\S+)", "\n".join(_header(p)[1]))}
assert {"LGPL-3.0-or-later", "CC-BY-4.0", "CC0-1.0"} <= used
missing = sorted(name for name in used if not (ROOT / "LICENSES" / f"{name}.txt").exists())
assert not missing, missing
Expand All @@ -90,3 +90,11 @@ def test_every_data_file_is_annotated_in_reuse_toml():
assert data, "no data file found"
missing = [name for name in data if f'"{name}"' not in reuse]
assert not missing, missing


def test_a_file_carries_one_licence_tag_not_a_pattern_that_looks_like_one():
# `reuse lint` reads every licence tag it finds, in code and strings included: a regular expression written with the tag in one piece made a test file fail it
# ("invalid SPDX license expression"). A file holds its header tag and nothing that looks like another.
tag = "SPDX-License-" + "Identifier:"
several = {p.relative_to(ROOT).as_posix(): n for p in FILES if (n := p.read_text(encoding="utf-8", errors="replace").count(tag)) > 1}
assert not several, several
Loading