@@ -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"""
175213abstract type AbstractStochasticBenchmark{exogenous} <: AbstractBenchmark end
176214
177215is_exogenous (:: AbstractStochasticBenchmark{exogenous} ) where {exogenous} = exogenous
178216is_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"""
183229function 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"""
188281function 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"""
199380abstract 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