Skip to content
Open
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
6 changes: 6 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ updates:
directory: "/"
schedule:
interval: "weekly"
ignore:
# The v5→v7 bump silently broke coverage uploads in the sibling
# DecisionRules.jl repo. Keep codecov-action pinned until a deliberate,
# verified migration — see the comment in .github/workflows/CI.yml.
- dependency-name: "codecov/codecov-action"
update-types: ["version-update:semver-major"]
21 changes: 18 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ jobs:
permissions:
actions: write
contents: read
id-token: write # OIDC token for tokenless Codecov uploads (see codecov step)
strategy:
fail-fast: false
matrix:
Expand All @@ -37,8 +38,22 @@ jobs:
- uses: julia-actions/julia-buildpkg@v1
- uses: julia-actions/julia-runtest@v1
- uses: julia-actions/julia-processcoverage@v1
- uses: codecov/codecov-action@v7
# Pinned to v5: codecov-action@v7 silently stopped uploading coverage in
# the sibling DecisionRules.jl repo (Codecov "Missing Head Commit" on
# PRs); v5 is the last version verified to upload from this workflow
# shape. Before re-bumping, migrate deliberately and confirm a commit
# appears on Codecov.
# Authentication uses OIDC (`use_oidc` + the job's `id-token: write`
# permission) because the CODECOV_TOKEN secret is not set in this repo
# ("Token length: 0" in CI) and tokenless uploads are rejected on
# protected branches. OIDC requires the Codecov GitHub App to be
# installed for the organization AND this repository to be activated on
# codecov.io (it currently is not); if uploads fail with an OIDC error,
# either install the app + activate the repo, or set the CODECOV_TOKEN
# secret and replace `use_oidc` with `token: ${{ secrets.CODECOV_TOKEN }}`.
- uses: codecov/codecov-action@v5
with:
files: lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
use_oidc: true
# Fail loudly so upload breakage is visible instead of silent.
fail_ci_if_error: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ logs/
# Slurm batch scripts (user-specific, not part of the package)
*.sbatch
*.sh
examples/HydroPowerModels/results/
15 changes: 12 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,33 +1,42 @@
name = "DecisionRulesExa"
uuid = "7c3e91a4-d8f2-4b6a-9e15-a2c4f7b80d53"
authors = ["Andrew Rosemberg <andrewrosemberg@gmail.com> and contributors"]
version = "0.1.0"
authors = ["Andrew Rosemberg <andrewrosemberg@gmail.com> and contributors"]

[deps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"
ExaModels = "1037b233-b668-4ce9-9b63-f9f681f55dd2"
Flux = "587475ba-b771-5e3f-ad9e-33799f191a9c"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e"
KernelAbstractions = "63c18a36-062a-441e-b654-da1e3ab1ce7c"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598"
NLPModels = "a4795742-8479-5a88-8948-cc11e1c8c1a6"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Tables = "bd369af6-aec1-5ad0-b16a-f7cc5008161c"
Zygote = "e88e6eb3-aa80-5325-afca-941959d7151f"

[compat]
CSV = "0.10.16"
CUDA = "6"
ChainRulesCore = "1.26"
ExaModels = "0.11"
Flux = "0.16"
JSON = "1.6.1"
MadNLP = "0.10"
MadNLPGPU = "0.10"
NLPModels = "0.21"
Tables = "1.13.0"
Zygote = "0.7"
julia = "1.10, 1.11, 1.12"

[extras]
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test"]
test = ["Test", "Statistics"]
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ train_tsddr(
)
```

> **Note on the uncertainty parameter**: `train_tsddr` writes the full sampled
> trajectory (length `T * nw`) into `p_uncertainty` with
> `ExaModels.set_parameter!`, which enforces an exact size match (ExaModels ≥
> 0.11). The `p_w` built by `build_deterministic_equivalent` /
> `build_linear_tracking_problem` holds only the `(T - 1) * nw` dynamics
> entries, so for `train_tsddr` your NLP needs an uncertainty parameter of
> length `T * nw` (as the Hydro example's `p_inflow` is). See the
> `"train_tsddr open-loop smoke test"` testset in `test/runtests.jl` for a
> minimal full-length variant of the problem above.

For GPU, replace `backend = nothing` with `backend = CUDABackend()` and add `linear_solver = CUDSSSolver` to `madnlp_kwargs`.

## What you need to provide
Expand All @@ -73,6 +83,58 @@ For a custom problem you need:

The package provides `build_deterministic_equivalent` for generic problems and `build_linear_tracking_problem` as a ready-made demo. For domain-specific models (power systems, robotics), build the ExaModels NLP directly — see `examples/HydroPowerModels/` for a complete AC-OPF example.

## Strict reachable target equality

The usual TS-DDR deterministic equivalent uses slack-penalized target
constraints,

```text
x_t - pi_theta(w_t, x_{t-1}) = delta_t,
objective += rho * penalty(delta_t).
```

This is the right default for open-loop target trajectories and for cases where
the policy can request states that are not reachable from the previous realized
state. The target multipliers are then gradients of the penalized projection
problem, so their quality depends on the penalty calibration.

For policies whose output is guaranteed to lie in a one-stage reachable state
set, a stricter formulation is possible:

```text
x_t = pi_theta(w_t, x_{t-1}), pi_theta(w_t, x_{t-1}) in R(w_t, x_{t-1}).
```

In that case the deterministic equivalent does not need target slack variables
or target penalties. The multiplier on the equality is the local envelope
sensitivity of the true stage problem with respect to the policy-imposed next
state, not the sensitivity of a penalized approximation. This is useful when:

- users can define a differentiable or piecewise differentiable map into a
subset of the one-stage reachable set;
- total recourse is guaranteed by the model for every state produced by that
map.

There are two strict hydro paths:

- **Embedded strict DE** evaluates the policy inside the NLP against realized
reservoir states. This is the usual way to make strict mode safe because the
policy sees the state from which its next target must be reachable.
- **Regular strict DE with reachable rollout** computes targets before solving
the NLP, but starts from the true initial state and feeds the previous target
back to the reachable policy. If
`x̂_t ∈ R(x̂_{t-1}, w_t)` and `x̂_0 = x_0`, the full target path is feasible by
induction. The strict equality then forces the realized path to equal that
reachable target path.

Do not use strict equality for a generic open-loop target policy. For
unreachable targets, the slack-penalty formulation is the robust fallback.

The hydro reachable policy keeps recurrence over inflows only. Optional
`combiner_layers` / `DR_HEAD_LAYERS` add a nonlinear feed-forward map from
`[encoded_inflow; reservoir_state]` to targets without adding recurrence over
the state input.

## Parallel GPU solves

When training samples are independent, multiple NLP instances can be solved concurrently on the same GPU. Pass a `problem_pool` of independent ExaModels problem copies to `train_tsddr`:
Expand Down Expand Up @@ -217,7 +279,24 @@ Choose DecisionRules.jl when:

- [`examples/end_to_end_cpu.jl`](examples/end_to_end_cpu.jl) — minimal CPU demo with a linear tracking problem
- [`examples/end_to_end_gpu.jl`](examples/end_to_end_gpu.jl) — same demo on GPU with CUDSS
- [`examples/HydroPowerModels/`](examples/HydroPowerModels/) — full multi-stage hydrothermal scheduling with DC and AC OPF
- [`examples/HydroPowerModels/`](examples/HydroPowerModels/) — full multi-stage hydrothermal scheduling with DC and AC OPF (open-loop DE, embedded closed-loop, strict targets, critic control variate)

## Repository Map

| Path | Purpose |
|---|---|
| `src/DecisionRulesExa.jl` | Module entrypoint and public exports |
| `src/policy.jl` | MLP, state-conditioned LSTM policies, bounded target policies, nonlinear target heads |
| `src/deterministic_equivalent.jl` | Generic open-loop deterministic-equivalent builder and solve helpers |
| `src/embedded_deterministic_equivalent.jl` | Generic embedded-policy deterministic equivalent with nonlinear oracle |
| `src/training.jl` | `train_tsddr`, embedded training, solver retry/warm-start handling |
| `src/rollout.jl` | Stage-wise rollout evaluation for ExaModels problems |
| `src/critic_control_variate.jl` | Scalar critic/control-variate helpers |
| `src/utils.jl` | Indexing and small shared utilities |
| `examples/end_to_end_cpu.jl` | Minimal CPU training demo |
| `examples/end_to_end_gpu.jl` | Minimal GPU training demo |
| `examples/HydroPowerModels/` | Bolivia hydrothermal scheduling examples |
| `test/runtests.jl` | Unit and smoke tests |

## Citation

Expand Down
1 change: 1 addition & 0 deletions examples/HydroPowerModels/Project.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[deps]
StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3"
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e"
Expand Down
Loading
Loading