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
10 changes: 9 additions & 1 deletion .github/workflows/Breakage.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,18 @@ name: Breakage
# no access to secrets
on:
pull_request:
types: [labeled, synchronize, opened, reopened]
types: [labeled, synchronize, reopened]

jobs:
call:
# A 'labeled' event fires once per label added, and re-evaluates against the PR's
# *current* full label set — so adding several labels in a row would otherwise
# re-run this job on every subsequent label add. Only react to 'labeled' when it's
# this specific label; for other trigger types (synchronize/reopened), fall back to
# checking the current label set as before.
if: >
(github.event.action == 'labeled' && github.event.label.name == 'run breakage') ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run breakage'))
strategy:
fail-fast: false
matrix:
Expand Down
37 changes: 34 additions & 3 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,43 @@ on:
- main
tags: '*'
pull_request:

types: [labeled, synchronize, reopened]

jobs:
call:
# Job for GitHub-hosted runners (ubuntu, macos, windows)
test-cpu-github:
# A 'labeled' event fires once per label added, and re-evaluates against the PR's
# *current* full label set — so adding several labels in a row would otherwise
# re-run this job on every subsequent label add. Only react to 'labeled' when it's
# this specific label; for other trigger types (synchronize/reopened), fall back to
# checking the current label set as before.
if: >
github.event_name != 'pull_request' ||
(github.event.action == 'labeled' && github.event.label.name == 'run ci github-runner') ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run ci github-runner'))
uses: control-toolbox/CTActions/.github/workflows/ci.yml@main
with:
runs_on: '["ubuntu-latest", "macos-latest", "windows-latest"]'
runner_type: 'github'
use_ct_registry: true
secrets:
SSH_KEY: ${{ secrets.SSH_KEY }}

# Job for the self-hosted kkt runner (GPU/CUDA). Runs CTDirect's GPU-specific test
# suite (test/test_gpu.jl, selected via test_args=GPU — see test/runtests.jl), not
# the plain suite: replaces the former dedicated GPU.yml workflow.
test-gpu-kkt:
# See the comment on test-cpu-github above.
if: >
github.event_name != 'pull_request' ||
(github.event.action == 'labeled' && github.event.label.name == 'run ci kkt-runner') ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run ci kkt-runner'))
uses: control-toolbox/CTActions/.github/workflows/ci.yml@main
with:
runs_on: '["ubuntu-latest","macos-latest"]'
versions: '["1"]'
runs_on: '[["kkt"]]'
runner_type: 'self-hosted'
use_ct_registry: true
test_args: 'GPU'
secrets:
SSH_KEY: ${{ secrets.SSH_KEY }}
12 changes: 11 additions & 1 deletion .github/workflows/Documentation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,19 @@ on:
- main
tags: '*'
pull_request:

types: [labeled, synchronize, reopened]

jobs:
call:
# A 'labeled' event fires once per label added, and re-evaluates against the PR's
# *current* full label set — so adding several labels in a row would otherwise
# re-run this job on every subsequent label add. Only react to 'labeled' when it's
# this specific label; for other trigger types (synchronize/reopened), fall back to
# checking the current label set as before.
if: >
github.event_name != 'pull_request' ||
(github.event.action == 'labeled' && github.event.label.name == 'run documentation') ||
(github.event.action != 'labeled' && contains(github.event.pull_request.labels.*.name, 'run documentation'))
uses: control-toolbox/CTActions/.github/workflows/documentation.yml@main
with:
use_ct_registry: true
Expand Down
49 changes: 0 additions & 49 deletions .github/workflows/GPU.yml

This file was deleted.

6 changes: 4 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "CTDirect"
uuid = "790bbbee-bee9-49ee-8912-a9de031322d5"
version = "1.0.12"
version = "1.1.0-beta"
authors = ["Pierre Martinon <pierrecmartinon@gmail.com>"]

[workspace]
Expand All @@ -26,6 +26,7 @@ CTModels = "0.15"
CTParser = "0.8"
CTSolvers = "0.4"
CUDA = "5, 6"
CUDSS = "0.6, 0.7, 0.8"
CommonSolve = "0.2"
DocStringExtensions = "0.9"
ExaModels = "0.11"
Expand All @@ -44,6 +45,7 @@ julia = "1.10"
AMDGPU = "21141c5a-9bdb-4563-92ae-f87d6854732e"
CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1"
CUDA = "052768ef-5323-5732-b1bb-66c8b64840ba"
CUDSS = "45b445bb-4962-46a0-9369-b4df9d0f772e"
CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
MadNLP = "2621e9c9-9eb4-46b1-8089-e8c72242dfb6"
MadNLPGPU = "d72a61cc-809d-412f-99be-fd81f4b8a598"
Expand All @@ -53,4 +55,4 @@ SplitApplyCombine = "03a91e81-4c3e-53e1-a0a4-9c0c8f19dd66"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["AMDGPU", "CTParser", "CUDA", "CommonSolve", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "SplitApplyCombine", "Test"]
test = ["AMDGPU", "CTParser", "CUDA", "CUDSS", "CommonSolve", "MadNLP", "MadNLPGPU", "NLPModels", "NLPModelsIpopt", "SplitApplyCombine", "Test"]
12 changes: 12 additions & 0 deletions probe/shape/Project.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[deps]
ADNLPModels = "54578032-b7ea-4c30-94aa-7cbd1cce6c9a"
CTBase = "54762871-cc72-4466-b8e8-f6c8b58076cd"
CTDirect = "790bbbee-bee9-49ee-8912-a9de031322d5"
CTModels = "34c4fa32-2049-4079-8329-de33c2a22e2d"
CTParser = "32681960-a1b1-40db-9bff-a1ca817385d1"
CTSolvers = "d3e8d392-8e4b-4d9b-8e92-d7d4e3650ef6"
CommonSolve = "38540f10-b2f7-11e9-35d8-d573e4eb0ff2"
ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210"
NLPModelsIpopt = "f4238b75-b362-5c4c-b852-0801c9a21d71"
Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
58 changes: 58 additions & 0 deletions probe/shape/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# Shape probe — "1-D = scalar" boundary audit

A **diagnostic** (not a test suite) that measures, against the *current, unmodified*
CTDirect, what shapes the transcription actually hands to the user OCP functions stored
in a `CTModels.Model`, and what the cost of each candidate fix would be.

It backs the roadmap item
[#610 §7 / discussion #609 §7](https://github.com/control-toolbox/CTDirect.jl/discussions/609)
and the Handbook rule
[`philosophy/dimension-and-shape.md`](https://github.com/control-toolbox/Handbook/blob/main/philosophy/dimension-and-shape.md):

> A one-dimensional quantity is a `scalar`, never a length-1 vector.

Nothing here asserts pass/fail. Every experiment is wrapped in `try/catch`, so a single
failure never stops the run; each outcome is collected into capability matrices printed
at the bottom of the log.

## Running it

```console
julia --project=probe/shape probe/shape/probe_shape.jl
```

The header activates this folder's environment and `Pkg.develop`s the checked-out
CTDirect, so the probe always measures the working-tree source.

## What it probes

- **A. Boundary shapes today** — a *type-recording* OCP (dynamics, Lagrange, Mayer, path
and boundary constraints each record the types they were handed, and compute with `[1]`
so they stay correct for either shape) is driven through `__objective` /
`__constraints!` for every collocation scheme, once with `n=m=q=1` and once with
`n=m=q=2`. The cell is `arg1/arg2/arg3` — `x/u/v` for dynamics, Lagrange and path;
`x0/xf/v` for Mayer and boundary.
- **B. Scalar-written user functions** — an OCP whose dynamics is written in pure scalar
style (`r[1] = -x + u`, no `[1]` on the *inputs*). This is exactly the code the
convention exists to make legal.
- **C. Option A audit** — coercing *inside the getters* would hand a scalar to every
internal consumer too, not only to the user functions. This block measures whether each
such consumer survives a scalar: the free-time accessors, the solution-building writes,
the midpoint/trapeze state algebra, and the IRK stage state.
- **D. Solution side** — solve a 1-D problem and inspect what `state(sol)(t)`,
`control(sol)(t)`, `costate(sol)(t)` return.
- **E. `only` as the coercion** — idempotence on a scalar, behaviour on a `SubArray`
view, transparency under `ForwardDiff`, behaviour on an *empty* collection, and the
allocation count.

## Measured (2026-07-27, branch `refactor/docp-dispatch`)

| Block | Result |
| --- | --- |
| A | **Uniformly non-compliant**: all 7 schemes × 5 user functions pass `Vec(1)` for every 1-D quantity. n-D is correct (`Vec(n)`). |
| B | **Fails on every scheme** (`MethodError`): scalar-written user functions are currently illegal. |
| C | Free-time accessors take **both** shapes (`CTModels` has a `ctNumber` method). Broken by a scalar: `build_OCP_solution`'s `v[:] = …` (`ArgumentError`) and trapeze's `x_next += h*(w_i+w_{i+1})` (`MethodError`). Unreachable by any getter: the IRK stage state, which is a **work buffer**. |
| D | **Already compliant** — `CTModels` wraps 1-D trajectories with `only` (`Solutions/interpolation_helpers.jl`), so `state`/`control`/`costate` already return `Number`. |
| E | `only` is idempotent on a scalar, 0-allocation on a length-1 view, AD-transparent, and **throws on an empty collection** — so the dimension-driven map must be `dim == 1 ? only : identity` (never `dim <= 1`). |

The full report built on these numbers lives in `.reports/` (local, git-ignored).
Loading
Loading