Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/simulation/simulation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,7 @@ function _build!(
end
=#

_fill_event_models!(sim)
_build_decision_models!(sim)
_build_emulation_model!(sim)

Expand Down
41 changes: 40 additions & 1 deletion src/simulation/simulation_events.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,18 @@ function apply_simulation_events!(simulation::Simulation)
# updated every step regardless, or a running outage would freeze the moment the
# condition stopped holding and never recover.
may_start = _condition_holds(simulation, event_model)
event_type = POM.get_event_type(event_model)
for (attribute_id, device_type_maps) in POM.get_attribute_device_map(event_model)
attribute = PSY.get_supplemental_attribute(sys, attribute_id)
attribute isa event_type || error(
"Supplemental attribute $attribute_id of the emulator's system is a \
$(typeof(attribute)), but the $event_type event model was filled with \
it. Event models must be filled from the emulator's system.",
)
apply_event_step!(
simulation,
event_model,
PSY.get_supplemental_attribute(sys, attribute_id),
attribute,
device_type_maps;
may_start = may_start,
)
Expand All @@ -34,6 +41,38 @@ function apply_simulation_events!(simulation::Simulation)
return
end

"""
Fill every event model of the simulation from the emulator's system, before any model is
built. Events take their outages from the emulator's system only; each model then applies
the event to the outaged devices it has. Outage attributes in a decision model's own system
do not affect events; they may serve other formulations, such as security-constrained ones.

Throws `IS.ConflictingInputsError` when the simulation has event models but no emulation
model.
"""
function _fill_event_models!(simulation::Simulation)
events = get_events(get_sequence(simulation))
isempty(events) && return
models = get_models(simulation)
em_model = get_emulation_model(models)
if isnothing(em_model)
throw(
IS.ConflictingInputsError(
"The simulation sequence has event models but the simulation has no \
emulation model. Outage events are drawn and applied by the emulation \
model from its system: add an emulation model or remove the events from \
the SimulationSequence.",
),
)
end
em_sys = get_system(em_model)
for event_model in events
outages = POM.discover_event_attribute_device_map(event_model, em_sys)
copy!(POM.get_attribute_device_map(event_model), outages)
end
return
end

"""
Resolve one input an event condition declared. POM says what it needs; this says where it
comes from.
Expand Down
10 changes: 5 additions & 5 deletions src/simulation/simulation_sequence.jl
Original file line number Diff line number Diff line change
Expand Up @@ -201,11 +201,11 @@ end
"""
Attach every event model to the template of every model in the simulation.

Discovery and validation happen in PowerOperationsModels at build time: attaching an
event model to a template is what makes `build!` find the system's outage attributes,
populate the event model's `attribute_device_map`, and distribute the event to the
matching `DeviceModel`s. This function used to do that work itself, per model, against
a map keyed by model name.
Every model shares the same event model objects. At simulation build,
`_fill_event_models!` fills each event model's `attribute_device_map` from the emulator's
system; each model's build then distributes the event to its matching `DeviceModel`s and
applies it to the outaged devices that model has. Outage attributes in a decision model's
own system do not affect events.
"""
function _attach_events!(
models::SimulationModels,
Expand Down
36 changes: 29 additions & 7 deletions src/simulation/simulation_state.jl
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,11 @@ function _get_state_params(models::SimulationModels, simulation_step::Dates.Mill
field_containers = getfield(container, type)
for key in keys(field_containers)
!should_write_resulting_value(key) && continue
if !haskey(params, key)
if !haskey(params, key) && _is_event_parameter_key(key)
# The emulator reads its event parameters from the decision state, so
# a device type only the emulator applies events to still needs it.
params[key] = (horizon = simulation_step, resolution = model_resolution)
elseif !haskey(params, key)
@warn "New parameter $key found in emulator only; \
not registered by any decision model"
else
Expand All @@ -95,6 +99,9 @@ function _get_state_params(models::SimulationModels, simulation_step::Dates.Mill
return params
end

_is_event_parameter_key(::OptimizationContainerKey) = false
_is_event_parameter_key(::ParameterKey{<:EventParameter}) = true

_state_models(models::SimulationModels) = _state_models(models, get_emulation_model(models))
_state_models(models::SimulationModels, ::Nothing) = get_decision_models(models)
_state_models(models::SimulationModels, em::EmulationModel) =
Expand Down Expand Up @@ -161,6 +168,17 @@ function _get_state_columns(models::SimulationModels)
return columns
end

# Event parameters are exogenous state the emulator reads before any event step writes it.
# Starting them at their no-outage values covers the components no decision model writes,
# such as a device only the emulator has; every other key starts unset.
_initial_state_value(::OptimizationContainerKey) = NaN
_initial_state_value(::ParameterKey{<:EventParameter}) = 0.0
_initial_state_value(::ParameterKey{AvailableStatusParameter}) = 1.0

# The outaged devices of an event are those of the emulator's system, but a variable's state
# holds only the components of the models that write it, which need not include all of them.
_has_component(data::InMemoryDataset, name::String) = name in axes(data.values)[1]

function _initialize_decision_states!(
sim_state::SimulationState,
simulation_initial_time::Dates.DateTime,
Expand All @@ -174,7 +192,7 @@ function _initialize_decision_states!(
states,
key,
InMemoryDataset(
NaN,
_initial_state_value(key),
simulation_initial_time,
key_params.resolution,
Int(simulation_step / key_params.resolution),
Expand Down Expand Up @@ -230,11 +248,9 @@ function _set_system_states!(
system_states = get_system_states(sim_state)
min_res = minimum([v.resolution for v in values(params)])
for key in state_keys
set_dataset!(
system_states,
key,
make_system_state(simulation_initial_time, min_res, columns[key]),
)
dataset = make_system_state(simulation_initial_time, min_res, columns[key])
fill!(dataset.values.data, _initial_state_value(key))
set_dataset!(system_states, key, dataset)
end
return
end
Expand Down Expand Up @@ -541,6 +557,7 @@ function update_decision_state!(
event_occurence_index =
find_aligned_timestamp_index(event_occurrence_data, simulation_time)
for name in column_names
_has_component(state_data, name) || continue
if event_occurrence_data.values[name, event_occurence_index] > 0.0
state_data.values[name, (state_data_index + 1):end] .=
MISSING_INITIAL_CONDITIONS_TIME_COUNT
Expand All @@ -566,6 +583,7 @@ function update_decision_state!(
event_occurence_index =
find_aligned_timestamp_index(event_occurrence_data, simulation_time)
for name in column_names
_has_component(state_data, name) || continue
if event_occurrence_data.values[name, event_occurence_index] > 0.0
for (time_off, ix) in
enumerate((state_data_index + 1):length(state_data.values[name, :]))
Expand Down Expand Up @@ -593,6 +611,7 @@ function update_decision_state!(
event_occurence_index =
find_aligned_timestamp_index(event_occurrence_data, simulation_time)
for name in column_names
_has_component(state_data, name) || continue
if event_occurrence_data.values[name, event_occurence_index] > 0.0
state_data.values[name, (state_data_index + 1):end] .= 0.0
end
Expand Down Expand Up @@ -1073,6 +1092,7 @@ function update_system_state!(
current_status_values = get_last_recorded_value(current_status_data)
set_update_timestamp!(system_dataset, simulation_time)
for name in column_names
_has_component(current_status_data, name) || continue
if event_occurrence_values[name] > 0.0
current_status_data.values[name, 1] = 0.0
end
Expand All @@ -1099,6 +1119,7 @@ function update_system_state!(
current_status_values = get_last_recorded_value(current_status_data)
set_update_timestamp!(system_dataset, simulation_time)
for name in column_names
_has_component(current_status_data, name) || continue
if event_occurrence_values[name] > 0.0
current_status_data.values[name, 1] = MISSING_INITIAL_CONDITIONS_TIME_COUNT
end
Expand All @@ -1125,6 +1146,7 @@ function update_system_state!(
current_status_values = get_last_recorded_value(current_status_data)
set_update_timestamp!(system_dataset, simulation_time)
for name in column_names
_has_component(current_status_data, name) || continue
if event_occurrence_values[name] > 0.0
current_status_data.values[name, 1] = 0.0
end
Expand Down
Loading
Loading