Skip to content

Commit 73e2314

Browse files
committed
wip
1 parent 89a581e commit 73e2314

1 file changed

Lines changed: 214 additions & 7 deletions

File tree

src/Utils/interface.jl

Lines changed: 214 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -166,27 +166,208 @@ end
166166
"""
167167
$TYPEDEF
168168
169-
Abstract type interface for stochastic benchmark problems.
170-
This type should be used for benchmarks that involve single stage stochastic optimization problems.
171-
172-
It follows the same interface as [`AbstractBenchmark`](@ref), with the addition of the following methods:
173-
- TODO
169+
Abstract type interface for single-stage stochastic benchmark problems.
170+
171+
A stochastic benchmark separates the problem into a **deterministic instance** (the
172+
context known before the scenario is revealed) and a **random scenario** (the uncertain
173+
part). The combinatorial oracle sees only the instance; scenarios are used to evaluate
174+
anticipative solutions, generate targets, and compute objective values.
175+
176+
# Required methods (exogenous benchmarks, `{true}` only)
177+
- [`generate_sample`](@ref)`(bench, rng)`: returns a [`DataSample`](@ref) with instance
178+
and features but **no scenario**. The scenario is omitted so that
179+
[`generate_dataset`](@ref) can draw K independent scenarios from the same instance.
180+
- [`generate_scenario`](@ref)`(bench, sample, rng)`: draws a random scenario for the
181+
instance encoded in `sample`. The full sample is passed (not just the instance)
182+
because context is tied to the instance and implementations may need fields beyond
183+
`sample.instance`.
184+
185+
# Optional methods
186+
- [`generate_anticipative_solver`](@ref)`(bench)`: returns a callable
187+
`(scenario; kwargs...) -> y` that computes the anticipative solution per scenario.
188+
- [`generate_parametric_anticipative_solver`](@ref)`(bench)`: returns a callable
189+
`(θ, scenario; kwargs...) -> y` for the parametric anticipative subproblem
190+
`argmin_{y ∈ Y} c(y, scenario) + θᵀy`.
191+
- [`generate_instance_samples`](@ref)`(bench, sample, scenarios; compute_targets,
192+
kwargs...)`: maps K scenarios to `DataSample`s for one instance. Override to change
193+
the scenario→sample mapping (e.g. SAA: K scenarios → 1 sample with shared target).
194+
195+
# Dataset generation (exogenous only)
196+
[`generate_dataset`](@ref) is specialised for `AbstractStochasticBenchmark{true}` and
197+
supports all three standard structures via `nb_scenarios_per_instance`:
198+
199+
| Setting | Call |
200+
|---------|------|
201+
| 1 instance with K scenarios | `generate_dataset(bench, 1; nb_scenarios_per_instance=K)` |
202+
| N instances with 1 scenario | `generate_dataset(bench, N)` (default) |
203+
| N instances with K scenarios | `generate_dataset(bench, N; nb_scenarios_per_instance=K)` |
204+
205+
Extra keyword arguments are forwarded to [`generate_instance_samples`](@ref), enabling
206+
solver choice to reach target computation (e.g. `algorithm=compact_mip`).
207+
208+
By default, each [`DataSample`](@ref) has `context` holding the instance (solver kwargs)
209+
and `extra=(; scenario)` holding one scenario. Override
210+
[`generate_instance_samples`](@ref) to store scenarios differently (e.g.
211+
`extra=(; scenarios=[ξ₁,…,ξ_K])` for SAA).
174212
"""
175213
abstract type AbstractStochasticBenchmark{exogenous} <: AbstractBenchmark end
176214

177215
is_exogenous(::AbstractStochasticBenchmark{exogenous}) where {exogenous} = exogenous
178216
is_endogenous(::AbstractStochasticBenchmark{exogenous}) where {exogenous} = !exogenous
179217

180218
"""
181-
generate_scenario(::AbstractStochasticBenchmark{true}, instance; kwargs...)
219+
generate_scenario(::AbstractStochasticBenchmark{true}, sample::DataSample,
220+
rng::AbstractRNG) -> scenario
221+
222+
Draw a random scenario for the instance encoded in `sample`.
223+
Called once per scenario by the specialised [`generate_dataset`](@ref).
224+
225+
The full `sample` is passed (not just `sample.instance`) because both the scenario
226+
and the context are tied to the same instance — implementations may need any field
227+
of the sample. Consistent with [`generate_environment`](@ref) for dynamic benchmarks.
182228
"""
183229
function generate_scenario end
184230

185231
"""
186-
generate_anticipative_solution(::AbstractStochasticBenchmark{true}, instance, scenario; kwargs...)
232+
generate_anticipative_solver(::AbstractStochasticBenchmark) -> callable
233+
234+
Return a callable `(scenario; kwargs...) -> y` that computes the anticipative solution for a given
235+
scenario. The instance and other solver-relevant fields are spread from the sample context:
236+
237+
solver = generate_anticipative_solver(bench)
238+
y = solver(scenario; sample.context...)
239+
240+
This mirrors the maximizer calling convention `maximizer(θ; sample.context...)`.
241+
242+
Used by Imitating Anticipative and DAgger algorithms. Replaces the deprecated
243+
[`generate_anticipative_solution`](@ref).
244+
"""
245+
function generate_anticipative_solver(bench::AbstractStochasticBenchmark)
246+
return (scenario; kwargs...) -> error(
247+
"`generate_anticipative_solver` is not implemented for $(typeof(bench)). " *
248+
"Implement `generate_anticipative_solver(::$(typeof(bench))) -> (scenario; kwargs...) -> y` " *
249+
"to use `compute_targets=true`.",
250+
)
251+
end
252+
253+
"""
254+
generate_parametric_anticipative_solver(::AbstractStochasticBenchmark) -> callable
255+
256+
**Optional.** Return a callable `(θ, scenario; kwargs...) -> y` that solves the
257+
parametric anticipative subproblem:
258+
259+
argmin_{y ∈ Y(instance)} c(y, scenario) + θᵀy
260+
261+
The scenario comes first (it defines the stochastic cost function); `θ` is the
262+
perturbation added on top, coupling the benchmark to the model output.
263+
264+
The κ weight from the Alternating Minimization algorithm is not a parameter of this
265+
solver. Since the subproblem is linear in `θ`, the algorithm scales θ by κ before
266+
calling: `solver(κ * θ, scenario; sample.context...)`.
267+
268+
Partially apply `scenario` to obtain a `(θ; kwargs...) -> y` closure, then wrap in
269+
`PerturbedAdditive` (InferOpt) to compute targets `μᵢ` during the decomposition step.
270+
"""
271+
function generate_parametric_anticipative_solver end
272+
273+
"""
274+
generate_anticipative_solution(::AbstractStochasticBenchmark, instance, scenario; kwargs...)
275+
276+
!!! warning "Deprecated"
277+
Use [`generate_anticipative_solver`](@ref) instead, which returns a callable
278+
`(scenario; kwargs...) -> y` consistent with the [`generate_maximizer`](@ref)
279+
convention.
187280
"""
188281
function generate_anticipative_solution end
189282

283+
"""
284+
$TYPEDSIGNATURES
285+
286+
Map K scenarios to [`DataSample`](@ref)s for a single instance (encoded in `sample`).
287+
288+
This is the key customisation point for scenario→sample mapping in
289+
[`generate_dataset`](@ref).
290+
291+
**Default** (anticipative / DAgger — 1:1 mapping):
292+
Returns K samples, each with one scenario in `extra=(; scenario=ξ)`.
293+
When `compute_targets=true`, calls [`generate_anticipative_solver`](@ref) to compute
294+
an independent anticipative target per scenario.
295+
296+
**Override for batch strategies** (e.g. SAA):
297+
Return fewer samples (or one) using all K scenarios together. Extra keyword arguments
298+
forwarded from [`generate_dataset`](@ref) reach here, enabling solver choice:
299+
300+
```julia
301+
function generate_instance_samples(bench::MySAABench, sample, scenarios;
302+
compute_targets=false, algorithm=my_solver, kwargs...)
303+
y = compute_targets ? algorithm(sample.instance, scenarios; kwargs...) : nothing
304+
return [DataSample(; x=sample.x, θ=sample.θ, y, sample.context...,
305+
extra=(; scenarios))]
306+
end
307+
```
308+
"""
309+
function generate_instance_samples(
310+
bench::AbstractStochasticBenchmark{true},
311+
sample::DataSample,
312+
scenarios::AbstractVector;
313+
compute_targets::Bool=false,
314+
kwargs...,
315+
)
316+
solver = generate_anticipative_solver(bench)
317+
return [
318+
DataSample(;
319+
x=sample.x,
320+
θ=sample.θ,
321+
y=compute_targets ? solver(ξ; sample.context...) : nothing,
322+
sample.context...,
323+
extra=(; scenario=ξ),
324+
) for ξ in scenarios
325+
]
326+
end
327+
328+
"""
329+
$TYPEDSIGNATURES
330+
331+
Specialised [`generate_dataset`](@ref) for exogenous stochastic benchmarks.
332+
333+
Generates `nb_instances` problem instances, each with `nb_scenarios_per_instance`
334+
independent scenario draws. The scenario→sample mapping is controlled by
335+
[`generate_instance_samples`](@ref): by default K scenarios produce K samples
336+
(1:1, anticipative), but overriding it enables batch strategies such as SAA
337+
(K scenarios → 1 sample with a shared target).
338+
339+
# Keyword arguments
340+
- `nb_scenarios_per_instance::Int = 1` — scenarios per instance (K).
341+
- `compute_targets::Bool = false` — when `true`, passed to
342+
[`generate_instance_samples`](@ref) to trigger target computation.
343+
- `seed` — passed to `MersenneTwister` when `rng` is not provided.
344+
- `rng` — random number generator; overrides `seed` when provided.
345+
- `kwargs...` — forwarded to [`generate_instance_samples`](@ref) (e.g. `algorithm=...`).
346+
"""
347+
function generate_dataset(
348+
bench::AbstractStochasticBenchmark{true},
349+
nb_instances::Int;
350+
nb_scenarios_per_instance::Int=1,
351+
compute_targets::Bool=false,
352+
seed=nothing,
353+
rng=MersenneTwister(seed),
354+
kwargs...,
355+
)
356+
Random.seed!(rng, seed)
357+
samples = DataSample[]
358+
for _ in 1:nb_instances
359+
sample = generate_sample(bench, rng)
360+
scenarios = [
361+
generate_scenario(bench, sample, rng) for _ in 1:nb_scenarios_per_instance
362+
]
363+
append!(
364+
samples,
365+
generate_instance_samples(bench, sample, scenarios; compute_targets, kwargs...),
366+
)
367+
end
368+
return samples
369+
end
370+
190371
"""
191372
$TYPEDEF
192373
@@ -198,6 +379,32 @@ TODO
198379
"""
199380
abstract type AbstractDynamicBenchmark{exogenous} <: AbstractStochasticBenchmark{exogenous} end
200381

382+
# Dynamic benchmarks do not use the stochastic dataset generation (which draws independent
383+
# scenarios per instance). They generate each sample independently via `generate_sample`,
384+
# using the standard AbstractBenchmark default.
385+
function generate_dataset(
386+
bench::AbstractDynamicBenchmark,
387+
dataset_size::Int;
388+
seed=nothing,
389+
rng=MersenneTwister(seed),
390+
kwargs...,
391+
)
392+
Random.seed!(rng, seed)
393+
return [generate_sample(bench, rng; kwargs...) for _ in 1:dataset_size]
394+
end
395+
396+
# Dynamic benchmarks generate complete trajectories via `generate_sample` and do not
397+
# decompose problems into (instance, scenario) pairs. `generate_scenario` is not
398+
# applicable to them; this method exists only to provide a clear error.
399+
function generate_scenario(
400+
bench::AbstractDynamicBenchmark, sample::DataSample, rng::AbstractRNG; kwargs...
401+
)
402+
return error(
403+
"`generate_scenario` is not applicable to dynamic benchmarks ($(typeof(bench))). " *
404+
"Dynamic benchmarks generate complete trajectories via `generate_sample`.",
405+
)
406+
end
407+
201408
"""
202409
generate_environment(::AbstractDynamicBenchmark, instance, rng::AbstractRNG; kwargs...)
203410

0 commit comments

Comments
 (0)