Skip to content

Deduplicate the 22 construct_service! methods via a shared pipeline #223

Description

@rodrigomha

Sibling of #172 (construct_device!) and #195 (network-family constructor pairs), for the service side.

Raised by @luke-kiernan reviewing #206:

Maybe have AI do a quick pass on this file for low-hanging fruit for code deduplication.
Pre-existing but these construct_device! calls all look fairly similar...

Deferred out of #206 to keep an already-large refactor reviewable.

Current shape

src/services_models/services_constructor.jl is 943 lines holding 22 construct_service! methods: roughly one per (stage, service type, formulation), split ~500 lines reserve family and ~340 lines transmission interface.

Formulation Argument stage Model stage
RangeReserve 102 (Reserve), 137 (ConstantReserve) 171
StepwiseCostReserve 222 263
GroupReserve 396 413
RampReserve 445 478
NonSpinningReserve 523 549
ConstantMaxInterfaceFlow 600, 625 651, 695, 739
VariableMaxInterfaceFlow 855 794, 839, 901

The repetition is near-exact, not just thematic

RangeReserve (102-133) and RampReserve (445-476) argument stages are identical apart from the formulation passed to add_service_variables!. Both are:

services = _services_with_contributors(model, sys)
isempty(services) && return
add_parameters!(container, RequirementTimeSeriesParameter, services, model)
for service in services
    contributing_devices = get_contributing_devices(model, PSY.get_name(service))
    add_service_variables!(container, ActivePowerReserveVariable, service,
                           contributing_devices, <Formulation>)
    add_to_expression!(container, ActivePowerReserveVariable, service, model, devices_template)
    add_feedforward_arguments!(container, model, service)
end

NonSpinningReserve (523-547) is the same minus add_to_expression!.
ConstantReserve (137) is the same minus add_parameters!.

Model stages repeat just as closely.
RangeReserve (171-213) and RampReserve (478-521) share the container build, the use_slacks line, the per-service loop, add_to_objective_function!, add_feedforward_constraints! and add_constraint_dual!.
The entire difference is that RampReserve inserts one extra add_constraints!(container, RampConstraint, ...) call.

Proposed direction

A shared pipeline for each stage, with the per-formulation variation expressed as small dispatching predicates rather than as whole copied method bodies.

The file already contains the shape to generalize, at line 215:

_maybe_process_stepwise(container, model,
    services::Vector{<:PSY.ReserveDemandTimeSeriesCurve}) =
    process_stepwise_cost_reserve_parameters!(container, model, services)
_maybe_process_stepwise(container, model, services) = nothing

Extending that idiom gives roughly:

  • _maybe_add_requirement_parameters! - no-op for ConstantReserve, which has no requirement time series
  • _maybe_add_reserve_expression! - no-op for NonSpinningReserve
  • _maybe_add_ramp_constraints! - active only for RampReserve
  • _maybe_add_participation_constraints! - no-op for StepwiseCostReserve

Each pair is two lines and dispatches on the formulation or service type, so the specialization stays statically resolved and the generic body is written once per stage.

Open questions for whoever picks this up:

  • Whether the transmission-interface methods fold into the same pipeline or want their own. They share the _services_with_contributors / loop / dual skeleton but differ in nearly everything else, and there are already five ModelConstructStage methods there worth looking at on their own.
  • Whether GroupReserve participates. It is deferred to last in construct_services! and reads its inputs from other services rather than from contributing devices, so it may be genuinely separate.

Constraints

  • Keep the two-stage ArgumentConstructStage / ModelConstructStage split intact; it is load-bearing for expression-before-constraint ordering.
  • Run Test.detect_ambiguities afterwards. Collapsing methods onto shared abstract bounds is exactly how ambiguities appear.
  • No behavior change. test_services_constructor.jl should pass untouched.

Worth coordinating with #216, which will also be editing these same method bodies for type stability.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions