Skip to content

Commit 18b3dac

Browse files
committed
feat(solve): implement solve_descriptive with strict option routing
- Implemented (Layer 2) to handle symbolic method descriptions - Added for strict option routing logic via CTSolvers - Added comprehensive unit tests in - Updated and to verify descriptive mode - Updated design doc - Moved kanban task to REVIEW
1 parent 19e47ce commit 18b3dac

9 files changed

Lines changed: 976 additions & 95 deletions

File tree

.reports/kanban_orchestration/DOING/05_commonsolve_solve.md renamed to .reports/kanban_orchestration/REVIEW/05_commonsolve_solve.md

File renamed without changes.

.reports/solve_descriptive.md

Lines changed: 255 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -2,81 +2,279 @@
22

33
**Layer**: 2 (Mode-Specific Logic - Descriptive Mode)
44

5-
## R0 - High-Level Description
5+
## R0 — Rôle et flux général
66

7-
`solve_descriptive` solves an optimal control problem using symbolic method descriptions (e.g., `:collocation`, `:adnlp`, `:ipopt`). It:
7+
`solve_descriptive` est le point d'entrée du mode **descriptif** : l'utilisateur passe des symboles (`:collocation`, `:adnlp`, `:ipopt`) et des options à plat (kwargs), et la fonction doit :
88

9-
1. Completes partial descriptions using the available methods registry
10-
2. Builds components from the complete description
11-
3. Handles component options and routing
9+
1. **Compléter** la description partielle en un triplet complet `(discretizer_id, modeler_id, solver_id)`
10+
2. **Router** les kwargs vers les bonnes stratégies via `CTSolvers.Orchestration.route_all_options`
11+
3. **Construire** les trois stratégies concrètes avec leurs options routées
12+
4. **Appeler** la couche canonique (Layer 3) avec les composants complets
1213

13-
## R1 - Signature and Delegation
14+
### Contexte d'appel (Layer 1 → Layer 2)
15+
16+
`dispatch.jl` appelle `solve_descriptive` après avoir :
17+
18+
- Normalisé `initial_guess` (via `CTModels.build_initial_guess`)
19+
- Créé ou extrait le `registry` (`CTSolvers.StrategyRegistry`)
20+
- Filtré les composants explicites (mode descriptif = aucun composant explicite dans kwargs)
1421

1522
```julia
16-
# ============================================================================
17-
# LAYER 2: Descriptive Mode - NO defaults (all values explicit from Layer 1)
18-
# ============================================================================
23+
# dispatch.jl (Layer 1 → Layer 2, mode descriptif)
24+
return solve_descriptive(
25+
ocp, description...;
26+
initial_guess = normalized_init,
27+
display = display,
28+
registry = registry,
29+
kwargs... # options à plat pour les stratégies
30+
)
31+
```
32+
33+
Les `kwargs` reçus par `solve_descriptive` sont **exclusivement** des options de stratégies
34+
(plus éventuellement des `RoutedOption` via `route_to`). Il n'y a **pas** de composants
35+
explicites (`discretizer=`, `modeler=`, `solver=`) car ceux-ci auraient déclenché
36+
`ExplicitMode` dans `_explicit_or_descriptive`.
37+
38+
---
39+
40+
## R1 — Signature et corps de haut niveau
1941

42+
```julia
2043
function solve_descriptive(
21-
ocp::AbstractModel,
22-
initial_guess::AbstractInitialGuess; # Already normalized by Layer 1
23-
description::Symbol..., # Symbolic description (may be partial)
24-
discretizer::Union{AbstractDiscretizer, Nothing}, # Optional override
25-
modeler::Union{AbstractNLPModeler, Nothing}, # Optional override
26-
solver::Union{AbstractNLPSolver, Nothing}, # Optional override
27-
display::Bool, # NO default
28-
kwargs... # Component options
29-
)::AbstractSolution
30-
31-
# 1. Complete the description using available methods registry
32-
complete_description = CTBase.complete(
33-
description...;
34-
descriptions=available_methods()
35-
)
36-
37-
# 2. Validate and route component options
38-
_validate_component_options(complete_description, kwargs)
39-
40-
# 3. Build components from complete description
41-
# Use provided components as overrides if present
42-
components = _build_components_from_description(
43-
complete_description;
44-
discretizer_override=discretizer,
45-
modeler_override=modeler,
46-
solver_override=solver,
47-
kwargs...
48-
)
49-
50-
# 4. Call canonical solve with complete components
44+
ocp::CTModels.AbstractModel,
45+
description::Symbol...;
46+
initial_guess::CTModels.AbstractInitialGuess, # normalisé par Layer 1
47+
display::Bool, # sans défaut
48+
registry::CTSolvers.StrategyRegistry,
49+
kwargs... # options stratégies (plat + route_to)
50+
)::CTModels.AbstractSolution
51+
52+
# 1. Compléter la description partielle → triplet complet
53+
complete_description = _complete_description(description)
54+
55+
# 2. Router toutes les options vers les familles de stratégies
56+
routed = _route_descriptive_options(complete_description, registry, kwargs)
57+
58+
# 3. Construire les trois stratégies avec leurs options routées
59+
components = _build_components_from_routed(complete_description, registry, routed)
60+
61+
# 4. Appel canonique (Layer 3)
5162
return CommonSolve.solve(
5263
ocp, initial_guess,
5364
components.discretizer,
5465
components.modeler,
5566
components.solver;
56-
display=display
67+
display = display
68+
)
69+
end
70+
```
71+
72+
**Invariants** :
73+
74+
- Aucun défaut dans cette fonction (tout vient de Layer 1)
75+
- `_complete_description` réutilise le helper existant dans `helpers/strategy_builders.jl`
76+
- `_route_descriptive_options` encapsule l'appel à `CTSolvers.Orchestration.route_all_options`
77+
- `_build_components_from_routed` construit les stratégies via `CTSolvers.Strategies.build_strategy_from_method`
78+
79+
---
80+
81+
## R2 — Fonctions helpers à implémenter
82+
83+
### R2.1 — `_route_descriptive_options` (nouveau helper)
84+
85+
**Rôle** : Encapsule `CTSolvers.Orchestration.route_all_options` avec les familles et
86+
`action_defs` propres à OptimalControl.
87+
88+
```julia
89+
function _route_descriptive_options(
90+
complete_description::Tuple{Symbol, Symbol, Symbol},
91+
registry::CTSolvers.Strategies.StrategyRegistry,
92+
kwargs
93+
)
94+
families = _descriptive_families()
95+
action_defs = _descriptive_action_defs()
96+
return CTSolvers.Orchestration.route_all_options(
97+
complete_description,
98+
families,
99+
action_defs,
100+
NamedTuple(kwargs),
101+
registry;
102+
source_mode = :description,
103+
mode = :strict
104+
)
105+
end
106+
```
107+
108+
**Fichier** : `src/helpers/descriptive_routing.jl`
109+
110+
### R2.2 — `_descriptive_families` (nouveau helper, pur)
111+
112+
**Rôle** : Retourne le `NamedTuple` des familles abstraites pour le routage.
113+
114+
```julia
115+
function _descriptive_families()
116+
return (
117+
discretizer = CTDirect.AbstractDiscretizer,
118+
modeler = CTSolvers.AbstractNLPModeler,
119+
solver = CTSolvers.AbstractNLPSolver,
120+
)
121+
end
122+
```
123+
124+
**Fichier** : `src/helpers/descriptive_routing.jl`
125+
126+
### R2.3 — `_descriptive_action_defs` (nouveau helper, pur)
127+
128+
**Rôle** : Retourne les `OptionDefinition` pour les options d'action (niveau `solve`),
129+
c'est-à-dire les options qui ne sont **pas** des options de stratégies.
130+
131+
> **Note** : `display` et `initial_guess` sont gérés par Layer 1 et ne parviennent
132+
> **pas** dans les `kwargs` de `solve_descriptive`. Les `action_defs` sont donc vides
133+
> pour l'instant. Ce helper existe pour extensibilité future.
134+
135+
```julia
136+
function _descriptive_action_defs()
137+
return CTSolvers.Options.OptionDefinition[]
138+
end
139+
```
140+
141+
**Fichier** : `src/helpers/descriptive_routing.jl`
142+
143+
### R2.4 — `_build_components_from_routed` (nouveau helper)
144+
145+
**Rôle** : Construit les trois stratégies concrètes à partir du résultat de routage.
146+
147+
```julia
148+
function _build_components_from_routed(
149+
complete_description::Tuple{Symbol, Symbol, Symbol},
150+
registry::CTSolvers.Strategies.StrategyRegistry,
151+
routed::NamedTuple
152+
)
153+
discretizer = CTSolvers.Strategies.build_strategy_from_method(
154+
complete_description,
155+
CTDirect.AbstractDiscretizer,
156+
registry;
157+
routed.strategies.discretizer...
158+
)
159+
modeler = CTSolvers.Strategies.build_strategy_from_method(
160+
complete_description,
161+
CTSolvers.AbstractNLPModeler,
162+
registry;
163+
routed.strategies.modeler...
164+
)
165+
solver = CTSolvers.Strategies.build_strategy_from_method(
166+
complete_description,
167+
CTSolvers.AbstractNLPSolver,
168+
registry;
169+
routed.strategies.solver...
57170
)
171+
return (discretizer=discretizer, modeler=modeler, solver=solver)
58172
end
59173
```
60174

61-
### Functions Called (R2 candidates)
175+
**Fichier** : `src/helpers/descriptive_routing.jl`
176+
177+
---
178+
179+
## R3 — Gestion du mode strict vs permissive
180+
181+
### Principe
182+
183+
`route_all_options` accepte un paramètre `mode::Symbol` (`:strict` ou `:permissive`) :
184+
185+
- **`:strict`** (défaut) : toute option inconnue lève une `IncorrectArgument`
186+
- **`:permissive`** : une option inconnue *avec* `route_to` est acceptée avec un warning
187+
188+
### Règle adoptée pour `solve_descriptive`
189+
190+
**Toujours `:strict`** dans un premier temps. La raison :
191+
192+
- En mode descriptif, l'utilisateur passe des options à plat. Si une option est inconnue,
193+
c'est presque toujours une faute de frappe → erreur claire préférable.
194+
- Le mode `:permissive` est utile pour des backends expérimentaux qui ajoutent des options
195+
non déclarées dans les métadonnées. Ce cas peut être géré plus tard.
196+
197+
### Extension future (mode permissive)
198+
199+
Si l'utilisateur veut passer une option non déclarée à une stratégie spécifique, il peut
200+
utiliser `route_to` avec un mode permissif. Pour l'activer, il suffirait d'exposer un
201+
paramètre `validation_mode::Symbol=:strict` dans `solve_descriptive` et de le passer à
202+
`_route_descriptive_options`. Cette extension est **YAGNI** pour l'instant.
203+
204+
---
205+
206+
## R4 — Plan d'action
207+
208+
### Étape 1 : Nouveau fichier `src/helpers/descriptive_routing.jl`
209+
210+
Implémenter les 3 helpers purs (R2.2, R2.3, R2.4) et le helper principal (R2.1).
211+
Ces fonctions sont **directement testables** sans mock complexe.
212+
213+
Ajouter l'include dans `OptimalControl.jl` :
214+
215+
```julia
216+
include(joinpath(@__DIR__, "helpers", "descriptive_routing.jl"))
217+
```
218+
219+
### Étape 2 : Implémenter `solve_descriptive` dans `src/solve/descriptive.jl`
220+
221+
Remplacer le stub `NotImplemented` par l'implémentation réelle (R1).
222+
223+
### Étape 3 : Tests unitaires — `test/suite/solve/test_descriptive_routing.jl`
224+
225+
Tests des helpers purs (sans mock OCP, sans vrai solver) :
226+
227+
- `_descriptive_families` : structure correcte
228+
- `_descriptive_action_defs` : liste vide
229+
- `_route_descriptive_options` : auto-routing, disambiguation, erreurs
230+
- `_build_components_from_routed` : construction avec options routées
231+
232+
Utiliser des mocks de stratégies (pattern des tests CTSolvers) pour éviter les dépendances
233+
sur les vrais backends.
234+
235+
### Étape 4 : Tests d'intégration — mise à jour de `test/suite/solve/test_orchestration.jl`
236+
237+
Remplacer le test `"solve_descriptive raises NotImplemented"` par des tests réels :
238+
239+
- Descriptive mode complet : `solve(ocp, :collocation, :adnlp, :ipopt; display=false)`
240+
- Descriptive mode partiel : `solve(ocp, :collocation; display=false)`
241+
- Descriptive mode vide : `solve(ocp; display=false)`
242+
- Options routées : `solve(ocp, :collocation, :adnlp, :ipopt; grid_size=10, display=false)`
243+
- Disambiguation : `solve(ocp, ...; backend=route_to(adnlp=:sparse), display=false)`
244+
- Erreur option inconnue : `@test_throws IncorrectArgument solve(ocp, ...; bad_opt=1)`
245+
- Erreur option ambiguë : `@test_throws IncorrectArgument solve(ocp, ...; backend=:sparse)`
246+
247+
---
248+
249+
## R5 — Fichiers à créer / modifier
250+
251+
| Fichier | Action |
252+
| --- | --- |
253+
| `src/helpers/descriptive_routing.jl` | **Créer** — helpers R2.1–R2.4 |
254+
| `src/solve/descriptive.jl` | **Modifier** — remplacer stub par implémentation R1 |
255+
| `src/OptimalControl.jl` | **Modifier** — ajouter include de `descriptive_routing.jl` |
256+
| `test/suite/solve/test_descriptive_routing.jl` | **Créer** — tests unitaires helpers |
257+
| `test/suite/solve/test_orchestration.jl` | **Modifier** — remplacer test stub, ajouter intégration |
258+
| `test/runtests.jl` | **Modifier** — enregistrer `test_descriptive_routing` |
259+
260+
---
261+
262+
## R6 — Points d'attention
62263

63-
- `CTBase.complete(description...; descriptions=available_methods())` - Complete partial description
64-
- `_validate_component_options(complete_description, kwargs)` - Validate option routing
65-
- `_build_components_from_description(...)` - Build components from symbols
66-
- `CommonSolve.solve(ocp, initial_guess, discretizer, modeler, solver; display)` - Canonical solve (Layer 3)
264+
1. **`NamedTuple(kwargs)`** : `kwargs` dans `solve_descriptive` est un `Base.Pairs`.
265+
Il faut le convertir en `NamedTuple` pour `route_all_options`. Utiliser `(; kwargs...)`.
67266

68-
### Responsibilities
267+
2. **Splatting des options routées** : `routed.strategies.discretizer` est un `NamedTuple`.
268+
Le splatting `routed.strategies.discretizer...` passe les options comme kwargs à
269+
`build_strategy_from_method`.
69270

70-
1. **Description completion**: Transform partial symbolic description to complete triplet
71-
2. **Option routing**: Route kwargs to appropriate components (discretizer/modeler/solver)
72-
3. **Component construction**: Build concrete components from symbolic description
73-
4. **Override handling**: Allow explicit components to override description-based ones
74-
5. **Canonical solve invocation**: Call Layer 3 with complete components
271+
3. **`_complete_description` existe déjà** dans `helpers/strategy_builders.jl` — la
272+
réutiliser directement.
75273

76-
### Key Design Decisions
274+
4. **`CTSolvers.Orchestration`** est importé via `src/imports/ctsolvers.jl` mais
275+
`route_all_options` n'est pas encore importé. Il faudra vérifier si un import
276+
supplémentaire est nécessaire ou si l'accès qualifié `CTSolvers.Orchestration.route_all_options`
277+
suffit.
77278

78-
- **No defaults**: All parameters are explicit (passed from Layer 1)
79-
- **Registry-based**: Uses `available_methods()` registry for completion
80-
- **Flexible description**: Accepts empty, partial, or complete symbolic descriptions
81-
- **Component overrides**: Explicit components take precedence over description
82-
- **Option validation**: Ensures kwargs are routed to correct components
279+
5. **`CTSolvers.Options.OptionDefinition`** est importé comme `OptionDefinition` dans
280+
`ctsolvers.jl` — utiliser ce nom court dans les helpers.

src/OptimalControl.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ include(joinpath(@__DIR__, "helpers", "registry.jl"))
2929
include(joinpath(@__DIR__, "helpers", "component_checks.jl"))
3030
include(joinpath(@__DIR__, "helpers", "strategy_builders.jl"))
3131
include(joinpath(@__DIR__, "helpers", "component_completion.jl"))
32+
include(joinpath(@__DIR__, "helpers", "descriptive_routing.jl"))
3233

3334
# solve
3435
include(joinpath(@__DIR__, "solve", "mode.jl"))

0 commit comments

Comments
 (0)