Skip to content

Phase C: merged ORDC cost params + per-device reserve offers (stacked on #207) - #210

Closed
rodrigomha wants to merge 9 commits into
rh/dev_refactor_services_pt2from
rh/dev_refactor_services_pt3
Closed

Phase C: merged ORDC cost params + per-device reserve offers (stacked on #207)#210
rodrigomha wants to merge 9 commits into
rh/dev_refactor_services_pt2from
rh/dev_refactor_services_pt3

Conversation

@rodrigomha

Copy link
Copy Markdown
Contributor

Part 3 of the service-container refactor. Stacked on #207 (Phase A+B) → base branch rh/dev_refactor_services_pt2.

Review only the commits above #207; GitHub will show the full stack until #207 merges.

What this adds (Phase C)

C1 — merged ORDC PWL cost params. Fold the per-service TimeSeriesParameter slope/breakpoint
PWL cost containers into one merged container per service type (covariant Vector{<:ReserveDemandTimeSeriesCurve}
axis computation, batch-wide max-tranche padding). Retires the now-dead single-service
add_parameters!/_add_parameters! path.

C2 — per-device ancillary-service reserve offers. A contributing device can OFFER a PWL
price/quantity curve for a reserve service (PSY set_service_bid! / get_services_bid); it is now
priced by that curve instead of the flat DEFAULT_RESERVE_COST. This is the 4D
(service, device, segment, time) cost structure on top of the 3D (service, device, time) reserve award.

Tests

test_device_reserve_offers.jl: data model + a fixture that builds/solves and asserts the 4D block
var, the award-linking constraint (±1 coefficients), and the offer-slope objective coefficient
(≈ slope · base_p · dt, an independent unit-checked reference).

Full POM suite green (105,495 assertions), 0 method ambiguities.

Cross-package dependency

Depends on Sienna-Platform/InfrastructureOptimizationModels.jl#141 (adds sparse_variable_key_type).
The [sources] IOM pin here already tracks rh/dev_service_refactor.

🤖 Generated with Claude Code

https://claude.ai/code/session_01W1bssU5YnY8eZWQD9apDBM

…ner per type

Migrate the ORDC (StepwiseCostReserve) slope/breakpoint ObjectiveFunctionParameter
containers off per-service meta=name to one merged container per service type (names
axis + tranche axis, empty meta), mirroring the device offer path:
- calc_additional_axes gains a batched Vector{<:ReserveDemandTimeSeriesCurve} form
  sizing the tranche axis to the batch-wide max across the type's services (covariant
  Vector{<:...} sidesteps the invariant-Vector unification issue that forced only()).
- _calc_additional_axes now forwards the batch for services too (drops only()).
- the ORDC PWL _add_parameters!/add_parameters! take a Vector of services, empty meta.
- process_stepwise_cost_reserve_parameters! + _maybe_process_stepwise take the vector;
  the StepwiseCostReserve constructor calls them once, out of the per-service loop.
- the delta-PWL read drops meta=name (_get_pwl_data reads name-keyed, like devices).

The per-service/per-hour tranche padding (unwrap_for_param) now pads to the batch-wide
max; the build&solve ORDC test with two different-tranche ORDCs stays green and asserts
the merged (empty-meta) param container. POM-only, no IOM change.
Phase A moved the TransmissionInterface flow-limit params to the merged vector
path and C1 uses the ObjectiveFunctionParameter path, so the single-service
TimeSeriesParameter add_parameters!/_add_parameters! (service::U, ServiceModel)
have no remaining callers in src. Remove both. Every add_parameters! call site
now passes a vector.
A device can offer a PWL price/quantity curve for providing a reserve, stored via the
PSY set_service_bid! path (a PiecewiseStepData time series named after the service, on a
device with an OfferCurveCost; membership in MarketBidCost.ancillary_service_offers).
This is the (service, device, segment, time) 4D cost structure - one PWL offer per
(device, service) per hour, on top of the already-3D (service, device, time) reserve award.

add_device_reserve_offers! builds such a fixture; the tests pin the data model
(get_services_bid returns a per-device offer curve) and that the fixture builds and solves.
Today these offers are NOT consumed - the reserve is priced flat (DEFAULT_RESERVE_COST) and
no reserve-offer cost term is built; that consumer is the next step.
@rodrigomha
rodrigomha force-pushed the rh/dev_refactor_services_pt2 branch from 2423224 to 9b3b8e2 Compare July 30, 2026 16:45
Consume the per-device reserve OFFER curves (set via PSY `set_service_bid!`,
retrieved with `get_services_bid`) instead of pricing every reserve award at the
flat `DEFAULT_RESERVE_COST`. This is the (service, device, segment, time) 4D cost
structure sitting on top of the 3D `(service, device, time)` reserve award.

- `PiecewiseLinearBlockReserveOffer` (SparseVariableType) + `ReserveOfferLinkingConstraint`.
- `add_reserve_offer_costs!` builds the delta/block-offer formulation per offering
  device: segment vars `δ_k >= 0`, `δ_k <= P_{k+1}-P_k`, `Σ_k δ_k == award`,
  `objective += Σ_k slope_k · δ_k · dt`. Reuses the device-offer PWL machinery
  (`_get_raw_pwl_data`, `get_piecewise_curve_per_system_unit`,
  `get_pwl_cost_expression_delta`). Returns the priced device names.
- `add_to_objective_function!` prices offering devices here, then the flat-cost pass
  skips them via a new `skip_devices` kwarg on `add_reserves_proportional_cost!`.

The 4D block var is created through the axes+`sparse=true` container path so the key
type is `(String,String,Int,Int)`; the SparseVariableType default is hardcoded 3-tuple.

Test asserts the block var, the award-linking constraint (±1 coefficients), and the
offer-slope objective coefficient (segment-2/segment-1 ratio = 1.5).
…atch predicate

Addresses the four review items on the C2 consumer:

1. Proper 4D container. Consume a new IOM trait `sparse_variable_key_type` (default 3D
   `(device, segment, time)`) overridden for `PiecewiseLinearBlockReserveOffer` to the 4D
   `(service, device, segment, time)` key, so the SparseVariableType auto-path builds the
   right container. Replaces the `1:1` axis key-type-carrier workaround.
2. Magnitude assertion. The test now checks the objective coefficient equals
   `slope * base_p * dt` (an independent reference), not just the 1.5x segment ratio -
   this pins the natural->system-base conversion, catching a silent wrong-magnitude cost.
3. Dispatch instead of `isa`. `_has_reserve_offer` dispatches on the operation-cost type
   (`OfferCurveCost` offers; `OperationalCost` fallback returns false).

Also restores the `names` binding that had silently resolved to `Base.names`, which had
widened the linking-constraint key to `Tuple{String, Any, Int}`; now `Tuple{String, String, Int}`.

Reserve-offer PWL block vars remain unexported in results (verified export is structurally
correct - sum(delta)*base_p == award, cheapest segment fills first - but delta exports in pu,
mismatched against the MW-natural award, so it stays off).

Depends on an IOM change adding `sparse_variable_key_type` (rh/dev_service_refactor).
@rodrigomha
rodrigomha force-pushed the rh/dev_refactor_services_pt3 branch from cd265bf to ddbe923 Compare July 30, 2026 16:45
Add a short explanation to the 'Attach a per-device reserve offer' section covering the
two persistent effects of set_service_bid! (attach the named offer time series, register
the service in ancillary_service_offers) plus its validation, and why they let the model
price the reserve award by the per-device curve.
@rodrigomha
rodrigomha marked this pull request as ready for review July 30, 2026 18:36
@rodrigomha

Copy link
Copy Markdown
Contributor Author

Consolidated into #206: the whole stack (Phase A+B from #207 and Phase C here) has been fast-forwarded onto rh/dev_refactor_services, so #206 now contains everything for review. Closing this stacked PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant