From 692ec7459929774b5fb69799f5c03d3f48e98f3e Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 27 Jul 2021 21:22:49 -0600 Subject: [PATCH 01/19] Beginning new multi-infrastructure refactor. --- Project.toml | 6 +- src/PowerWaterModels.jl | 22 ++-- src/core/base.jl | 254 ++++++++++++++++++++-------------------- src/io/common.jl | 72 ++++++++---- test/runtests.jl | 39 +++--- 5 files changed, 218 insertions(+), 175 deletions(-) diff --git a/Project.toml b/Project.toml index 34c8a78..9f6d85c 100644 --- a/Project.toml +++ b/Project.toml @@ -5,12 +5,14 @@ repo = "https://github.com/lanl-ansi/PowerWaterModels.jl" version = "0.1.0" [deps] +InfrastructureModels = "2030c09a-7f63-5d83-885d-db604e0e9cc0" PowerModelsDistribution = "d7431456-977f-11e9-2de3-97ff7677985e" WaterModels = "7c60b362-08f4-5b14-8680-cd67a3e18348" [compat] -PowerModelsDistribution = "~0.9" -WaterModels = "~0.6" +InfrastructureModels = "~0.6" +PowerModelsDistribution = "~0.11" +WaterModels = "~0.7" julia = "^1" [extras] diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index 73759b7..c7c0742 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -1,13 +1,13 @@ module PowerWaterModels + import InfrastructureModels + import InfrastructureModels: optimize_model!, @im_fields, ismultinetwork, nw_id_default import PowerModelsDistribution import WaterModels # Initialize shortened package names for convenience. const _PMD = PowerModelsDistribution const _WM = WaterModels - - const _IM = _PMD._IM # InfrastructureModels - const _PM = _PMD._PM # PowerModels + const _IM = InfrastructureModels const _MOI = _IM._MOI # MathOptInterface # Borrow dependencies from other packages. @@ -37,15 +37,17 @@ module PowerWaterModels Memento.config!(Memento.getlogger("PowerWaterModels"), level) end - include("io/common.jl") + const _pwm_global_keys = union(_PMD._pmd_global_keys, _WM._wm_global_keys) + + # include("io/common.jl") - include("core/base.jl") - include("core/data.jl") - include("core/constraint.jl") - include("core/objective.jl") + # include("core/base.jl") + # include("core/data.jl") + # include("core/constraint.jl") + # include("core/objective.jl") - include("prob/pwf.jl") - include("prob/opwf.jl") + # include("prob/pwf.jl") + # include("prob/opwf.jl") # This must come last to support automated export. include("core/export.jl") diff --git a/src/core/base.jl b/src/core/base.jl index 8a6d3bd..ed6e07d 100644 --- a/src/core/base.jl +++ b/src/core/base.jl @@ -1,154 +1,160 @@ -""" - instantiate_model( - p_file, w_file, pw_file, p_type, w_type, build_method; pm_ref_extensions, - wm_ref_extensions, wm_ext, kwargs...) - - Instantiates and returns PowerModelsDistribution and WaterModels modeling objects from - power, water, and linking input files `p_file`, `w_file`, and `pw_file`, respectively. - Here, `p_type` and `w_type` are the power and water modeling types, `build_method` is - the build method for the problem specification being considered, `pm_ref_extensions` and - `wm_ref_extensions` are arrays of power and water modeling extensions, and `wm_ext` is a - dictionary of extra arguments for constructing the WaterModels object. -""" -function instantiate_model( - p_file::String, w_file::String, pw_file::String, p_type::Type, w_type::Type, - build_method::Function; pm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ext::Dict{Symbol,Any}=Dict{Symbol,Any}(), kwargs...) - # Read power, water, and linkage data from files. - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - - # Instantiate the PowerWaterModels object. - return instantiate_model( - p_data, w_data, pw_data, p_type, w_type, build_method; - pm_ref_extensions=pm_ref_extensions, wm_ref_extensions=wm_ref_extensions, - wm_ext=wm_ext, kwargs...) -end +"Root of the PowerWaterModels formulation hierarchy." +abstract type AbstractPowerWaterModel{ + T1<:_PMD.AbstractPowerModel, + T2<:_WM.AbstractWaterModel, +} <: _IM.AbstractInfrastructureModel end -""" - instantiate_model( - p_data, w_data, pw_data, p_type, w_type, build_method; pm_ref_extensions, - wm_ref_extensions, wm_ext, kwargs...) - - Instantiates and returns PowerModelsDistribution and WaterModels modeling objects from - power, water, and linking input data `p_data`, `w_data`, and `pw_data`, respectively. - Here, `p_type` and `w_type` are the power and water modeling types, `build_method` is - the build method for the problem specification being considered, `pm_ref_extensions` and - `wm_ref_extensions` are arrays of power and water modeling extensions, and `wm_ext` is a - dictionary of extra arguments for constructing the WaterModels object. -""" -function instantiate_model( - p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}, pw_data::Dict{String,<:Any}, - p_type::Type, w_type::Type, build_method::Function; - pm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ext::Dict{Symbol,Any}=Dict{Symbol,Any}(), kwargs...) - # Ensure network consistency, here. - if !networks_are_consistent(p_data, w_data) - Memento.error(_LOGGER, "Multinetworks are not of the same length.") - end +"A macro for adding the base PowerWaterModels fields to a type definition." +_IM.@def pwm_fields begin + PowerWaterModels.@im_fields +end - # Modify the loads associated with pumps. - p_data = _modify_loads(p_data, w_data, pw_data) - # Instantiate the WaterModels object. - wm = _WM.instantiate_model( - w_data, w_type, m->nothing; ref_extensions=wm_ref_extensions, ext=wm_ext) +function instantiate_model( + data::Dict{String,<:Any}, + model_type::Type, + build_method::Function; + kwargs..., +) + return _IM.instantiate_model( + data, + model_type, + build_method, + ref_add_core!, + _pwm_global_keys; + kwargs..., + ) +end - # Instantiate the PowerModelsDistribution object. - pm = _PMD.instantiate_mc_model( - p_data, p_type, m->nothing; ref_extensions=pm_ref_extensions, jump_model=wm.model) - # Build the corresponding problem. - build_method(pm, wm) +""" + instantiate_model(p_file, w_file, link_file, model_type, build_method; kwargs...) - # Return the two individual *Models objects. - return pm, wm + Instantiates and returns a PowerWaterModels modeling object from power and water input + files `p_file` and `w_file`. Additionally, `link_file` is an input file that links + power and water networks, `model_type` is the power-water modeling type, and + `build_method` is the build method for the problem specification being considered. +""" +function instantiate_model( + p_file::String, + w_file::String, + link_file::String, + model_type::Type, + build_method::Function; + kwargs..., +) + # Read power, water, and linking data from files. + data = parse_files(p_file, w_file, link_file) + + # Instantiate PowerModels and WaterModels modeling objects. + return instantiate_model(data, model_type, build_method; kwargs...) end """ run_model( - p_data, w_data, pw_data, p_type, w_type, optimizer, build_method; - pm_solution_processors, wm_solution_processors, pm_ref_extensions, - wm_ref_extensions, wm_ext, kwargs...) - - Instantiates and solves the joint PowerModelsDistribution and WaterModels modeling - objects from power, water, and linking input data `p_data`, `w_data`, and `pw_data`, - respectively. Here, `p_type` and `w_type` are the power and water modeling types, - `build_method` is the build method for the problem specification being considered, - `pm_solution_processors` and `wm_solution_processors` are arrays of power and water - model solution processors, `pm_ref_extensions` and `wm_ref_extensions` are arrays of - power and water modeling extensions, and `wm_ext` is a dictionary of extra arguments for - constructing the WaterModels modeling object. Returns a dictionary of combined results. + data, model_type, optimizer, build_method; + ref_extensions, solution_processors, kwargs...) + + Instantiates and solves the joint PowerWaterModels model from input data `data`, where + `model_type` is the power-water modeling type, `build_method` is the build method for + the problem specification being considered, `ref_extensions` is an array of power and + water modeling extensions, and `solution_processors` is an array of power and water + modeling solution data postprocessors. Returns a dictionary of model results. """ function run_model( - p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}, pw_data::Dict{String,<:Any}, - p_type::Type, w_type::Type, optimizer, - build_method::Function; pm_solution_processors::Array=[], - wm_solution_processors::Array=[], - pm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ext::Dict{Symbol,Any}=Dict{Symbol,Any}(), kwargs...) - # Build the model and time its construction. - start_time = time() # Start the timer. - - pm, wm = instantiate_model( - p_data, w_data, pw_data, p_type, w_type, build_method; - pm_ref_extensions=pm_ref_extensions, wm_ref_extensions=wm_ref_extensions, - wm_ext=wm_ext, kwargs...) + data::Dict{String,<:Any}, + model_type::Type, + optimizer, + build_method::Function; + ref_extensions = [], + solution_processors = [], + relax_integrality::Bool = false, + kwargs..., +) + start_time = time() + + pwm = instantiate_model( + data, + model_type, + build_method; + ref_extensions = ref_extensions, + ext = get(kwargs, :ext, Dict{Symbol,Any}()), + setting = get(kwargs, :setting, Dict{String,Any}()), + jump_model = get(kwargs, :jump_model, JuMP.Model()), + ) Memento.debug(_LOGGER, "pwm model build time: $(time() - start_time)") - # Solve the model and build the result, timing both processes. - start_time = time() # Start the timer. + start_time = time() - power_result = _IM.optimize_model!( - pm, optimizer=optimizer, solution_processors=pm_solution_processors) + solution_processors = transform_solution_processors(pwm, solution_processors) - water_result = _IM.build_result( - wm, power_result["solve_time"]; solution_processors=wm_solution_processors) + result = _IM.optimize_model!( + pwm, + optimizer = optimizer, + solution_processors = solution_processors, + relax_integrality = relax_integrality, + ) - # Create a combined power-water result object. - result = power_result # Contains most of the result data. - - # FIXME: There could possibly be component name clashes, here. - _IM.update_data!(result["solution"], water_result["solution"]) Memento.debug(_LOGGER, "pwm model solution time: $(time() - start_time)") - # Return the combined result dictionary. return result end +function transform_solution_processors( + pwm::AbstractPowerWaterModel, + solution_processors::Array, +) + pm = _get_powermodel_from_powerwatermodel(pwm) + wm = _get_watermodel_from_powerwatermodel(pwm) + + for (i, solution_processor) in enumerate(solution_processors) + model_type = methods(solution_processor).ms[1].sig.types[2] + + if model_type <: _PMD.AbstractPowerModel + solution_processors[i] = (pwm, sol) -> solution_processor(pm, sol) + elseif model_type <: _WM.AbstractWaterModel + solution_processors[i] = (pwm, sol) -> solution_processor(wm, sol) + end + end + + return solution_processors +end + + """ - run_model( - p_file, w_file, pw_file, p_type, w_type, optimizer, build_method; - pm_solution_processors, wm_solution_processors, pm_ref_extensions, - wm_ref_extensions, wm_ext, kwargs...) - - Instantiates and solves the joint PowerModelsDistribution and WaterModels modeling - objects from power, water, and linking input files `p_file`, `w_file`, and `pw_file`, - respectively. Here, `p_type` and `w_type` are the power and water modeling types, - `build_method` is the build method for the problem specification being considered, - `pm_solution_processors` and `wm_solution_processors` are arrays of power and water - model solution processors, `pm_ref_extensions` and `wm_ref_extensions` are arrays of - power and water modeling extensions, and `wm_ext` is a dictionary of extra arguments for - constructing the WaterModels modeling object. Returns a dictionary of combined results. + run_model(p_file, w_file, link_file, model_type, optimizer, build_method; kwargs...) + + Instantiates and solves a PowerWaterModels modeling object from power and water input + files `p_file` and `w_file`. Additionally, `link_file` is an input file that links + power and water networks, `model_type` is the power-water modeling type, and + `build_method` is the build method for the problem specification being considered. + Returns a dictionary of model results. """ function run_model( - p_file::String, w_file::String, pw_file::String, p_type::Type, w_type::Type, - optimizer::Union{_MOI.AbstractOptimizer, _MOI.OptimizerWithAttributes}, - build_method::Function; pm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ref_extensions::Vector{<:Function}=Vector{Function}([]), - wm_ext::Dict{Symbol,Any}=Dict{Symbol,Any}(), kwargs...) - # Read power, water, and linkage data from files. - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - - # Instantiate and solve the PowerWaterModels modeling object. - return run_model( - p_data, w_data, pw_data, p_type, w_type, optimizer, build_method; - pm_ref_extensions=pm_ref_extensions, wm_ref_extensions=wm_ref_extensions, - wm_ext=wm_ext, kwargs...) + p_file::String, + w_file::String, + link_file::String, + model_type::Type, + optimizer, + build_method::Function; + kwargs..., +) + # Read power, water, and linking data from files. + data = parse_files(p_file, w_file, link_file) + + # Solve the model and return the result dictionary. + return run_model(data, model_type, optimizer, build_method; kwargs...) +end + + +function ref_add_core!(ref::Dict{Symbol,<:Any}) + # Populate the PowerModels portion of the `ref` dictionary. + _PMD._ref_add_core!(ref) + + # Populate the WaterModels portion of the `ref` dictionary. + _WM.ref_add_core!(ref) end diff --git a/src/io/common.jl b/src/io/common.jl index 88ea93c..b12ce1e 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -1,37 +1,61 @@ """ - parse_json(path) + parse_link_file(path) -Parses a JavaScript Object Notation (JSON) file from the file path `path` and returns a -dictionary containing the corresponding parsed data. Primarily used for linkage files. +Parses a linking file from the file path `path`, depending on the file extension, and +returns a PowerWaterModels data structure that links power and water networks (a dictionary). """ -function parse_json(path::String) - return JSON.parsefile(path) +function parse_link_file(path::String) + if endswith(path, ".json") + data = parse_json(path) + else + error("\"$(path)\" is not a valid file type.") + end + + if !haskey(data, "multiinfrastructure") + data["multiinfrastructure"] = true + end + + return data +end + + +function parse_power_file(file_path::String; skip_correct::Bool = true) + # TODO: What should `skip_correct` do, here? + data = _PMD.parse_file(file_path) + return _IM.ismultiinfrastructure(data) ? data : + Dict("multiinfrastructure" => true, "it" => Dict(_PMD.pmd_it_name => data)) +end + + +function parse_water_file(file_path::String; skip_correct::Bool = true) + data = _WM.parse_file(file_path; skip_correct = skip_correct) + return _IM.ismultiinfrastructure(data) ? data : + Dict("multiinfrastructure" => true, "it" => Dict(_WM.wm_it_name => data)) end """ - parse_files(p_file, w_file, pw_file) + parse_files(power_path, water_path, link_path) -Parses power, water, and power-water linkage input files and returns three data dictionaries -for power, water, and power-water linkage data, respectively. +Parses power, water, and linking data from `power_path`, `water_path`, and `link_path`, +respectively, into a single data dictionary. Returns a PowerWaterModels +multi-infrastructure data structure keyed by the infrastructure type `it`. """ -function parse_files(p_file::String, w_file::String, pw_file::String) - # Read power distribution network data. - if split(p_file, ".")[end] == "m" # If reading a MATPOWER file. - p_data = _PM.parse_file(p_file) - _scale_loads!(p_data, inv(3.0)) - _PMD.make_multiconductor!(p_data, real(3)) - else # Otherwise, use the PowerModelsDistribution parser. - p_data = _PMD.parse_file(p_file) - end +function parse_files(power_path::String, water_path::String, link_path::String) + joint_network_data = parse_link_file(link_path) + _IM.update_data!(joint_network_data, parse_power_file(power_path)) + _IM.update_data!(joint_network_data, parse_water_file(water_path)) + + # Store whether or not each network uses per-unit data. + p_per_unit = get(joint_network_data["it"][_PMD.pmd_it_name], "per_unit", false) + w_per_unit = get(joint_network_data["it"][_WM.wm_it_name], "per_unit", false) - # Parse water and power-water linkage data. - w_data = _WM.parse_file(w_file) # Water distribution network data. - pw_data = parse_json(pw_file) # Power-water network linkage data. + # Correct the network data. + correct_network_data!(joint_network_data) - # Create new network data, where network sizes match. - p_data, w_data = make_multinetworks(p_data, w_data) + # Ensure all datasets use the same units for power. + resolve_units!(joint_network_data, p_per_unit, w_per_unit) - # Return three data dictionaries. - return p_data, w_data, pw_data + # Return the network dictionary. + return joint_network_data end diff --git a/test/runtests.jl b/test/runtests.jl index 2a39f68..04cea1b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,8 +1,7 @@ using PowerWaterModels - # Initialize shortened package names for convenience. +# Initialize shortened package names for convenience. const _IM = PowerWaterModels._IM -const _PM = PowerWaterModels._PM const _PMD = PowerWaterModels._PMD const _WM = PowerWaterModels._WM @@ -13,7 +12,6 @@ const Memento = _IM.Memento # Suppress warnings during testing. Memento.setlevel!(Memento.getlogger(_IM), "error") -Memento.setlevel!(Memento.getlogger(_PM), "error") Memento.setlevel!(Memento.getlogger(_PMD), "error") Memento.setlevel!(Memento.getlogger(_WM), "error") PowerWaterModels.logger_config!("error") @@ -27,15 +25,26 @@ Logging.disable_logging(Logging.Info) using Test -# Setup for optimizers. -ipopt = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "acceptable_tol"=>1.0e-8, "print_level"=>0, "sb"=>"yes") -cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel"=>0) +# Setup optimizers. +ipopt = JuMP.optimizer_with_attributes( + Ipopt.Optimizer, + "acceptable_tol" => 1.0e-8, + "print_level" => 0, + "sb" => "yes", +) + +cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) + juniper = JuMP.optimizer_with_attributes( - Juniper.Optimizer, "nl_solver"=>ipopt, "mip_solver"=>cbc, "log_levels"=>[], - "branch_strategy" => :MostInfeasible, "time_limit" => 60.0) + Juniper.Optimizer, + "nl_solver" => ipopt, + "mip_solver" => cbc, + "log_levels" => [], + "branch_strategy" => :MostInfeasible, + "time_limit" => 60.0, +) # Setup common test data paths (from dependencies). -pm_path = joinpath(dirname(pathof(_PM)), "..") pmd_path = joinpath(dirname(pathof(_PMD)), "..") wm_path = joinpath(dirname(pathof(_WM)), "..") @@ -43,16 +52,16 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("PowerWaterModels.jl") - include("base.jl") + # include("base.jl") - include("io.jl") + # include("io.jl") - include("data.jl") + # include("data.jl") - include("objective.jl") + # include("objective.jl") - include("pwf.jl") + # include("pwf.jl") - include("opwf.jl") + # include("opwf.jl") end From dd46faaa3905404e5aea40032f29358f0ef90eab Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 27 Jul 2021 21:46:54 -0600 Subject: [PATCH 02/19] Define new PowerWaterModel types. --- src/PowerWaterModels.jl | 15 ++++++++------- src/core/base.jl | 2 +- src/core/constraint.jl | 8 ++++---- src/core/objective.jl | 2 +- src/core/types.jl | 7 +++++++ src/prob/opwf.jl | 2 +- src/prob/pwf.jl | 2 +- test/base.jl | 11 ++++++++--- test/runtests.jl | 12 ++++++------ 9 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 src/core/types.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index c7c0742..d636e91 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -39,15 +39,16 @@ module PowerWaterModels const _pwm_global_keys = union(_PMD._pmd_global_keys, _WM._wm_global_keys) - # include("io/common.jl") + include("io/common.jl") - # include("core/base.jl") - # include("core/data.jl") - # include("core/constraint.jl") - # include("core/objective.jl") + include("core/base.jl") + include("core/data.jl") + include("core/constraint.jl") + include("core/objective.jl") + include("core/types.jl") - # include("prob/pwf.jl") - # include("prob/opwf.jl") + include("prob/pwf.jl") + include("prob/opwf.jl") # This must come last to support automated export. include("core/export.jl") diff --git a/src/core/base.jl b/src/core/base.jl index ed6e07d..4bd9c4d 100644 --- a/src/core/base.jl +++ b/src/core/base.jl @@ -1,6 +1,6 @@ "Root of the PowerWaterModels formulation hierarchy." abstract type AbstractPowerWaterModel{ - T1<:_PMD.AbstractPowerModel, + T1<:_PMD.AbstractUnbalancedPowerModel, T2<:_WM.AbstractWaterModel, } <: _IM.AbstractInfrastructureModel end diff --git a/src/core/constraint.jl b/src/core/constraint.jl index cd8b26f..70c80a6 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -1,23 +1,23 @@ -function constraint_fixed_load(pm::_PM.AbstractPowerModel, i::Int64; nw::Int=pm.cnw) +function constraint_fixed_load(pm::_PMD.AbstractUnbalancedPowerModel, i::Int64; nw::Int=pm.cnw) JuMP.@constraint(pm.model, _PMD.var(pm, nw, :z_demand, i) == 1.0) end -function constraint_pump_load(pm::_PM.AbstractPowerModel, wm::_WM.AbstractWaterModel, i::Int, a::Int; nw::Int=wm.cnw) +function constraint_pump_load(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel, i::Int, a::Int; nw::Int=wm.cnw) power_load = _get_power_load_expression(pm, i, nw=nw) pump_load = _get_pump_load_expression(pm, wm, a, nw=nw) c = JuMP.@constraint(pm.model, pump_load == power_load) end -function _get_power_load_expression(pm::_PM.AbstractPowerModel, i::Int; nw::Int=pm.cnw) +function _get_power_load_expression(pm::_PMD.AbstractUnbalancedPowerModel, i::Int; nw::Int=pm.cnw) pd = sum(_PM.ref(pm, nw, :load, i)["pd"]) z = _PM.var(pm, nw, :z_demand, i) return JuMP.@expression(pm.model, pd * z) end -function _get_pump_load_expression(pm::_PM.AbstractPowerModel, wm::_WM.AbstractWaterModel, a::Int; nw::Int=pm.cnw) +function _get_pump_load_expression(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel, a::Int; nw::Int=pm.cnw) scaling = 1.0e-6 * inv(_PM.ref(pm, nw, :baseMVA)) # Scaling factor for power. return JuMP.@expression(wm.model, scaling * _WM.var(wm, nw, :P_pump, a)) end \ No newline at end of file diff --git a/src/core/objective.jl b/src/core/objective.jl index 75c663a..41844fa 100644 --- a/src/core/objective.jl +++ b/src/core/objective.jl @@ -1,7 +1,7 @@ """ objective_min_max_generation_fluctuation(pm::AbstractPowerModel) """ -function objective_min_max_generation_fluctuation(pm::_PM.AbstractPowerModel) +function objective_min_max_generation_fluctuation(pm::_PMD.AbstractUnbalancedPowerModel) z = JuMP.@variable(pm.model, lower_bound = 0.0) nw_ids = sort(collect(_PM.nw_ids(pm))) diff --git a/src/core/types.jl b/src/core/types.jl new file mode 100644 index 0000000..a61c659 --- /dev/null +++ b/src/core/types.jl @@ -0,0 +1,7 @@ +mutable struct PowerWaterModel{T1,T2} <: AbstractPowerWaterModel{T1,T2} + @pwm_fields +end + +RelaxedPowerModels = Union{_PMD.LinDist3FlowPowerModel} +RelaxedWaterModels = Union{_WM.LRDWaterModel,_WM.PWLRDWaterModel} +RelaxedPowerWaterModel = PowerWaterModel{<:RelaxedPowerModels,<:RelaxedWaterModels} diff --git a/src/prob/opwf.jl b/src/prob/opwf.jl index 512dd31..9c26f3a 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -7,7 +7,7 @@ end "Construct the optimal power-water flow problem." -function build_opwf(pm::_PM.AbstractPowerModel, wm::_WM.AbstractWaterModel) +function build_opwf(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel) # Power-only related variables and constraints. _PMD.build_mn_mc_mld_simple(pm) diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index 12217a3..c107f9a 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -7,7 +7,7 @@ end "Construct the power-water flow feasibility problem." -function build_pwf(pm::_PM.AbstractPowerModel, wm::_WM.AbstractWaterModel) +function build_pwf(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel) # Power-only related variables and constraints. _PMD.build_mn_mc_mld_simple(pm) diff --git a/test/base.jl b/test/base.jl index aaa84d2..7bae05b 100644 --- a/test/base.jl +++ b/test/base.jl @@ -1,11 +1,16 @@ @testset "src/core/base.jl" begin p_file = "$(pmd_path)/test/data/opendss/case2_diag.dss" w_file = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - pw_file = "../test/data/json/case2-pump.json" - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel + link_file = "../test/data/json/case2-pump.json" @testset "instantiate_model (with file inputs)" begin - pm, wm = instantiate_model(p_file, w_file, pw_file, p_type, w_type, build_pwf) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, PWLRDWaterModel} + pwm = instantiate_model(p_file, w_file, link_file, pwm_type, build_pwf) + @test typeof(pwm.model) == JuMP.Model + end + + @testset "instantiate_model (with file inputs)" begin + pwm = instantiate_model(p_file, w_file, pw_file, p_type, w_type, build_pwf) @test pm.model == wm.model end diff --git a/test/runtests.jl b/test/runtests.jl index 04cea1b..034ef1a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -52,16 +52,16 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("PowerWaterModels.jl") - # include("base.jl") + include("base.jl") - # include("io.jl") + include("io.jl") - # include("data.jl") + include("data.jl") - # include("objective.jl") + include("objective.jl") - # include("pwf.jl") + include("pwf.jl") - # include("opwf.jl") + include("opwf.jl") end From 89e16d07adcf5ea7e1d84310ebcf607cfd2788a6 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Mon, 16 Aug 2021 13:06:18 -0600 Subject: [PATCH 03/19] Update data parsing, correction, and merging functions. --- src/PowerWaterModels.jl | 16 ++++--- src/core/constants.jl | 9 ++++ src/core/data.jl | 46 ++++++++++++++++++++ src/core/types.jl | 6 +-- src/io/common.jl | 5 +-- src/io/json.jl | 9 ++++ test/data/json/case2-pump.json | 25 ++++++----- test/data/json/case3-pump.json | 25 ++++++----- test/data/json/case4-example_1.json | 25 ++++++----- test/data/json/opendss-epanet.json | 25 ++++++----- test/io.jl | 66 +++++++++++++++++++++-------- test/runtests.jl | 12 +++--- 12 files changed, 186 insertions(+), 83 deletions(-) create mode 100644 src/core/constants.jl create mode 100644 src/io/json.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index d636e91..b3bcc40 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -1,6 +1,6 @@ module PowerWaterModels import InfrastructureModels - import InfrastructureModels: optimize_model!, @im_fields, ismultinetwork, nw_id_default + import InfrastructureModels: nw_id_default import PowerModelsDistribution import WaterModels @@ -39,16 +39,18 @@ module PowerWaterModels const _pwm_global_keys = union(_PMD._pmd_global_keys, _WM._wm_global_keys) + include("io/json.jl") include("io/common.jl") - include("core/base.jl") + include("core/constants.jl") include("core/data.jl") - include("core/constraint.jl") - include("core/objective.jl") - include("core/types.jl") + # include("core/base.jl") + # include("core/constraint.jl") + # include("core/objective.jl") + # include("core/types.jl") - include("prob/pwf.jl") - include("prob/opwf.jl") + # include("prob/pwf.jl") + # include("prob/opwf.jl") # This must come last to support automated export. include("core/export.jl") diff --git a/src/core/constants.jl b/src/core/constants.jl new file mode 100644 index 0000000..dc5a517 --- /dev/null +++ b/src/core/constants.jl @@ -0,0 +1,9 @@ +"Enumerated type specifying the status of the component." +@enum STATUS begin + STATUS_UNKNOWN = -1 # The status of the component is unknown (i.e., on or off). + STATUS_INACTIVE = 0 # The status of the component is inactive (i.e., off or removed). + STATUS_ACTIVE = 1 # The status of the component is active (i.e., on or present). +end + +"Ensures that JSON serialization of `STATUS` returns an integer." +JSON.lower(x::STATUS) = Int(x) \ No newline at end of file diff --git a/src/core/data.jl b/src/core/data.jl index b44c232..80153aa 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -1,3 +1,49 @@ +function correct_network_data!(data::Dict{String, Any}) + # Correct and prepare linking data. + assign_pump_loads!(data) + + # Correct and prepare power network data. + _PMD.correct_network_data!(data; make_pu = true) + + # Correct and prepare water network data. + _WM.correct_network_data!(data) + + # Correct linking data again. + assign_pump_loads!(data) +end + + +function assign_pump_loads!(data::Dict{String, Any}) + for pump_load in values(data["it"]["dep"]["pump_load"]) + # Change the indices of the pump to match network subdataset. + pump_name = pump_load["pump"]["source_id"] + pumps = data["it"][_WM.wm_it_name]["pump"] + pump_name = typeof(pump_name) == String ? pump_name : string(pump_name) + pump = pumps[findfirst(x -> pump_name == x["source_id"][2], pumps)] + pump_load["pump"]["index"] = pump["index"] + + # Change the indices of the load to match network subdataset. + load_name = pump_load["load"]["source_id"] + loads = data["it"][_PMD.pmd_it_name]["load"] + load_name = typeof(load_name) == String ? load_name : string(load_name) + load_key = findfirst(x -> lowercase(load_name) == x["source_id"], loads) + pump_load["load"]["index"] = load_key + + # Check if either of the components or the dependency is inactive. + load_is_inactive = loads[load_key]["status"] == _PMD.DISABLED + pump_is_inactive = pump["status"] == _WM.STATUS_INACTIVE + pump_load_is_inactive = pump_load["status"] == STATUS_INACTIVE + + if (load_is_inactive || pump_is_inactive) || pump_load_is_inactive + # If any of the above statuses are inactive, all are inactive. + loads[load_key]["status"] = _PMD.DISABLED + pump["status"] = _WM.STATUS_INACTIVE + pump_load["status"] = STATUS_INACTIVE + end + end +end + + function networks_are_consistent(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}) return _IM.get_num_networks(p_data) == _IM.get_num_networks(w_data) end diff --git a/src/core/types.jl b/src/core/types.jl index a61c659..e2dfd35 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -1,7 +1,3 @@ mutable struct PowerWaterModel{T1,T2} <: AbstractPowerWaterModel{T1,T2} @pwm_fields -end - -RelaxedPowerModels = Union{_PMD.LinDist3FlowPowerModel} -RelaxedWaterModels = Union{_WM.LRDWaterModel,_WM.PWLRDWaterModel} -RelaxedPowerWaterModel = PowerWaterModel{<:RelaxedPowerModels,<:RelaxedWaterModels} +end \ No newline at end of file diff --git a/src/io/common.jl b/src/io/common.jl index b12ce1e..e0010a0 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -20,7 +20,7 @@ end function parse_power_file(file_path::String; skip_correct::Bool = true) - # TODO: What should `skip_correct` do, here? + # TODO: What should `skip_correct` do, here, if anything? data = _PMD.parse_file(file_path) return _IM.ismultiinfrastructure(data) ? data : Dict("multiinfrastructure" => true, "it" => Dict(_PMD.pmd_it_name => data)) @@ -53,9 +53,6 @@ function parse_files(power_path::String, water_path::String, link_path::String) # Correct the network data. correct_network_data!(joint_network_data) - # Ensure all datasets use the same units for power. - resolve_units!(joint_network_data, p_per_unit, w_per_unit) - # Return the network dictionary. return joint_network_data end diff --git a/src/io/json.jl b/src/io/json.jl new file mode 100644 index 0000000..96383e1 --- /dev/null +++ b/src/io/json.jl @@ -0,0 +1,9 @@ +""" + parse_json(path) + +Parses a JavaScript Object Notation (JSON) file from the file path `path` and returns a +PowerWaterModels data structure that links power and water networks (a dictionary of data). +""" +function parse_json(path::String) + return JSON.parsefile(path) +end diff --git a/test/data/json/case2-pump.json b/test/data/json/case2-pump.json index 64c1771..41b6555 100644 --- a/test/data/json/case2-pump.json +++ b/test/data/json/case2-pump.json @@ -1,14 +1,17 @@ { - "power_water_links": [ - { - "load_source_id": "Load.L1", - "pump_source_id": "1" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "1" + }, + "load": { + "source_id": "Load.L1" + }, + "status": 1 + } + } } - ], - "power_metadata": { - "source_type": "opendss" - }, - "water_metadata": { - "source_type": "epanet" } -} +} \ No newline at end of file diff --git a/test/data/json/case3-pump.json b/test/data/json/case3-pump.json index 1e9d802..89df9c7 100644 --- a/test/data/json/case3-pump.json +++ b/test/data/json/case3-pump.json @@ -1,14 +1,17 @@ { - "power_water_links": [ - { - "load_source_id": "3", - "pump_source_id": "1" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "1" + }, + "load": { + "source_id": "Load.L3" + }, + "status": 1 + } + } } - ], - "power_metadata": { - "source_type": "matpower" - }, - "water_metadata": { - "source_type": "epanet" } -} +} \ No newline at end of file diff --git a/test/data/json/case4-example_1.json b/test/data/json/case4-example_1.json index 00374a0..8d81774 100644 --- a/test/data/json/case4-example_1.json +++ b/test/data/json/case4-example_1.json @@ -1,14 +1,17 @@ { - "power_water_links": [ - { - "load_source_id": "4", - "pump_source_id": "9" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "9" + }, + "load": { + "source_id": "4" + }, + "status": 1 + } + } } - ], - "power_metadata": { - "source_type": "matpower" - }, - "water_metadata": { - "source_type": "epanet" } -} +} \ No newline at end of file diff --git a/test/data/json/opendss-epanet.json b/test/data/json/opendss-epanet.json index 0861832..1c1886f 100644 --- a/test/data/json/opendss-epanet.json +++ b/test/data/json/opendss-epanet.json @@ -1,14 +1,17 @@ { - "power_water_links": [ - { - "load_source_id": "L3", - "pump_source_id": "9" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "9" + }, + "load": { + "source_id": "L3" + }, + "status": 1 + } + } } - ], - "power_metadata": { - "source_type": "opendss" - }, - "water_metadata": { - "source_type": "epanet" } -} +} \ No newline at end of file diff --git a/test/io.jl b/test/io.jl index b245e89..a5cae6e 100644 --- a/test/io.jl +++ b/test/io.jl @@ -1,27 +1,59 @@ @testset "src/io/common.jl" begin @testset "parse_json" begin - pw_data = parse_json("../test/data/json/case3-pump.json") - @test pw_data["water_metadata"]["source_type"] == "epanet" - @test pw_data["power_metadata"]["source_type"] == "matpower" + data = parse_json("../test/data/json/case3-pump.json") + pump_loads = data["it"]["dep"]["pump_load"] + + @test pump_loads["1"]["pump"]["source_id"] == "1" + @test pump_loads["1"]["load"]["source_id"] == "Load.L3" + @test pump_loads["1"]["status"] == 1 end - @testset "parse_files (.m, .inp, .json)" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" - w_file = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - @test pw_data["water_metadata"]["source_type"] == "epanet" - @test pw_data["power_metadata"]["source_type"] == "matpower" + @testset "parse_link_file" begin + data = parse_link_file("../test/data/json/case3-pump.json") + pump_loads = data["it"]["dep"]["pump_load"] + + @test haskey(data, "multiinfrastructure") + @test data["multiinfrastructure"] == true + + @test pump_loads["1"]["pump"]["source_id"] == "1" + @test pump_loads["1"]["load"]["source_id"] == "Load.L3" + @test pump_loads["1"]["status"] == 1 + end + + + @testset "parse_link_file (invalid extension)" begin + path = "../examples/data/json/no_file.txt" + @test_throws ErrorException parse_link_file(path) + end + + + @testset "parse_power_file" begin + path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + data = parse_power_file(path) + @test haskey(data, "multiinfrastructure") + @test data["multiinfrastructure"] == true + end + + + @testset "parse_water_file" begin + path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + data = parse_water_file(path) + @test haskey(data, "multiinfrastructure") + @test data["multiinfrastructure"] == true end - @testset "parse_files (.dss, .inp, .json)" begin - p_file = "$(pmd_path)/test/data/opendss/case2_diag.dss" - w_file = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - pw_file = "../test/data/json/case2-pump.json" - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - @test pw_data["water_metadata"]["source_type"] == "epanet" - @test pw_data["power_metadata"]["source_type"] == "opendss" + @testset "parse_files" begin + power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump.json" + data = parse_files(power_path, water_path, link_path) + + @test haskey(data, "multiinfrastructure") + @test data["multiinfrastructure"] == true + @test haskey(data["it"], "dep") + @test haskey(data["it"], _PMD.pmd_it_name) + @test haskey(data["it"], _WM.wm_it_name) end end diff --git a/test/runtests.jl b/test/runtests.jl index 034ef1a..85c0272 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -52,16 +52,16 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("PowerWaterModels.jl") - include("base.jl") - include("io.jl") - include("data.jl") + # include("base.jl") + + # include("data.jl") - include("objective.jl") + # include("objective.jl") - include("pwf.jl") + # include("pwf.jl") - include("opwf.jl") + # include("opwf.jl") end From bb23a9861d366f9c390646b05f89377ece99b751 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Mon, 16 Aug 2021 18:10:58 -0600 Subject: [PATCH 04/19] Begin to refactor pump load and multinetwork functions. --- Project.toml | 2 + src/PowerWaterModels.jl | 2 + src/core/data.jl | 82 +++++++++++++++------- src/io/common.jl | 11 ++- test/data.jl | 122 +++++++++++++++++++-------------- test/data/json/case3-pump.json | 8 ++- test/io.jl | 10 +-- test/runtests.jl | 4 +- 8 files changed, 159 insertions(+), 82 deletions(-) diff --git a/Project.toml b/Project.toml index 9f6d85c..5bc2a3d 100644 --- a/Project.toml +++ b/Project.toml @@ -6,11 +6,13 @@ version = "0.1.0" [deps] InfrastructureModels = "2030c09a-7f63-5d83-885d-db604e0e9cc0" +PowerModels = "c36e90e8-916a-50a6-bd94-075b64ef4655" PowerModelsDistribution = "d7431456-977f-11e9-2de3-97ff7677985e" WaterModels = "7c60b362-08f4-5b14-8680-cd67a3e18348" [compat] InfrastructureModels = "~0.6" +PowerModels = "~0.18" PowerModelsDistribution = "~0.11" WaterModels = "~0.7" julia = "^1" diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index b3bcc40..08f0b35 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -1,10 +1,12 @@ module PowerWaterModels import InfrastructureModels import InfrastructureModels: nw_id_default + import PowerModels import PowerModelsDistribution import WaterModels # Initialize shortened package names for convenience. + const _PM = PowerModels const _PMD = PowerModelsDistribution const _WM = WaterModels const _IM = InfrastructureModels diff --git a/src/core/data.jl b/src/core/data.jl index 80153aa..6f85883 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -13,6 +13,18 @@ function correct_network_data!(data::Dict{String, Any}) end +function _get_load_id_from_name(data::Dict{String,<:Any}, name::String) + pmd_data = _PMD.get_pmd_data(data) + + if "source_type" in keys(pmd_data) && pmd_data["source_type"] == "matpower" + return findfirst(x -> x["source_id"][2] == parse(Int64, name), pmd_data["load"]) + else + return findfirst(x -> x["source_id"] == lowercase(name), pmd_data["load"]) + end +end + + + function assign_pump_loads!(data::Dict{String, Any}) for pump_load in values(data["it"]["dep"]["pump_load"]) # Change the indices of the pump to match network subdataset. @@ -26,7 +38,7 @@ function assign_pump_loads!(data::Dict{String, Any}) load_name = pump_load["load"]["source_id"] loads = data["it"][_PMD.pmd_it_name]["load"] load_name = typeof(load_name) == String ? load_name : string(load_name) - load_key = findfirst(x -> lowercase(load_name) == x["source_id"], loads) + load_key = _get_load_id_from_name(data, load_name) pump_load["load"]["index"] = load_key # Check if either of the components or the dependency is inactive. @@ -45,49 +57,73 @@ end function networks_are_consistent(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}) - return _IM.get_num_networks(p_data) == _IM.get_num_networks(w_data) + return get_num_networks_pmd(p_data) == _IM.get_num_networks(w_data) end -function make_multinetworks(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}) +function get_num_networks_pmd(data::Dict{String,<:Any}) + if _IM.ismultinetwork(data) + return length(data["nw"]) + elseif _IM.has_time_series(data) + if haskey(data["time_series"], "num_steps") + return data["time_series"]["num_steps"] + else + first_key = collect(keys(data["time_series"]))[1] + return length(data["time_series"][first_key]["time"]) + end + else + return 1 + end +end + + +function make_multinetwork(data::Dict{String,<:Any}) + # Parse the PowerModelsDistribution data. + pmd_data = _PMD.get_pmd_data(data) + # If the network comes from OpenDSS data, transform to a mathematical model. - if !(haskey(p_data, "source_type") && p_data["source_type"] == "matpower") - p_data = _PMD.transform_data_model(p_data; build_multinetwork=true) + if !(haskey(pmd_data, "source_type") && pmd_data["source_type"] == "matpower") + pmd_data = _PMD.transform_data_model(pmd_data; multinetwork = true) end - # Check if the networks need to be translated to multinetwork. - translate_p = !_IM.ismultinetwork(p_data) - translate_w = !_IM.ismultinetwork(w_data) + # Get multinetwork properties of the power network. + translate_p = !_IM.ismultinetwork(pmd_data) + num_steps_p = get_num_networks_pmd(pmd_data) - # Get the maximum number of steps represented by each network. - num_steps_p = _IM.get_num_networks(p_data) - num_steps_w = _IM.get_num_networks(w_data) + # Get multinetwork properties of the water network. + wm_data = _WM.get_wm_data(data) + translate_w = !_IM.ismultinetwork(wm_data) + num_steps_w = _IM.get_num_networks(wm_data) # Depending on the number of steps present in each network, adjust the data. if num_steps_p == 1 && num_steps_w == 1 - p_data_tmp = translate_p ? _replicate_power_data(p_data, 1) : p_data - w_data_tmp = translate_w ? _IM.replicate(w_data, 1, _WM._wm_global_keys) : w_data + p_data_tmp = translate_p ? _replicate_power_data(pmd_data, 1) : pmd_data + w_data_tmp = translate_w ? _IM.replicate(wm_data, 1, _WM._wm_global_keys) : wm_data elseif num_steps_p == 1 && num_steps_w > 1 - p_data_tmp = translate_p ? _replicate_power_data(p_data, num_steps_w) : p_data - w_data_tmp = translate_w ? _IM.make_multinetwork(w_data, _WM._wm_global_keys) : w_data + p_data_tmp = translate_p ? _replicate_power_data(pmd_data, num_steps_w) : pmd_data + w_data_tmp = translate_w ? _IM.make_multinetwork(wm_data, _WM._wm_global_keys) : wm_data elseif num_steps_p > 1 && num_steps_w == 1 - p_data_tmp = translate_p ? _make_power_multinetwork(p_data) : p_data - w_data_tmp = translate_w ? _IM.replicate(w_data, num_steps_p, _WM._wm_global_keys) : w_data + p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data + w_data_tmp = translate_w ? _WM.replicate(wm_data, num_steps_p) : wm_data else - p_data_tmp = translate_p ? _make_power_multinetwork(p_data) : p_data - w_data_tmp = translate_w ? _IM.make_multinetwork(w_data, _WM._wm_global_keys) : w_data + p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data + w_data_tmp = translate_w ? _IM.make_multinetwork(wm_data, _WM._wm_global_keys) : wm_data end - # Return the (potentially modified) power and water networks. - return p_data_tmp, w_data_tmp + # Store the (potentially modified) power and water networks. + data["it"][_PMD.pmd_it_name] = p_data_tmp + data["it"][_WM.wm_it_name] = w_data_tmp + + # Return the modified data dictionary. + return data end function _make_power_multinetwork(p_data::Dict{String,<:Any}) if haskey(p_data, "source_type") && p_data["source_type"] == "matpower" - return _IM.make_multinetwork(p_data, _PM._pm_global_keys) + return _IM.make_multinetwork(p_data, _PMD._pmd_global_keys) else - return _PMD.transform_data_model(p_data; build_multinetwork=true) + return _PMD.transform_data_model(p_data; multinetwork = true) end end diff --git a/src/io/common.jl b/src/io/common.jl index e0010a0..b643c0c 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -20,8 +20,15 @@ end function parse_power_file(file_path::String; skip_correct::Bool = true) - # TODO: What should `skip_correct` do, here, if anything? - data = _PMD.parse_file(file_path) + if split(file_path, ".")[end] == "m" # If reading a MATPOWER file. + data = _PM.parse_file(file_path; validate = !skip_correct) + _scale_loads!(data, 1.0 / 3.0) + _PMD.make_multiconductor!(data, real(3)) + else + # TODO: What should `skip_correct` do, here, if anything? + data = _PMD.parse_file(file_path) + end + return _IM.ismultiinfrastructure(data) ? data : Dict("multiinfrastructure" => true, "it" => Dict(_PMD.pmd_it_name => data)) end diff --git a/test/data.jl b/test/data.jl index 31286dd..939f6fa 100644 --- a/test/data.jl +++ b/test/data.jl @@ -1,56 +1,78 @@ @testset "src/core/data.jl" begin - @testset "make_multinetworks" begin - # Snapshot MATPOWER and EPANET networks. - p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test networks_are_consistent(p_new, w_new) - - # Snapshot MATPOWER and multistep EPANET networks. - p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test networks_are_consistent(p_new, w_new) - - # Snapshot OpenDSS and EPANET networks. - p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test networks_are_consistent(p_new, w_new) - - # Snapshot OpenDSS and multistep EPANET networks. - p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test networks_are_consistent(p_new, w_new) - - # Multistep OpenDSS and snapshot EPANET networks. - p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test networks_are_consistent(p_new, w_new) - - # Multistep OpenDSS and EPANET networks. - p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") - w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - p_new, w_new = make_multinetworks(p_data, w_data) - @test !networks_are_consistent(p_new, w_new) # Multinetwork mismatch. - end + @testset "make_multinetwork" begin + # Snapshot MATPOWER and snapshot EPANET networks. + power_path = "$(pmd_path)/test/data/matpower/case3.m" + water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump.json" - @testset "_modify_loads" begin - p_file_mn = "$(pmd_path)/test/data/opendss/case3_balanced.dss" - w_file_mn = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - pw_file_mn = "../test/data/json/case3-pump.json" - p_data, w_data, pw_data = parse_files(p_file_mn, w_file_mn, pw_file_mn) - @test_throws ErrorException PowerWaterModels._modify_loads(p_data, w_data, pw_data) - end + # Parse the data and use `make_multinetwork` + data = parse_files(power_path, water_path, link_path) + mn_data = make_multinetwork(data) + + # Ensure the networks are consistently sized. + pmd_data = mn_data["it"][_PMD.pmd_it_name] + wm_data = mn_data["it"][_WM.wm_it_name] + @test networks_are_consistent(pmd_data, wm_data) + + # # Multistep OpenDSS and snapshot EPANET networks. + # power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + # water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + # link_path = "../test/data/json/case3-pump.json" + + # # Parse the data and use `make_multinetwork` + # data = parse_files(power_path, water_path, link_path) + # mn_data = make_multinetwork(data) + + # # Ensure the networks are consistently sized. + # pmd_data = mn_data["it"][_PMD.pmd_it_name] + # wm_data = mn_data["it"][_WM.wm_it_name] + # @test networks_are_consistent(pmd_data, wm_data) + + # # Snapshot MATPOWER and multistep EPANET networks. + # p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") + # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") + # p_new, w_new = make_multinetworks(p_data, w_data) + # @test networks_are_consistent(p_new, w_new) - @testset "_make_power_multinetwork" begin - p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") - p_data["time_series"] = Dict{String,Any}("num_steps"=>3) - p_data = PowerWaterModels._make_power_multinetwork(p_data) + # # Snapshot OpenDSS and EPANET networks. + # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") + # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") + # p_new, w_new = make_multinetworks(p_data, w_data) + # @test networks_are_consistent(p_new, w_new) - p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - p_data = PowerWaterModels._make_power_multinetwork(p_data) + # # Snapshot OpenDSS and multistep EPANET networks. + # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") + # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") + # p_new, w_new = make_multinetworks(p_data, w_data) + # @test networks_are_consistent(p_new, w_new) + + # # Multistep OpenDSS and snapshot EPANET networks. + # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") + # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") + # p_new, w_new = make_multinetworks(p_data, w_data) + # @test networks_are_consistent(p_new, w_new) + + # # Multistep OpenDSS and EPANET networks. + # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") + # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") + # p_new, w_new = make_multinetworks(p_data, w_data) + # @test !networks_are_consistent(p_new, w_new) # Multinetwork mismatch. end + + # @testset "_modify_loads" begin + # p_file_mn = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + # w_file_mn = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" + # pw_file_mn = "../test/data/json/case3-pump.json" + # p_data, w_data, pw_data = parse_files(p_file_mn, w_file_mn, pw_file_mn) + # @test_throws ErrorException PowerWaterModels._modify_loads(p_data, w_data, pw_data) + # end + + # @testset "_make_power_multinetwork" begin + # p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") + # p_data["time_series"] = Dict{String,Any}("num_steps"=>3) + # p_data = PowerWaterModels._make_power_multinetwork(p_data) + + # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") + # p_data = PowerWaterModels._make_power_multinetwork(p_data) + # end end diff --git a/test/data/json/case3-pump.json b/test/data/json/case3-pump.json index 89df9c7..1f64278 100644 --- a/test/data/json/case3-pump.json +++ b/test/data/json/case3-pump.json @@ -7,11 +7,17 @@ "source_id": "1" }, "load": { - "source_id": "Load.L3" + "source_id": "3" }, "status": 1 } } + }, + "pmd": { + "source_type": "matpower" + }, + "wm": { + "source_type": "epanet" } } } \ No newline at end of file diff --git a/test/io.jl b/test/io.jl index a5cae6e..a646d73 100644 --- a/test/io.jl +++ b/test/io.jl @@ -4,7 +4,7 @@ pump_loads = data["it"]["dep"]["pump_load"] @test pump_loads["1"]["pump"]["source_id"] == "1" - @test pump_loads["1"]["load"]["source_id"] == "Load.L3" + @test pump_loads["1"]["load"]["source_id"] == "3" @test pump_loads["1"]["status"] == 1 end @@ -17,7 +17,7 @@ @test data["multiinfrastructure"] == true @test pump_loads["1"]["pump"]["source_id"] == "1" - @test pump_loads["1"]["load"]["source_id"] == "Load.L3" + @test pump_loads["1"]["load"]["source_id"] == "3" @test pump_loads["1"]["status"] == 1 end @@ -29,8 +29,9 @@ @testset "parse_power_file" begin - path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + path = "$(pmd_path)/test/data/matpower/case3.m" data = parse_power_file(path) + @test haskey(data, "multiinfrastructure") @test data["multiinfrastructure"] == true end @@ -39,13 +40,14 @@ @testset "parse_water_file" begin path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" data = parse_water_file(path) + @test haskey(data, "multiinfrastructure") @test data["multiinfrastructure"] == true end @testset "parse_files" begin - power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + power_path = "$(pmd_path)/test/data/matpower/case3.m" water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" link_path = "../test/data/json/case3-pump.json" data = parse_files(power_path, water_path, link_path) diff --git a/test/runtests.jl b/test/runtests.jl index 85c0272..136a3d8 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,9 +54,9 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("io.jl") - # include("base.jl") + include("data.jl") - # include("data.jl") + # include("base.jl") # include("objective.jl") From 4ee17d2628e41e37296837cb46e8219734552571 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 17 Aug 2021 20:41:53 -0600 Subject: [PATCH 05/19] Update multinetwork matching functions and tests. --- src/core/data.jl | 24 +++++++- test/data.jl | 88 ++++++++++++++---------------- test/data/json/case2-pump.json | 6 ++ test/data/json/case3-pump-dss.json | 23 ++++++++ 4 files changed, 91 insertions(+), 50 deletions(-) create mode 100644 test/data/json/case3-pump-dss.json diff --git a/src/core/data.jl b/src/core/data.jl index 6f85883..5226040 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -100,14 +100,32 @@ function make_multinetwork(data::Dict{String,<:Any}) p_data_tmp = translate_p ? _replicate_power_data(pmd_data, 1) : pmd_data w_data_tmp = translate_w ? _IM.replicate(wm_data, 1, _WM._wm_global_keys) : wm_data elseif num_steps_p == 1 && num_steps_w > 1 - p_data_tmp = translate_p ? _replicate_power_data(pmd_data, num_steps_w) : pmd_data - w_data_tmp = translate_w ? _IM.make_multinetwork(wm_data, _WM._wm_global_keys) : wm_data + w_data_tmp = translate_w ? _WM.make_multinetwork(wm_data) : wm_data + + if translate_p + p_data_tmp = _replicate_power_data(pmd_data, num_steps_w) + else + if num_steps_p == num_steps_w + p_data_tmp = pmd_data + elseif num_steps_p == 1 + # Get water and power network indices. + nw_id_pmd = collect(keys(pmd_data["nw"]))[1] + nw_ids_wm = collect(keys(w_data_tmp["nw"])) + + # Assume the same power properties across all subnetworks. + p_data_tmp = deepcopy(pmd_data) + p_data_tmp["nw"] = Dict(nw => deepcopy( + pmd_data["nw"][nw_id_pmd]) for nw in nw_ids_wm) + else + Memento.error(_LOGGER, "Multinetworks cannot be reconciled.") + end + end elseif num_steps_p > 1 && num_steps_w == 1 p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data w_data_tmp = translate_w ? _WM.replicate(wm_data, num_steps_p) : wm_data else p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data - w_data_tmp = translate_w ? _IM.make_multinetwork(wm_data, _WM._wm_global_keys) : wm_data + w_data_tmp = translate_w ? _WM.make_multinetwork(wm_data) : wm_data end # Store the (potentially modified) power and water networks. diff --git a/test/data.jl b/test/data.jl index 939f6fa..6d1dffa 100644 --- a/test/data.jl +++ b/test/data.jl @@ -1,62 +1,56 @@ +function consistent_multinetworks(power_path::String, water_path::String, link_path::String) + data = parse_files(power_path, water_path, link_path) + mn_data = make_multinetwork(data) + + # Ensure the networks are consistently sized. + pmd_data = mn_data["it"][_PMD.pmd_it_name] + wm_data = mn_data["it"][_WM.wm_it_name] + return networks_are_consistent(pmd_data, wm_data) +end + @testset "src/core/data.jl" begin @testset "make_multinetwork" begin # Snapshot MATPOWER and snapshot EPANET networks. power_path = "$(pmd_path)/test/data/matpower/case3.m" water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" link_path = "../test/data/json/case3-pump.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # Parse the data and use `make_multinetwork` - data = parse_files(power_path, water_path, link_path) - mn_data = make_multinetwork(data) - - # Ensure the networks are consistently sized. - pmd_data = mn_data["it"][_PMD.pmd_it_name] - wm_data = mn_data["it"][_WM.wm_it_name] - @test networks_are_consistent(pmd_data, wm_data) - - # # Multistep OpenDSS and snapshot EPANET networks. - # power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" - # water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - # link_path = "../test/data/json/case3-pump.json" - - # # Parse the data and use `make_multinetwork` - # data = parse_files(power_path, water_path, link_path) - # mn_data = make_multinetwork(data) - - # # Ensure the networks are consistently sized. - # pmd_data = mn_data["it"][_PMD.pmd_it_name] - # wm_data = mn_data["it"][_WM.wm_it_name] - # @test networks_are_consistent(pmd_data, wm_data) + # Multistep OpenDSS and snapshot EPANET networks. + power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump-dss.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # # Snapshot MATPOWER and multistep EPANET networks. - # p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") - # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - # p_new, w_new = make_multinetworks(p_data, w_data) - # @test networks_are_consistent(p_new, w_new) + # Snapshot MATPOWER and multistep EPANET networks. + power_path = "$(pmd_path)/test/data/matpower/case3.m" + water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # # Snapshot OpenDSS and EPANET networks. - # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") - # p_new, w_new = make_multinetworks(p_data, w_data) - # @test networks_are_consistent(p_new, w_new) + # Snapshot OpenDSS and EPANET networks. + power_path = "$(pmd_path)/test/data/opendss/case2_diag.dss" + water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + link_path = "../test/data/json/case2-pump.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # # Snapshot OpenDSS and multistep EPANET networks. - # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - # p_new, w_new = make_multinetworks(p_data, w_data) - # @test networks_are_consistent(p_new, w_new) + # Snapshot OpenDSS and multistep EPANET networks. + power_path = "$(pmd_path)/test/data/opendss/case2_diag.dss" + water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" + link_path = "../test/data/json/case2-pump.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # # Multistep OpenDSS and snapshot EPANET networks. - # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") - # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp") - # p_new, w_new = make_multinetworks(p_data, w_data) - # @test networks_are_consistent(p_new, w_new) + # Multistep OpenDSS and snapshot EPANET networks. + power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + water_path = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump-dss.json" + @test consistent_multinetworks(power_path, water_path, link_path) - # # Multistep OpenDSS and EPANET networks. - # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case3_balanced.dss") - # w_data = _WM.parse_file("$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp") - # p_new, w_new = make_multinetworks(p_data, w_data) - # @test !networks_are_consistent(p_new, w_new) # Multinetwork mismatch. + # Multistep OpenDSS and EPANET networks (mismatch, should fail). + power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump-dss.json" + @test !consistent_multinetworks(power_path, water_path, link_path) end # @testset "_modify_loads" begin diff --git a/test/data/json/case2-pump.json b/test/data/json/case2-pump.json index 41b6555..e261128 100644 --- a/test/data/json/case2-pump.json +++ b/test/data/json/case2-pump.json @@ -12,6 +12,12 @@ "status": 1 } } + }, + "pmd": { + "source_type": "opendss" + }, + "wm": { + "source_type": "epanet" } } } \ No newline at end of file diff --git a/test/data/json/case3-pump-dss.json b/test/data/json/case3-pump-dss.json new file mode 100644 index 0000000..7bbee24 --- /dev/null +++ b/test/data/json/case3-pump-dss.json @@ -0,0 +1,23 @@ +{ + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "1" + }, + "load": { + "source_id": "Load.L3" + }, + "status": 1 + } + } + }, + "pmd": { + "source_type": "opendss" + }, + "wm": { + "source_type": "epanet" + } + } +} \ No newline at end of file From b14b59d9daad2ea8488b4306c760b8c5d3eb409b Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 17 Aug 2021 21:29:28 -0600 Subject: [PATCH 06/19] Correct remaining data test. --- src/core/data.jl | 10 +++++++--- test/data.jl | 28 ++++++++++++++-------------- test/runtests.jl | 1 + 3 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/core/data.jl b/src/core/data.jl index 5226040..7bd21d3 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -139,7 +139,7 @@ end function _make_power_multinetwork(p_data::Dict{String,<:Any}) if haskey(p_data, "source_type") && p_data["source_type"] == "matpower" - return _IM.make_multinetwork(p_data, _PMD._pmd_global_keys) + return _IM.make_multinetwork(p_data, _PMD.pmd_it_name, _PMD._pmd_global_keys) else return _PMD.transform_data_model(p_data; multinetwork = true) end @@ -172,7 +172,11 @@ function _get_load_from_name(name::String, p_data::Dict{String,<:Any}) end -function _modify_loads(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}, pw_data::Dict{String,<:Any}) +function _modify_loads(data::Dict{String,<:Any}) + # Get the separated power and water subdatasets. + p_data = data["it"][_PMD.pmd_it_name] + w_data = data["it"][_WM.wm_it_name] + # Ensure the two networks have the same multinetwork keys. if keys(p_data["nw"]) != keys(w_data["nw"]) Memento.error(_LOGGER, "Multinetworks do not have the same indices.") @@ -180,7 +184,7 @@ function _modify_loads(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}, p for nw in keys(p_data["nw"]) # Loop over all subnetworks. # Where pumps are linked to power network components, change the loads. - for link in pw_data["power_water_links"] + for (k, pump_load) in data["it"]["dep"]["pump_load"] # Estimate maximum pump power in units used by the power network. base_power = 1.0e-6 * inv(p_data["nw"][nw]["baseMVA"]) pump = _get_pump_from_name(link["pump_source_id"], w_data["nw"][nw]) diff --git a/test/data.jl b/test/data.jl index 6d1dffa..bdafa10 100644 --- a/test/data.jl +++ b/test/data.jl @@ -53,20 +53,20 @@ end @test !consistent_multinetworks(power_path, water_path, link_path) end - # @testset "_modify_loads" begin - # p_file_mn = "$(pmd_path)/test/data/opendss/case3_balanced.dss" - # w_file_mn = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - # pw_file_mn = "../test/data/json/case3-pump.json" - # p_data, w_data, pw_data = parse_files(p_file_mn, w_file_mn, pw_file_mn) - # @test_throws ErrorException PowerWaterModels._modify_loads(p_data, w_data, pw_data) - # end + @testset "_modify_loads" begin + power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" + water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" + link_path = "../test/data/json/case3-pump-dss.json" + mn_data = make_multinetwork(parse_files(power_path, water_path, link_path)) + @test_throws ErrorException PowerWaterModels._modify_loads(mn_data) + end - # @testset "_make_power_multinetwork" begin - # p_data = _PM.parse_file("$(pm_path)/test/data/matpower/case3.m") - # p_data["time_series"] = Dict{String,Any}("num_steps"=>3) - # p_data = PowerWaterModels._make_power_multinetwork(p_data) + @testset "_make_power_multinetwork" begin + p_data = _PM.parse_file("$(pmd_path)/test/data/matpower/case3.m") + p_data["time_series"] = Dict{String,Any}("num_steps" => 3) + p_data = PowerWaterModels._make_power_multinetwork(p_data) - # p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") - # p_data = PowerWaterModels._make_power_multinetwork(p_data) - # end + p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") + p_data = PowerWaterModels._make_power_multinetwork(p_data) + end end diff --git a/test/runtests.jl b/test/runtests.jl index 136a3d8..4e2da37 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2,6 +2,7 @@ using PowerWaterModels # Initialize shortened package names for convenience. const _IM = PowerWaterModels._IM +const _PM = PowerWaterModels._PM const _PMD = PowerWaterModels._PMD const _WM = PowerWaterModels._WM From 9077c5ebf57ea64c8538b59f8aaa90ddaa501c29 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 11:01:15 -0600 Subject: [PATCH 07/19] Partial refactor for new multi-infrastructure functionality. --- src/PowerWaterModels.jl | 15 ++++++++------- src/core/base.jl | 8 ++++++-- src/core/constraint.jl | 6 +++--- src/core/helpers.jl | 18 ++++++++++++++++++ src/core/objective.jl | 27 ++++++++++++++------------- src/core/types.jl | 2 +- src/io/common.jl | 4 ++-- src/prob/opwf.jl | 21 ++++++++++++--------- src/prob/pwf.jl | 20 +++++++++++--------- test/base.jl | 29 ++++++++++++++--------------- test/data.jl | 2 ++ test/objective.jl | 18 ++++++++---------- test/opwf.jl | 26 ++++++++++++-------------- test/pwf.jl | 26 ++++++++++++-------------- test/runtests.jl | 8 ++++---- 15 files changed, 127 insertions(+), 103 deletions(-) create mode 100644 src/core/helpers.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index 08f0b35..11f164d 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -1,6 +1,6 @@ module PowerWaterModels import InfrastructureModels - import InfrastructureModels: nw_id_default + import InfrastructureModels: optimize_model!, @im_fields, ismultinetwork, nw_id_default import PowerModels import PowerModelsDistribution import WaterModels @@ -44,15 +44,16 @@ module PowerWaterModels include("io/json.jl") include("io/common.jl") + include("core/base.jl") include("core/constants.jl") include("core/data.jl") - # include("core/base.jl") - # include("core/constraint.jl") - # include("core/objective.jl") - # include("core/types.jl") + include("core/helpers.jl") + include("core/constraint.jl") + include("core/objective.jl") + include("core/types.jl") - # include("prob/pwf.jl") - # include("prob/opwf.jl") + include("prob/pwf.jl") + include("prob/opwf.jl") # This must come last to support automated export. include("core/export.jl") diff --git a/src/core/base.jl b/src/core/base.jl index 4bd9c4d..263f966 100644 --- a/src/core/base.jl +++ b/src/core/base.jl @@ -17,6 +17,10 @@ function instantiate_model( build_method::Function; kwargs..., ) + if !networks_are_consistent(data["it"][_PMD.pmd_it_name], data["it"][_WM.wm_it_name]) + Memento.error(_LOGGER, "Multinetworks are not of the same length.") + end + return _IM.instantiate_model( data, model_type, @@ -152,8 +156,8 @@ end function ref_add_core!(ref::Dict{Symbol,<:Any}) - # Populate the PowerModels portion of the `ref` dictionary. - _PMD._ref_add_core!(ref) + # Populate the PowerModelsDistribution portion of the `ref` dictionary. + _PMD.ref_add_core!(ref) # Populate the WaterModels portion of the `ref` dictionary. _WM.ref_add_core!(ref) diff --git a/src/core/constraint.jl b/src/core/constraint.jl index 70c80a6..4fa3a0a 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -3,9 +3,9 @@ function constraint_fixed_load(pm::_PMD.AbstractUnbalancedPowerModel, i::Int64; end -function constraint_pump_load(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel, i::Int, a::Int; nw::Int=wm.cnw) - power_load = _get_power_load_expression(pm, i, nw=nw) - pump_load = _get_pump_load_expression(pm, wm, a, nw=nw) +function constraint_pump_load(pwm::AbstractPowerWaterModel, i::Int, a::Int; nw::Int=wm.cnw) + power_load = _get_power_load_expression(pwm, i, nw=nw) + pump_load = _get_pump_load_expression(pwm, a, nw=nw) c = JuMP.@constraint(pm.model, pump_load == power_load) end diff --git a/src/core/helpers.jl b/src/core/helpers.jl new file mode 100644 index 0000000..ce0f103 --- /dev/null +++ b/src/core/helpers.jl @@ -0,0 +1,18 @@ +function _get_powermodel_from_powerwatermodel(pwm::AbstractPowerWaterModel) + # Determine the PowerModelsDistribution modeling type. + pmd_type = typeof(pwm).parameters[1] + + # Power-only variables and constraints. + return pmd_type(pwm.model, pwm.data, pwm.setting, pwm.solution, + pwm.ref, pwm.var, pwm.con, pwm.sol, pwm.sol_proc, pwm.ext) +end + + +function _get_watermodel_from_powerwatermodel(pwm::AbstractPowerWaterModel) + # Determine the WaterModels modeling type. + wm_type = typeof(pwm).parameters[2] + + # Water-only variables and constraints. + return wm_type(pwm.model, pwm.data, pwm.setting, pwm.solution, + pwm.ref, pwm.var, pwm.con, pwm.sol, pwm.sol_proc, pwm.ext) +end diff --git a/src/core/objective.jl b/src/core/objective.jl index 41844fa..6d68244 100644 --- a/src/core/objective.jl +++ b/src/core/objective.jl @@ -1,25 +1,26 @@ """ objective_min_max_generation_fluctuation(pm::AbstractPowerModel) """ -function objective_min_max_generation_fluctuation(pm::_PMD.AbstractUnbalancedPowerModel) - z = JuMP.@variable(pm.model, lower_bound = 0.0) - nw_ids = sort(collect(_PM.nw_ids(pm))) +function objective_min_max_generation_fluctuation(pwm::AbstractPowerWaterModel) + pmd = _get_powermodel_from_powerwatermodel(pwm) + z = JuMP.@variable(pmd.model, lower_bound = 0.0) + nw_ids = sort(collect(_PMD.nw_ids(pmd))) for n in 2:length(nw_ids) nw_1, nw_2 = nw_ids[n-1], nw_ids[n] - for (i, gen) in _PMD.ref(pm, nw_2, :gen) - pg_1 = _PM.var(pm, nw_1, :pg, i) - pg_2 = _PM.var(pm, nw_2, :pg, i) + for (i, gen) in _PMD.ref(pmd, nw_2, :gen) + pg_1 = _PMD.var(pmd, nw_1, :pg, i) + pg_2 = _PMD.var(pmd, nw_2, :pg, i) - JuMP.@constraint(pm.model, z >= pg_1[1] - pg_2[1]) - JuMP.@constraint(pm.model, z >= pg_2[1] - pg_1[1]) - JuMP.@constraint(pm.model, z >= pg_1[2] - pg_2[2]) - JuMP.@constraint(pm.model, z >= pg_2[2] - pg_1[2]) - JuMP.@constraint(pm.model, z >= pg_1[3] - pg_2[3]) - JuMP.@constraint(pm.model, z >= pg_2[3] - pg_1[3]) + JuMP.@constraint(pwm.model, z >= pg_1[1] - pg_2[1]) + JuMP.@constraint(pwm.model, z >= pg_2[1] - pg_1[1]) + JuMP.@constraint(pwm.model, z >= pg_1[2] - pg_2[2]) + JuMP.@constraint(pwm.model, z >= pg_2[2] - pg_1[2]) + JuMP.@constraint(pwm.model, z >= pg_1[3] - pg_2[3]) + JuMP.@constraint(pwm.model, z >= pg_2[3] - pg_1[3]) end end - return JuMP.@objective(pm.model, _IM._MOI.MIN_SENSE, z); + return JuMP.@objective(pwm.model, _IM._MOI.MIN_SENSE, z); end \ No newline at end of file diff --git a/src/core/types.jl b/src/core/types.jl index e2dfd35..b17e120 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -1,3 +1,3 @@ mutable struct PowerWaterModel{T1,T2} <: AbstractPowerWaterModel{T1,T2} @pwm_fields -end \ No newline at end of file +end diff --git a/src/io/common.jl b/src/io/common.jl index b643c0c..968ae52 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -26,7 +26,7 @@ function parse_power_file(file_path::String; skip_correct::Bool = true) _PMD.make_multiconductor!(data, real(3)) else # TODO: What should `skip_correct` do, here, if anything? - data = _PMD.parse_file(file_path) + data = _PMD.parse_file(file_path; data_model = _PMD.MATHEMATICAL) end return _IM.ismultiinfrastructure(data) ? data : @@ -61,5 +61,5 @@ function parse_files(power_path::String, water_path::String, link_path::String) correct_network_data!(joint_network_data) # Return the network dictionary. - return joint_network_data + return make_multinetwork(joint_network_data) end diff --git a/src/prob/opwf.jl b/src/prob/opwf.jl index 9c26f3a..f3351d4 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -1,34 +1,37 @@ # Definitions for solving a joint optimal power-water flow problem. "Entry point for running the optimal power-water flow problem." -function run_opwf(p_file, w_file, pw_file, p_type, w_type, optimizer; kwargs...) - return run_model(p_file, w_file, pw_file, p_type, w_type, optimizer, build_opwf; kwargs...) +function run_opwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) + return run_model(p_file, w_file, pw_file, pwm_type, optimizer, build_opwf; kwargs...) end "Construct the optimal power-water flow problem." -function build_opwf(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel) +function build_opwf(pwm::AbstractPowerWaterModel) # Power-only related variables and constraints. - _PMD.build_mn_mc_mld_simple(pm) + pmd = _get_powermodel_from_powerwatermodel(pwm) + _PMD.build_mn_mc_mld_simple(pmd) # Water-only related variables and constraints. + wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_owf(wm) - for (nw, network) in _PMD.nws(pm) + for (nw, network) in _PMD.nws(pmd) # Get all loads defined in the power network. - loads = _PMD.ref(pm, nw, :load) + loads = _PMD.ref(pmd, nw, :load) # Constrain load variables if they are connected to a pump. for (i, load) in filter(x -> "pump_id" in keys(x.second), loads) - constraint_pump_load(pm, wm, i, load["pump_id"]; nw=nw) + # constraint_pump_load(pwm, i, load["pump_id"]; nw=nw) end # Constrain load variables if they are not connected to a pump. for (i, load) in filter(x -> !("pump_id" in keys(x.second)), loads) - constraint_fixed_load(pm, i; nw=nw) + # constraint_fixed_load(pwm, i; nw=nw) end end # Add the objective that minimizes power generation costs. - _PM.objective_min_fuel_cost(pm) + pmd = _get_powermodel_from_powerwatermodel(pwm) + _PMD.objective_mc_min_fuel_cost(pmd) end diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index c107f9a..929f870 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -1,34 +1,36 @@ # Definitions for solving a joint power-water flow feasibility problem. "Entry point for running the power-water flow feasibility problem." -function run_pwf(pfile, wfile, pwfile, ptype, wtype, optimizer; kwargs...) - return run_model(pfile, wfile, pwfile, ptype, wtype, optimizer, build_pwf; kwargs...) +function run_pwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) + return run_model(p_file, w_file, pw_file, pwm_type, optimizer, build_pwf; kwargs...) end "Construct the power-water flow feasibility problem." -function build_pwf(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel) +function build_pwf(pwm::AbstractPowerWaterModel) # Power-only related variables and constraints. - _PMD.build_mn_mc_mld_simple(pm) + pmd = _get_powermodel_from_powerwatermodel(pwm) + _PMD.build_mn_mc_mld_simple(pmd) # Water-only related variables and constraints. + wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_wf(wm) - for (nw, network) in _PMD.nws(pm) + for (nw, network) in _PMD.nws(pmd) # Get all loads defined in the power network. - loads = _PMD.ref(pm, nw, :load) + loads = _PMD.ref(pmd, nw, :load) # Constrain load variables if they are connected to a pump. for (i, load) in filter(x -> "pump_id" in keys(x.second), loads) - constraint_pump_load(pm, wm, i, load["pump_id"]; nw=nw) + # constraint_pump_load(pwm, i, load["pump_id"]; nw=nw) end # Constrain load variables if they are not connected to a pump. for (i, load) in filter(x -> !("pump_id" in keys(x.second)), loads) - constraint_fixed_load(pm, i; nw=nw) + # constraint_fixed_load(pwm, i; nw=nw) end end # Add a feasibility-only objective. - JuMP.@objective(pm.model, _MOI.FEASIBILITY_SENSE, 0.0) + JuMP.@objective(pwm.model, _MOI.FEASIBILITY_SENSE, 0.0) end diff --git a/test/base.jl b/test/base.jl index 7bae05b..ee8aec2 100644 --- a/test/base.jl +++ b/test/base.jl @@ -4,38 +4,37 @@ link_file = "../test/data/json/case2-pump.json" @testset "instantiate_model (with file inputs)" begin - pwm_type = PowerWaterModel{LinDist3FlowPowerModel, PWLRDWaterModel} + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} pwm = instantiate_model(p_file, w_file, link_file, pwm_type, build_pwf) @test typeof(pwm.model) == JuMP.Model end - @testset "instantiate_model (with file inputs)" begin - pwm = instantiate_model(p_file, w_file, pw_file, p_type, w_type, build_pwf) - @test pm.model == wm.model - end - @testset "instantiate_model (with network inputs)" begin - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - pm, wm = instantiate_model(p_data, w_data, pw_data, p_type, w_type, build_pwf) - @test pm.model == wm.model + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + data = parse_files(p_file, w_file, link_file) + pwm = instantiate_model(data, pwm_type, build_pwf) + @test typeof(pwm.model) == JuMP.Model end @testset "instantiate_model (with network inputs; error)" begin p_file_mn = "$(pmd_path)/test/data/opendss/case3_balanced.dss" w_file_mn = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - pw_file_mn = "../test/data/json/case3-pump.json" - p_data, w_data, pw_data = parse_files(p_file_mn, w_file_mn, pw_file_mn) - @test_throws ErrorException instantiate_model(p_data, w_data, pw_data, p_type, w_type, build_pwf) + link_file_mn = "../test/data/json/case3-pump-dss.json" + data = parse_files(p_file_mn, w_file_mn, link_file_mn) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + @test_throws ErrorException instantiate_model(data, pwm_type, build_pwf) end @testset "run_model (with file inputs)" begin - result = run_model(p_file, w_file, pw_file, p_type, w_type, juniper, build_pwf) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + result = run_model(p_file, w_file, link_file, pwm_type, juniper, build_pwf) @test result["termination_status"] == LOCALLY_SOLVED end @testset "run_model (with network inputs)" begin - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - result = run_model(p_data, w_data, pw_data, p_type, w_type, juniper, build_pwf) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + data = parse_files(p_file, w_file, link_file) + result = run_model(data, pwm_type, juniper, build_pwf) @test result["termination_status"] == LOCALLY_SOLVED end end diff --git a/test/data.jl b/test/data.jl index bdafa10..7344840 100644 --- a/test/data.jl +++ b/test/data.jl @@ -65,8 +65,10 @@ end p_data = _PM.parse_file("$(pmd_path)/test/data/matpower/case3.m") p_data["time_series"] = Dict{String,Any}("num_steps" => 3) p_data = PowerWaterModels._make_power_multinetwork(p_data) + @test length(p_data["nw"]) == 3 p_data = _PMD.parse_file("$(pmd_path)/test/data/opendss/case2_diag.dss") p_data = PowerWaterModels._make_power_multinetwork(p_data) + @test length(p_data["nw"]) == 1 end end diff --git a/test/objective.jl b/test/objective.jl index 30e732a..dbc7d93 100644 --- a/test/objective.jl +++ b/test/objective.jl @@ -1,17 +1,15 @@ @testset "src/io/objective.jl" begin @testset "objective_min_max_generation_fluctuation" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" + p_file = "$(pmd_path)/test/data/matpower/case3.m" w_file = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" + link_file = "../test/data/json/case3-pump.json" + data = parse_files(p_file, w_file, link_file) - w_ext = Dict{Symbol,Any}(:pump_breakpoints => 3) - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + pwm = instantiate_model(data, pwm_type, build_opwf) + objective_min_max_generation_fluctuation(pwm) - pm, wm = instantiate_model(p_data, w_data, pw_data, p_type, w_type, build_opwf; w_ext=w_ext) - objective_min_max_generation_fluctuation(pm) - - power_result = _IM.optimize_model!(pm, optimizer=juniper) - @test power_result["termination_status"] == LOCALLY_SOLVED + result = _IM.optimize_model!(pwm, optimizer = juniper) + @test result["termination_status"] == LOCALLY_SOLVED end end \ No newline at end of file diff --git a/test/opwf.jl b/test/opwf.jl index d363985..6d200de 100644 --- a/test/opwf.jl +++ b/test/opwf.jl @@ -1,25 +1,23 @@ @testset "Optimal Power-Water Flow Problems" begin - @testset "3-bus LinDist3FlowPowerModel and PWLRDWaterModel" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" + @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel" begin + p_file = "$(pmd_path)/test/data/matpower/case3.m" w_file = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" + link_file = "../test/data/json/case3-pump.json" - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - w_ext = Dict{Symbol,Any}(:pump_breakpoints=>3) - result = run_opwf(p_file, w_file, pw_file, p_type, w_type, juniper, w_ext=w_ext) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + result = run_opwf(p_file, w_file, link_file, pwm_type, juniper) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 2932.00, rtol=1.0e-2) + @test isapprox(result["objective"], 2932.00, rtol = 1.0e-2) end - @testset "3-bus LinDist3FlowPowerModel and PWLRDWaterModel (Multistep)" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" + @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel (Multistep)" begin + p_file = "$(pmd_path)/test/data/matpower/case3.m" w_file = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" + link_file = "../test/data/json/case3-pump.json" - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - w_ext = Dict{Symbol,Any}(:pump_breakpoints=>3) - result = run_opwf(p_file, w_file, pw_file, p_type, w_type, juniper, w_ext=w_ext) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + result = run_opwf(p_file, w_file, link_file, pwm_type, juniper) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 8794.26, rtol=1.0e-2) + @test isapprox(result["objective"], 8794.26, rtol = 1.0e-2) end end diff --git a/test/pwf.jl b/test/pwf.jl index 6d4adf6..0e06b36 100644 --- a/test/pwf.jl +++ b/test/pwf.jl @@ -1,25 +1,23 @@ @testset "Power-Water Flow Feasibility Problems" begin - @testset "3-bus LinDist3FlowPowerModel and PWLRDWaterModel" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" + @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel" begin + p_file = "$(pmd_path)/test/data/matpower/case3.m" w_file = "$(wm_path)/test/data/epanet/snapshot/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" + link_file = "../test/data/json/case3-pump.json" - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - w_ext = Dict{Symbol,Any}(:pump_breakpoints=>3) - result = run_pwf(p_file, w_file, pw_file, p_type, w_type, juniper; w_ext=w_ext) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + result = run_pwf(p_file, w_file, link_file, pwm_type, juniper) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 0.0, atol=1.0e-6) + @test isapprox(result["objective"], 0.0, atol = 1.0e-6) end - @testset "3-bus LinDist3FlowPowerModel and PWLRDWaterModel (Multistep)" begin - p_file = "$(pm_path)/test/data/matpower/case3.m" + @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel (Multistep)" begin + p_file = "$(pmd_path)/test/data/matpower/case3.m" w_file = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - pw_file = "../test/data/json/case3-pump.json" + link_file = "../test/data/json/case3-pump.json" - p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - w_ext = Dict{Symbol,Any}(:pump_breakpoints=>3) - result = run_pwf(p_file, w_file, pw_file, p_type, w_type, juniper; w_ext=w_ext) + pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} + result = run_pwf(p_file, w_file, link_file, pwm_type, juniper) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 0.0, atol=1.0e-6) + @test isapprox(result["objective"], 0.0, atol = 1.0e-6) end end diff --git a/test/runtests.jl b/test/runtests.jl index 4e2da37..1c76347 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -57,12 +57,12 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("data.jl") - # include("base.jl") + include("base.jl") - # include("objective.jl") + include("objective.jl") - # include("pwf.jl") + include("pwf.jl") - # include("opwf.jl") + include("opwf.jl") end From 95eeac09839d89adb78c86ff6d1008e3d91aa5b9 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 17:18:39 -0600 Subject: [PATCH 08/19] Shuffling data functions to support PowerModelsDistribution data models. --- src/core/data.jl | 73 +++++++++++++++++++++++++++++------------------ src/io/common.jl | 20 +++++++------ test/base.jl | 9 ------ test/data.jl | 10 +------ test/objective.jl | 2 +- test/opwf.jl | 8 +++--- test/pwf.jl | 4 +-- 7 files changed, 66 insertions(+), 60 deletions(-) diff --git a/src/core/data.jl b/src/core/data.jl index 7bd21d3..4da1d1a 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -1,44 +1,58 @@ function correct_network_data!(data::Dict{String, Any}) - # Correct and prepare linking data. - assign_pump_loads!(data) - # Correct and prepare power network data. _PMD.correct_network_data!(data; make_pu = true) # Correct and prepare water network data. _WM.correct_network_data!(data) - - # Correct linking data again. - assign_pump_loads!(data) end -function _get_load_id_from_name(data::Dict{String,<:Any}, name::String) +function _get_load_id_from_name(data::Dict{String,<:Any}, name::String, power_source_type::String; nw::String = _PMD.nw_id_default) pmd_data = _PMD.get_pmd_data(data) - if "source_type" in keys(pmd_data) && pmd_data["source_type"] == "matpower" - return findfirst(x -> x["source_id"][2] == parse(Int64, name), pmd_data["load"]) + if power_source_type == "matpower" + return findfirst(x -> x["source_id"][2] == parse(Int64, name), pmd_data["nw"][nw]["load"]) else - return findfirst(x -> x["source_id"] == lowercase(name), pmd_data["load"]) + return findfirst(x -> x["source_id"] == lowercase(name), pmd_data["nw"][nw]["load"]) end end - function assign_pump_loads!(data::Dict{String, Any}) - for pump_load in values(data["it"]["dep"]["pump_load"]) + # Ensure power and water network multinetworks are consistent. + if !networks_are_consistent(data["it"][_PMD.pmd_it_name], data["it"][_WM.wm_it_name]) + Memento.error(_LOGGER, "Multinetworks cannot be reconciled.") + end + + # Ensure the multinetwork indices are the same across power and water data sets. + nw_ids_pmd = sort(collect(keys(data["it"][_PMD.pmd_it_name]["nw"]))) + nw_ids_wm = sort(collect(keys(data["it"][_WM.wm_it_name]["nw"]))) + @assert nw_ids_pmd == nw_ids_wm + + # Ensure the power source data type has been specified. + @assert haskey(data["it"][_PMD.pmd_it_name], "source_type") + power_source_type = data["it"][_PMD.pmd_it_name]["source_type"] + + for nw in nw_ids_pmd + # Assign the pump loads at every multinetwork index. + _assign_pump_loads!(data, power_source_type, nw) + end +end + +function _assign_pump_loads!(data::Dict{String, Any}, power_source_type::String, nw::String) + for pump_load in values(data["it"]["dep"]["nw"][nw]["pump_load"]) # Change the indices of the pump to match network subdataset. pump_name = pump_load["pump"]["source_id"] - pumps = data["it"][_WM.wm_it_name]["pump"] + pumps = data["it"][_WM.wm_it_name]["nw"][nw]["pump"] pump_name = typeof(pump_name) == String ? pump_name : string(pump_name) pump = pumps[findfirst(x -> pump_name == x["source_id"][2], pumps)] pump_load["pump"]["index"] = pump["index"] # Change the indices of the load to match network subdataset. load_name = pump_load["load"]["source_id"] - loads = data["it"][_PMD.pmd_it_name]["load"] + loads = data["it"][_PMD.pmd_it_name]["nw"][nw]["load"] load_name = typeof(load_name) == String ? load_name : string(load_name) - load_key = _get_load_id_from_name(data, load_name) + load_key = _get_load_id_from_name(data, load_name, power_source_type; nw = nw) pump_load["load"]["index"] = load_key # Check if either of the components or the dependency is inactive. @@ -80,7 +94,8 @@ end function make_multinetwork(data::Dict{String,<:Any}) # Parse the PowerModelsDistribution data. pmd_data = _PMD.get_pmd_data(data) - + pmd_source_type = pmd_data["source_type"] + # If the network comes from OpenDSS data, transform to a mathematical model. if !(haskey(pmd_data, "source_type") && pmd_data["source_type"] == "matpower") pmd_data = _PMD.transform_data_model(pmd_data; multinetwork = true) @@ -99,6 +114,11 @@ function make_multinetwork(data::Dict{String,<:Any}) if num_steps_p == 1 && num_steps_w == 1 p_data_tmp = translate_p ? _replicate_power_data(pmd_data, 1) : pmd_data w_data_tmp = translate_w ? _IM.replicate(wm_data, 1, _WM._wm_global_keys) : wm_data + + # Ensure consistency of the multinetwork keys. + p_nw = collect(keys(p_data_tmp["nw"]))[1] + w_nw = collect(keys(w_data_tmp["nw"]))[1] + p_data_tmp["nw"][w_nw] = pop!(p_data_tmp["nw"], p_nw) elseif num_steps_p == 1 && num_steps_w > 1 w_data_tmp = translate_w ? _WM.make_multinetwork(wm_data) : wm_data @@ -129,9 +149,16 @@ function make_multinetwork(data::Dict{String,<:Any}) end # Store the (potentially modified) power and water networks. + p_data_tmp["source_type"] = pmd_source_type data["it"][_PMD.pmd_it_name] = p_data_tmp data["it"][_WM.wm_it_name] = w_data_tmp + # Replicate the dependency dictionary, if necessary. + if !_IM.ismultinetwork(data["it"]["dep"]) + num_steps = get_num_networks_pmd(p_data_tmp) + data["it"]["dep"] = _IM.replicate(data["it"]["dep"], num_steps, Set{String}()) + end + # Return the modified data dictionary. return data end @@ -146,12 +173,9 @@ function _make_power_multinetwork(p_data::Dict{String,<:Any}) end -function _replicate_power_data(p_data::Dict{String,<:Any}, num_networks::Int64) - if haskey(p_data, "source_type") && p_data["source_type"] == "matpower" - return _IM.replicate(p_data, num_networks, _PM._pm_global_keys) - else - return _IM.replicate(p_data, num_networks, _PMD._pmd_math_global_keys) - end +function _replicate_power_data(data::Dict{String,<:Any}, num_networks::Int64) + pmd_data = _PMD.get_pmd_data(data) + return _IM.replicate(pmd_data, num_networks, _PMD._pmd_math_global_keys) end @@ -177,11 +201,6 @@ function _modify_loads(data::Dict{String,<:Any}) p_data = data["it"][_PMD.pmd_it_name] w_data = data["it"][_WM.wm_it_name] - # Ensure the two networks have the same multinetwork keys. - if keys(p_data["nw"]) != keys(w_data["nw"]) - Memento.error(_LOGGER, "Multinetworks do not have the same indices.") - end - for nw in keys(p_data["nw"]) # Loop over all subnetworks. # Where pumps are linked to power network components, change the loads. for (k, pump_load) in data["it"]["dep"]["pump_load"] diff --git a/src/io/common.jl b/src/io/common.jl index 968ae52..95ade92 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -21,16 +21,16 @@ end function parse_power_file(file_path::String; skip_correct::Bool = true) if split(file_path, ".")[end] == "m" # If reading a MATPOWER file. - data = _PM.parse_file(file_path; validate = !skip_correct) - _scale_loads!(data, 1.0 / 3.0) - _PMD.make_multiconductor!(data, real(3)) + data = _PM.parse_file(file_path)#; validate = !skip_correct) + # _scale_loads!(data, 1.0 / 3.0) + _PMD.make_multiconductor!(data, 3) else # TODO: What should `skip_correct` do, here, if anything? - data = _PMD.parse_file(file_path; data_model = _PMD.MATHEMATICAL) + data = _PMD.parse_file(file_path) end return _IM.ismultiinfrastructure(data) ? data : - Dict("multiinfrastructure" => true, "it" => Dict(_PMD.pmd_it_name => data)) + Dict("multiinfrastructure" => true, "it" => Dict(_PMD.pmd_it_name => data)) end @@ -52,14 +52,18 @@ function parse_files(power_path::String, water_path::String, link_path::String) joint_network_data = parse_link_file(link_path) _IM.update_data!(joint_network_data, parse_power_file(power_path)) _IM.update_data!(joint_network_data, parse_water_file(water_path)) + correct_network_data!(joint_network_data) # Store whether or not each network uses per-unit data. p_per_unit = get(joint_network_data["it"][_PMD.pmd_it_name], "per_unit", false) w_per_unit = get(joint_network_data["it"][_WM.wm_it_name], "per_unit", false) - # Correct the network data. - correct_network_data!(joint_network_data) + # Make the power and water data sets multinetwork. + joint_network_data_mn = make_multinetwork(joint_network_data) + + # Prepare and correct pump load linking data. + assign_pump_loads!(joint_network_data_mn) # Return the network dictionary. - return make_multinetwork(joint_network_data) + return joint_network_data_mn end diff --git a/test/base.jl b/test/base.jl index ee8aec2..d55c602 100644 --- a/test/base.jl +++ b/test/base.jl @@ -16,15 +16,6 @@ @test typeof(pwm.model) == JuMP.Model end - @testset "instantiate_model (with network inputs; error)" begin - p_file_mn = "$(pmd_path)/test/data/opendss/case3_balanced.dss" - w_file_mn = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - link_file_mn = "../test/data/json/case3-pump-dss.json" - data = parse_files(p_file_mn, w_file_mn, link_file_mn) - pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} - @test_throws ErrorException instantiate_model(data, pwm_type, build_pwf) - end - @testset "run_model (with file inputs)" begin pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} result = run_model(p_file, w_file, link_file, pwm_type, juniper, build_pwf) diff --git a/test/data.jl b/test/data.jl index 7344840..f343448 100644 --- a/test/data.jl +++ b/test/data.jl @@ -50,15 +50,7 @@ end power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" link_path = "../test/data/json/case3-pump-dss.json" - @test !consistent_multinetworks(power_path, water_path, link_path) - end - - @testset "_modify_loads" begin - power_path = "$(pmd_path)/test/data/opendss/case3_balanced.dss" - water_path = "$(wm_path)/test/data/epanet/multinetwork/pump-hw-lps.inp" - link_path = "../test/data/json/case3-pump-dss.json" - mn_data = make_multinetwork(parse_files(power_path, water_path, link_path)) - @test_throws ErrorException PowerWaterModels._modify_loads(mn_data) + @test_throws ErrorException parse_files(power_path, water_path, link_path) end @testset "_make_power_multinetwork" begin diff --git a/test/objective.jl b/test/objective.jl index dbc7d93..e8f4cf6 100644 --- a/test/objective.jl +++ b/test/objective.jl @@ -9,7 +9,7 @@ pwm = instantiate_model(data, pwm_type, build_opwf) objective_min_max_generation_fluctuation(pwm) - result = _IM.optimize_model!(pwm, optimizer = juniper) + result = _IM.optimize_model!(pwm, optimizer = ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED end end \ No newline at end of file diff --git a/test/opwf.jl b/test/opwf.jl index 6d200de..cca85c8 100644 --- a/test/opwf.jl +++ b/test/opwf.jl @@ -5,9 +5,9 @@ link_file = "../test/data/json/case3-pump.json" pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} - result = run_opwf(p_file, w_file, link_file, pwm_type, juniper) + result = run_opwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 2932.00, rtol = 1.0e-2) + # @test isapprox(result["objective"], 2932.00, rtol = 1.0e-2) end @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel (Multistep)" begin @@ -16,8 +16,8 @@ link_file = "../test/data/json/case3-pump.json" pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} - result = run_opwf(p_file, w_file, link_file, pwm_type, juniper) + result = run_opwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED - @test isapprox(result["objective"], 8794.26, rtol = 1.0e-2) + # @test isapprox(result["objective"], 8794.26, rtol = 1.0e-2) end end diff --git a/test/pwf.jl b/test/pwf.jl index 0e06b36..ef9b96a 100644 --- a/test/pwf.jl +++ b/test/pwf.jl @@ -5,7 +5,7 @@ link_file = "../test/data/json/case3-pump.json" pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} - result = run_pwf(p_file, w_file, link_file, pwm_type, juniper) + result = run_pwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0, atol = 1.0e-6) end @@ -16,7 +16,7 @@ link_file = "../test/data/json/case3-pump.json" pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} - result = run_pwf(p_file, w_file, link_file, pwm_type, juniper) + result = run_pwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED @test isapprox(result["objective"], 0.0, atol = 1.0e-6) end From 0d9d5e5fb236deabb9321cf7fd90fe2d21b46f05 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 19:33:20 -0600 Subject: [PATCH 09/19] Begin to refactor load linking constraints. --- src/core/constraint.jl | 30 +++++++++++++++++------------- src/core/data.jl | 2 +- src/prob/opwf.jl | 31 ++++++++++++++++++++----------- src/prob/pwf.jl | 30 ++++++++++++++++++++---------- 4 files changed, 58 insertions(+), 35 deletions(-) diff --git a/src/core/constraint.jl b/src/core/constraint.jl index 4fa3a0a..9fffe08 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -1,23 +1,27 @@ -function constraint_fixed_load(pm::_PMD.AbstractUnbalancedPowerModel, i::Int64; nw::Int=pm.cnw) - JuMP.@constraint(pm.model, _PMD.var(pm, nw, :z_demand, i) == 1.0) +function constraint_fixed_load(pwm::AbstractPowerWaterModel, i::Int64; nw::Int = _IM.nw_id_default) + pmd = _get_powermodel_from_powerwatermodel(pwm) + z = _PMD.var(pmd, nw, :z_demand, i) + JuMP.@constraint(pmd.model, z == 1.0) end -function constraint_pump_load(pwm::AbstractPowerWaterModel, i::Int, a::Int; nw::Int=wm.cnw) - power_load = _get_power_load_expression(pwm, i, nw=nw) - pump_load = _get_pump_load_expression(pwm, a, nw=nw) - c = JuMP.@constraint(pm.model, pump_load == power_load) +function constraint_pump_load(pwm::AbstractPowerWaterModel, i::Int, a::Int; nw::Int = _IM.nw_id_default) + power_load = _get_power_load_expression(pwm, i, nw = nw) + pump_load = _get_pump_load_expression(pwm, a, nw = nw) + # TODO: Include a conversion factor using per-unit transformations. + JuMP.@constraint(pwm.model, pump_load == power_load) end -function _get_power_load_expression(pm::_PMD.AbstractUnbalancedPowerModel, i::Int; nw::Int=pm.cnw) - pd = sum(_PM.ref(pm, nw, :load, i)["pd"]) - z = _PM.var(pm, nw, :z_demand, i) - return JuMP.@expression(pm.model, pd * z) +function _get_power_load_expression(pwm::AbstractPowerWaterModel, i::Int; nw::Int = _IM.nw_id_default) + pmd = _get_powermodel_from_powerwatermodel(pwm) + pd = sum(_PMD.ref(pmd, nw, :load, i)["pd"]) + z = _PMD.var(pmd, nw, :z_demand, i) + return JuMP.@expression(pmd.model, pd * z) end -function _get_pump_load_expression(pm::_PMD.AbstractUnbalancedPowerModel, wm::_WM.AbstractWaterModel, a::Int; nw::Int=pm.cnw) - scaling = 1.0e-6 * inv(_PM.ref(pm, nw, :baseMVA)) # Scaling factor for power. - return JuMP.@expression(wm.model, scaling * _WM.var(wm, nw, :P_pump, a)) +function _get_pump_load_expression(pwm::AbstractPowerWaterModel, i::Int; nw::Int = _IM.nw_id_default) + wm = _get_watermodel_from_powerwatermodel(pwm) + return _WM.var(wm, nw, :P_pump, i) end \ No newline at end of file diff --git a/src/core/data.jl b/src/core/data.jl index 4da1d1a..c2e1822 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -53,7 +53,7 @@ function _assign_pump_loads!(data::Dict{String, Any}, power_source_type::String, loads = data["it"][_PMD.pmd_it_name]["nw"][nw]["load"] load_name = typeof(load_name) == String ? load_name : string(load_name) load_key = _get_load_id_from_name(data, load_name, power_source_type; nw = nw) - pump_load["load"]["index"] = load_key + pump_load["load"]["index"] = parse(Int, load_key) # Check if either of the components or the dependency is inactive. load_is_inactive = loads[load_key]["status"] == _PMD.DISABLED diff --git a/src/prob/opwf.jl b/src/prob/opwf.jl index f3351d4..899bd5b 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -1,5 +1,6 @@ # Definitions for solving a joint optimal power-water flow problem. + "Entry point for running the optimal power-water flow problem." function run_opwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) return run_model(p_file, w_file, pw_file, pwm_type, optimizer, build_opwf; kwargs...) @@ -16,22 +17,30 @@ function build_opwf(pwm::AbstractPowerWaterModel) wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_owf(wm) - for (nw, network) in _PMD.nws(pmd) - # Get all loads defined in the power network. - loads = _PMD.ref(pmd, nw, :load) - - # Constrain load variables if they are connected to a pump. - for (i, load) in filter(x -> "pump_id" in keys(x.second), loads) - # constraint_pump_load(pwm, i, load["pump_id"]; nw=nw) + nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) + nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids + + for nw in nw_ids_inner + # Obtain all pump loads at multinetwork index. + pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) + + for (pump_load_id, pump_load) in pump_loads + # Constrain load variables if they are connected to a pump. + pump_index = pump_load["pump"]["index"] + load_index = pump_load["load"]["index"] + constraint_pump_load(pwm, load_index, pump_index; nw = nw) end - # Constrain load variables if they are not connected to a pump. - for (i, load) in filter(x -> !("pump_id" in keys(x.second)), loads) - # constraint_fixed_load(pwm, i; nw=nw) + # Discern the indices for variable loads (i.e., loads connected to pumps). + load_ids = _PMD.ids(pmd, nw, :load) + var_load_ids = [x["load"]["index"] for x in values(pump_loads)] + + for load_index in setdiff(load_ids, var_load_ids) + # Constrain load variables if they are not connected to a pump. + constraint_fixed_load(pwm, load_index; nw = nw) end end # Add the objective that minimizes power generation costs. - pmd = _get_powermodel_from_powerwatermodel(pwm) _PMD.objective_mc_min_fuel_cost(pmd) end diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index 929f870..c83765e 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -1,5 +1,6 @@ # Definitions for solving a joint power-water flow feasibility problem. + "Entry point for running the power-water flow feasibility problem." function run_pwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) return run_model(p_file, w_file, pw_file, pwm_type, optimizer, build_pwf; kwargs...) @@ -16,18 +17,27 @@ function build_pwf(pwm::AbstractPowerWaterModel) wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_wf(wm) - for (nw, network) in _PMD.nws(pmd) - # Get all loads defined in the power network. - loads = _PMD.ref(pmd, nw, :load) - - # Constrain load variables if they are connected to a pump. - for (i, load) in filter(x -> "pump_id" in keys(x.second), loads) - # constraint_pump_load(pwm, i, load["pump_id"]; nw=nw) + nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) + nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids + + for nw in nw_ids_inner + # Obtain all pump loads at multinetwork index. + pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) + + for (pump_load_id, pump_load) in pump_loads + # Constrain load variables if they are connected to a pump. + pump_index = pump_load["pump"]["index"] + load_index = pump_load["load"]["index"] + constraint_pump_load(pwm, load_index, pump_index; nw = nw) end - # Constrain load variables if they are not connected to a pump. - for (i, load) in filter(x -> !("pump_id" in keys(x.second)), loads) - # constraint_fixed_load(pwm, i; nw=nw) + # Discern the indices for variable loads (i.e., loads connected to pumps). + load_ids = _PMD.ids(pmd, nw, :load) + var_load_ids = [x["load"]["index"] for x in values(pump_loads)] + + for load_index in setdiff(load_ids, var_load_ids) + # Constrain load variables if they are not connected to a pump. + constraint_fixed_load(pwm, load_index; nw = nw) end end From 2c39dd0a50fccffa7a10eed151c08b597fc88e56 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 19:41:26 -0600 Subject: [PATCH 10/19] Refactor common linking routines into its own function. --- src/PowerWaterModels.jl | 1 + src/prob/linking.jl | 27 +++++++++++++++++++++++++++ src/prob/opwf.jl | 25 ++----------------------- src/prob/pwf.jl | 25 ++----------------------- 4 files changed, 32 insertions(+), 46 deletions(-) create mode 100644 src/prob/linking.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index 11f164d..b5c8e02 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -52,6 +52,7 @@ module PowerWaterModels include("core/objective.jl") include("core/types.jl") + include("prob/linking.jl") include("prob/pwf.jl") include("prob/opwf.jl") diff --git a/src/prob/linking.jl b/src/prob/linking.jl new file mode 100644 index 0000000..79a85cc --- /dev/null +++ b/src/prob/linking.jl @@ -0,0 +1,27 @@ +function build_linking(pwm::AbstractPowerWaterModel) + # Get important data that will be used in the modeling loop. + pmd = _get_powermodel_from_powerwatermodel(pwm) + nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) + nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids + + for nw in nw_ids_inner + # Obtain all pump loads at multinetwork index. + pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) + + for pump_load in values(pump_loads) + # Constrain load variables if they are connected to a pump. + pump_index = pump_load["pump"]["index"] + load_index = pump_load["load"]["index"] + constraint_pump_load(pwm, load_index, pump_index; nw = nw) + end + + # Discern the indices for variable loads (i.e., loads connected to pumps). + load_ids = _PMD.ids(pmd, nw, :load) + var_load_ids = [x["load"]["index"] for x in values(pump_loads)] + + for load_index in setdiff(load_ids, var_load_ids) + # Constrain load variables if they are not connected to a pump. + constraint_fixed_load(pwm, load_index; nw = nw) + end + end +end \ No newline at end of file diff --git a/src/prob/opwf.jl b/src/prob/opwf.jl index 899bd5b..38d0b00 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -17,29 +17,8 @@ function build_opwf(pwm::AbstractPowerWaterModel) wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_owf(wm) - nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) - nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids - - for nw in nw_ids_inner - # Obtain all pump loads at multinetwork index. - pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) - - for (pump_load_id, pump_load) in pump_loads - # Constrain load variables if they are connected to a pump. - pump_index = pump_load["pump"]["index"] - load_index = pump_load["load"]["index"] - constraint_pump_load(pwm, load_index, pump_index; nw = nw) - end - - # Discern the indices for variable loads (i.e., loads connected to pumps). - load_ids = _PMD.ids(pmd, nw, :load) - var_load_ids = [x["load"]["index"] for x in values(pump_loads)] - - for load_index in setdiff(load_ids, var_load_ids) - # Constrain load variables if they are not connected to a pump. - constraint_fixed_load(pwm, load_index; nw = nw) - end - end + # Power-water linking constraints. + build_linking(pwm) # Add the objective that minimizes power generation costs. _PMD.objective_mc_min_fuel_cost(pmd) diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index c83765e..33a5e69 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -17,29 +17,8 @@ function build_pwf(pwm::AbstractPowerWaterModel) wm = _get_watermodel_from_powerwatermodel(pwm) _WM.build_mn_wf(wm) - nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) - nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids - - for nw in nw_ids_inner - # Obtain all pump loads at multinetwork index. - pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) - - for (pump_load_id, pump_load) in pump_loads - # Constrain load variables if they are connected to a pump. - pump_index = pump_load["pump"]["index"] - load_index = pump_load["load"]["index"] - constraint_pump_load(pwm, load_index, pump_index; nw = nw) - end - - # Discern the indices for variable loads (i.e., loads connected to pumps). - load_ids = _PMD.ids(pmd, nw, :load) - var_load_ids = [x["load"]["index"] for x in values(pump_loads)] - - for load_index in setdiff(load_ids, var_load_ids) - # Constrain load variables if they are not connected to a pump. - constraint_fixed_load(pwm, load_index; nw = nw) - end - end + # Power-water linking constraints. + build_linking(pwm) # Add a feasibility-only objective. JuMP.@objective(pwm.model, _MOI.FEASIBILITY_SENSE, 0.0) From 873f00ad41bc7fb37ccebdc5509c50d17120c034 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 20:28:40 -0600 Subject: [PATCH 11/19] Incorporate (unvalidated) scaling of pump power in linking constraints. --- src/PowerWaterModels.jl | 1 + src/core/constraint.jl | 4 ++-- src/core/ref.jl | 24 ++++++++++++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/core/ref.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index b5c8e02..66406e5 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -45,6 +45,7 @@ module PowerWaterModels include("io/common.jl") include("core/base.jl") + include("core/ref.jl") include("core/constants.jl") include("core/data.jl") include("core/helpers.jl") diff --git a/src/core/constraint.jl b/src/core/constraint.jl index 9fffe08..95f9d1a 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -8,8 +8,8 @@ end function constraint_pump_load(pwm::AbstractPowerWaterModel, i::Int, a::Int; nw::Int = _IM.nw_id_default) power_load = _get_power_load_expression(pwm, i, nw = nw) pump_load = _get_pump_load_expression(pwm, a, nw = nw) - # TODO: Include a conversion factor using per-unit transformations. - JuMP.@constraint(pwm.model, pump_load == power_load) + factor = _get_power_conversion_factor(pwm; nw = nw) + JuMP.@constraint(pwm.model, factor * pump_load == power_load) end diff --git a/src/core/ref.jl b/src/core/ref.jl new file mode 100644 index 0000000..4241356 --- /dev/null +++ b/src/core/ref.jl @@ -0,0 +1,24 @@ +function _get_power_conversion_factor(pwm::AbstractPowerWaterModel; nw::Int = _IM.nw_id_default)::Float64 + # Get the conversion factor for power used by the power network. + data_pmd = _PMD.get_pmd_data(pwm.data) + + if haskey(data_pmd["nw"][string(nw)], "baseMVA") + base_mva_pmd = data_pmd["nw"][string(nw)]["baseMVA"] + else + sbase = data_pmd["nw"][string(nw)]["settings"]["sbase"] + psf = data_pmd["nw"][string(nw)]["settings"]["power_scale_factor"] + base_mva_pmd = sbase / psf + end + + base_power_pmd = 1.0e-6 / base_mva_pmd # Watts per PM unit. + + # Get the conversion factor for power used by the water network. + data_wm = _WM.get_wm_data(pwm.data) + transform_mass = _WM._calc_mass_per_unit_transform(data_wm) + transform_time = _WM._calc_time_per_unit_transform(data_wm) + transform_length = _WM._calc_length_per_unit_transform(data_wm) + base_power_wm = transform_mass(1.0) * transform_length(1.0)^2 / transform_time(1.0)^3 + + # Return the power conversion factor for pumps. + return base_power_wm / base_power_pmd +end \ No newline at end of file From 6363e66040cc19baff0456502c9932e731b6d79d Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 21:14:43 -0600 Subject: [PATCH 12/19] Fix power conversion calculation. --- src/core/ref.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/ref.jl b/src/core/ref.jl index 4241356..a1202f0 100644 --- a/src/core/ref.jl +++ b/src/core/ref.jl @@ -9,16 +9,19 @@ function _get_power_conversion_factor(pwm::AbstractPowerWaterModel; nw::Int = _I psf = data_pmd["nw"][string(nw)]["settings"]["power_scale_factor"] base_mva_pmd = sbase / psf end - - base_power_pmd = 1.0e-6 / base_mva_pmd # Watts per PM unit. + + # Watts per PowerModelsDistribution power unit. + base_power_pmd = 1.0e6 * base_mva_pmd # Get the conversion factor for power used by the water network. data_wm = _WM.get_wm_data(pwm.data) transform_mass = _WM._calc_mass_per_unit_transform(data_wm) transform_time = _WM._calc_time_per_unit_transform(data_wm) transform_length = _WM._calc_length_per_unit_transform(data_wm) - base_power_wm = transform_mass(1.0) * transform_length(1.0)^2 / transform_time(1.0)^3 + + # Scalar for WaterModels power units per Watt. + scalar_power_wm = transform_mass(1.0) * transform_length(1.0)^2 / transform_time(1.0)^3 # Return the power conversion factor for pumps. - return base_power_wm / base_power_pmd + return (1.0 / scalar_power_wm) / base_power_pmd end \ No newline at end of file From 2e5396819d21edbe0afabb8d4a3bf2d392445e23 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 24 Aug 2021 21:23:44 -0600 Subject: [PATCH 13/19] Scale loads when translating MATPOWER data. --- src/io/common.jl | 2 +- test/opwf.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/io/common.jl b/src/io/common.jl index 95ade92..3528a65 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -22,7 +22,7 @@ end function parse_power_file(file_path::String; skip_correct::Bool = true) if split(file_path, ".")[end] == "m" # If reading a MATPOWER file. data = _PM.parse_file(file_path)#; validate = !skip_correct) - # _scale_loads!(data, 1.0 / 3.0) + _scale_loads!(data, 1.0 / 3.0) _PMD.make_multiconductor!(data, 3) else # TODO: What should `skip_correct` do, here, if anything? diff --git a/test/opwf.jl b/test/opwf.jl index cca85c8..0f48c0b 100644 --- a/test/opwf.jl +++ b/test/opwf.jl @@ -7,7 +7,7 @@ pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} result = run_opwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED - # @test isapprox(result["objective"], 2932.00, rtol = 1.0e-2) + @test isapprox(result["objective"], 2932.00, rtol = 1.0e-2) end @testset "3-bus LinDist3FlowPowerModel and CRDWaterModel (Multistep)" begin @@ -18,6 +18,6 @@ pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} result = run_opwf(p_file, w_file, link_file, pwm_type, ipopt; relax_integrality = true) @test result["termination_status"] == LOCALLY_SOLVED - # @test isapprox(result["objective"], 8794.26, rtol = 1.0e-2) + @test isapprox(result["objective"], 8794.26, rtol = 1.0e-2) end end From 83b51eecfa556c45804b1119ddf9dfbe4f81a31a Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Wed, 25 Aug 2021 09:31:06 -0600 Subject: [PATCH 14/19] Scale load according to maximum pump power. Consolidate function that computes the power conversion factor. --- src/PowerWaterModels.jl | 1 - src/core/constraint.jl | 2 +- src/core/data.jl | 91 +++++++++++++++++++++++++---------------- src/core/ref.jl | 27 ------------ src/io/common.jl | 10 +++-- 5 files changed, 63 insertions(+), 68 deletions(-) delete mode 100644 src/core/ref.jl diff --git a/src/PowerWaterModels.jl b/src/PowerWaterModels.jl index 66406e5..b5c8e02 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -45,7 +45,6 @@ module PowerWaterModels include("io/common.jl") include("core/base.jl") - include("core/ref.jl") include("core/constants.jl") include("core/data.jl") include("core/helpers.jl") diff --git a/src/core/constraint.jl b/src/core/constraint.jl index 95f9d1a..b8e2d28 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -8,7 +8,7 @@ end function constraint_pump_load(pwm::AbstractPowerWaterModel, i::Int, a::Int; nw::Int = _IM.nw_id_default) power_load = _get_power_load_expression(pwm, i, nw = nw) pump_load = _get_pump_load_expression(pwm, a, nw = nw) - factor = _get_power_conversion_factor(pwm; nw = nw) + factor = _get_power_conversion_factor(pwm.data, string(nw)) JuMP.@constraint(pwm.model, factor * pump_load == power_load) end diff --git a/src/core/data.jl b/src/core/data.jl index c2e1822..dc09f4f 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -179,56 +179,77 @@ function _replicate_power_data(data::Dict{String,<:Any}, num_networks::Int64) end -function _get_pump_from_name(name::String, w_data::Dict{String,<:Any}) - pump_id = findfirst(x -> x["source_id"][2] == name, w_data["pump"]) - return w_data["pump"][pump_id] -end - - -function _get_load_from_name(name::String, p_data::Dict{String,<:Any}) - if "source_type" in keys(p_data) && p_data["source_type"] == "matpower" - load_id = findfirst(x -> x["source_id"][2] == parse(Int64, name), p_data["load"]) - else - load_id = findfirst(x -> x["source_id"] == lowercase(name), p_data["load"]) - end - - return p_data["load"][load_id] -end - - -function _modify_loads(data::Dict{String,<:Any}) +function _modify_loads!(data::Dict{String,<:Any}) # Get the separated power and water subdatasets. p_data = data["it"][_PMD.pmd_it_name] w_data = data["it"][_WM.wm_it_name] - for nw in keys(p_data["nw"]) # Loop over all subnetworks. + # Ensure the multinetwork indices are the same across power and water data sets. + nw_ids_pmd = sort([parse(Int, x) for x in collect(keys(p_data["nw"]))]) + nw_ids_wm = sort([parse(Int, x) for x in collect(keys(w_data["nw"]))]) + nw_ids_inner = length(nw_ids_wm) > 1 ? nw_ids_wm[1:end-1] : nw_ids_wm + @assert nw_ids_pmd == nw_ids_wm + + # Get important scaling data. + base_mass = get(w_data, "base_mass", 1.0) + base_length = get(w_data, "base_length", 1.0) + base_time = get(w_data, "base_time", 1.0) + + rho_s = _WM._calc_scaled_density(base_mass, base_length) + g_s = _WM._calc_scaled_gravity(base_length, base_time) + + for nw in nw_ids_inner # Loop over all subnetworks. # Where pumps are linked to power network components, change the loads. - for (k, pump_load) in data["it"]["dep"]["pump_load"] - # Estimate maximum pump power in units used by the power network. - base_power = 1.0e-6 * inv(p_data["nw"][nw]["baseMVA"]) - pump = _get_pump_from_name(link["pump_source_id"], w_data["nw"][nw]) - node_fr = w_data["nw"][nw]["node"][string(pump["node_fr"])] - node_to = w_data["nw"][nw]["node"][string(pump["node_to"])] - max_pump_power = base_power * _WM._calc_pump_power_max(pump, node_fr, node_to) + factor = _get_power_conversion_factor(data, string(nw)) + + for pump_load in values(data["it"]["dep"]["nw"][string(nw)]["pump_load"]) + # Obtain maximum pump power in units used by the water network. + pump = w_data["nw"][string(nw)]["pump"][string(pump_load["pump"]["index"])] + node_fr = w_data["nw"][string(nw)]["node"][string(pump["node_fr"])] + node_to = w_data["nw"][string(nw)]["node"][string(pump["node_to"])] + P_max = _WM._calc_pump_power_max(pump, node_fr, node_to, rho_s, g_s) # Change the loads associated with pumps. - load = _get_load_from_name(link["load_source_id"], p_data["nw"][nw]) - load_power = inv(sum(x -> x > 0.0, load["pd"])) * max_pump_power + load = p_data["nw"][string(nw)]["load"][string(pump_load["load"]["index"])] + load_power = inv(sum(x -> x > 0.0, load["pd"])) * factor * P_max load["pd"][load["pd"] .> 0.0] .= load_power load["qd"][load["qd"] .> 0.0] .= 0.0 # Assume no reactive load. - - # Add an index variable for the pump within the load object. - load["pump_id"] = pump["index"] end end - - # Return the modified power network data. - return p_data end function _scale_loads!(p_data::Dict{String,<:Any}, scalar::Float64) - for (i, load) in p_data["load"] + for load in values(p_data["load"]) load["pd"] *= scalar end end + + +function _get_power_conversion_factor(data::Dict{String,<:Any}, nw::String)::Float64 + # Get the conversion factor for power used by the power network. + data_pmd = _PMD.get_pmd_data(data) + + if haskey(data_pmd["nw"][string(nw)], "baseMVA") + base_mva_pmd = data_pmd["nw"][string(nw)]["baseMVA"] + else + sbase = data_pmd["nw"][string(nw)]["settings"]["sbase"] + psf = data_pmd["nw"][string(nw)]["settings"]["power_scale_factor"] + base_mva_pmd = sbase / psf + end + + # Watts per PowerModelsDistribution power unit. + base_power_pmd = 1.0e6 * base_mva_pmd + + # Get the conversion factor for power used by the water network. + data_wm = _WM.get_wm_data(data) + transform_mass = _WM._calc_mass_per_unit_transform(data_wm) + transform_time = _WM._calc_time_per_unit_transform(data_wm) + transform_length = _WM._calc_length_per_unit_transform(data_wm) + + # Scalar for WaterModels power units per Watt. + scalar_power_wm = transform_mass(1.0) * transform_length(1.0)^2 / transform_time(1.0)^3 + + # Return the power conversion factor for pumps. + return (1.0 / scalar_power_wm) / base_power_pmd +end \ No newline at end of file diff --git a/src/core/ref.jl b/src/core/ref.jl deleted file mode 100644 index a1202f0..0000000 --- a/src/core/ref.jl +++ /dev/null @@ -1,27 +0,0 @@ -function _get_power_conversion_factor(pwm::AbstractPowerWaterModel; nw::Int = _IM.nw_id_default)::Float64 - # Get the conversion factor for power used by the power network. - data_pmd = _PMD.get_pmd_data(pwm.data) - - if haskey(data_pmd["nw"][string(nw)], "baseMVA") - base_mva_pmd = data_pmd["nw"][string(nw)]["baseMVA"] - else - sbase = data_pmd["nw"][string(nw)]["settings"]["sbase"] - psf = data_pmd["nw"][string(nw)]["settings"]["power_scale_factor"] - base_mva_pmd = sbase / psf - end - - # Watts per PowerModelsDistribution power unit. - base_power_pmd = 1.0e6 * base_mva_pmd - - # Get the conversion factor for power used by the water network. - data_wm = _WM.get_wm_data(pwm.data) - transform_mass = _WM._calc_mass_per_unit_transform(data_wm) - transform_time = _WM._calc_time_per_unit_transform(data_wm) - transform_length = _WM._calc_length_per_unit_transform(data_wm) - - # Scalar for WaterModels power units per Watt. - scalar_power_wm = transform_mass(1.0) * transform_length(1.0)^2 / transform_time(1.0)^3 - - # Return the power conversion factor for pumps. - return (1.0 / scalar_power_wm) / base_power_pmd -end \ No newline at end of file diff --git a/src/io/common.jl b/src/io/common.jl index 3528a65..0458589 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -19,13 +19,12 @@ function parse_link_file(path::String) end -function parse_power_file(file_path::String; skip_correct::Bool = true) +function parse_power_file(file_path::String) if split(file_path, ".")[end] == "m" # If reading a MATPOWER file. - data = _PM.parse_file(file_path)#; validate = !skip_correct) + data = _PM.parse_file(file_path) _scale_loads!(data, 1.0 / 3.0) _PMD.make_multiconductor!(data, 3) else - # TODO: What should `skip_correct` do, here, if anything? data = _PMD.parse_file(file_path) end @@ -59,11 +58,14 @@ function parse_files(power_path::String, water_path::String, link_path::String) w_per_unit = get(joint_network_data["it"][_WM.wm_it_name], "per_unit", false) # Make the power and water data sets multinetwork. - joint_network_data_mn = make_multinetwork(joint_network_data) + joint_network_data_mn = make_multinetwork(joint_network_data) # Prepare and correct pump load linking data. assign_pump_loads!(joint_network_data_mn) + # Modify variable load properties in the power network. + _modify_loads!(joint_network_data_mn) + # Return the network dictionary. return joint_network_data_mn end From b45dc7d9054a69abd698384df48f5f4c721abeff Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Wed, 25 Aug 2021 12:58:04 -0600 Subject: [PATCH 15/19] Update example problem and documentation. --- CHANGELOG.md | 4 +++ docs/src/index.md | 21 ++++++------ docs/src/quickguide.md | 59 ++++++++++++++-------------------- examples/data/json/zamzam.json | 51 ++++++++++++++++++++--------- src/prob/opwf.jl | 6 ++++ src/prob/pwf.jl | 6 ++++ 6 files changed, 85 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d183fd9..381e4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ PowerWaterModels.jl Change Log ======================= +### v0.2.0 +- Updates for WaterModels v0.8. +- Updates for PowerModelsDistribution v0.11. +- Updates for InfrastructureModels v0.6. ### v0.1.0 - Updates for WaterModels v0.6.0. diff --git a/docs/src/index.md b/docs/src/index.md index dafb0a8..7eaf0ab 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -5,7 +5,7 @@ CurrentModule = PowerWaterModels ``` ## Overview -PowerWaterModels.jl is a Julia/JuMP package for the joint optimization of steady state power and water distribution networks. +PowerWaterModels.jl is a Julia/JuMP package for the joint optimization of steady-state power and water distribution networks. It is designed to enable the computational evaluation of historical and emerging power-water network optimization formulations and algorithms using a common platform. The code is engineered to decouple [Problem Specifications](@ref) (e.g., power-water flow, optimal power-water flow) from [Network Formulations](@ref) (e.g., mixed-integer linear, mixed-integer nonlinear). This decoupling enables the definition of a variety of optimization formulations and their comparison on common problem specifications. @@ -44,11 +44,11 @@ using JuMP, Juniper, Ipopt, Cbc using PowerWaterModels # Set up the optimization solvers. -ipopt = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "print_level"=>0, "sb"=>"yes") -cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel"=>0) +ipopt = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0, "sb" => "yes") +cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) juniper = JuMP.optimizer_with_attributes( - Juniper.Optimizer, "nl_solver"=>ipopt, "mip_solver"=>cbc, - "branch_strategy" => :MostInfeasible, "time_limit" => 60.0) + Juniper.Optimizer, "nl_solver" => ipopt, "mip_solver" => cbc, + "time_limit" => 60.0) # Specify paths to the power, water, and power-water linking files. p_file = "examples/data/opendss/IEEE13_CDPSM.dss" # Power network. @@ -56,13 +56,10 @@ w_file = "examples/data/epanet/cohen-short.inp" # Water network. pw_file = "examples/data/json/zamzam.json" # Power-water linking. # Specify the power and water formulation types separately. -p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel - -# Specify the number of breakpoints used in the linearized water formulation. -wm_ext = Dict{Symbol,Any}(:pipe_breakpoints=>2, :pump_breakpoints=>3) +pwm_type = PowerWaterModel{LinDist3FlowPowerModel, CRDWaterModel} # Solve the joint optimal power-water flow problem and store the result. -result = run_opwf(p_file, w_file, pw_file, p_type, w_type, juniper; wm_ext=wm_ext) +result = run_opwf(p_file, w_file, pw_file, pwm_type, juniper) ``` After solving the problem, results can then be analyzed, e.g., @@ -72,8 +69,8 @@ After solving the problem, results can then be analyzed, e.g., result["objective"] # Generator 1's real power generation at the first time step. -result["solution"]["nw"]["1"]["gen"]["1"]["pg"] +result["solution"]["it"]["pmd"]["nw"]["1"]["gen"]["1"]["pg"] # Pump 2's head gain at the third time step. -result["solution"]["nw"]["3"]["pump"]["2"]["g"] +result["solution"]["it"]["wm"]["nw"]["3"]["pump"]["2"]["g"] ``` diff --git a/docs/src/quickguide.md b/docs/src/quickguide.md index 40923f2..b9d1674 100644 --- a/docs/src/quickguide.md +++ b/docs/src/quickguide.md @@ -44,12 +44,13 @@ After installation of the required solvers, an example optimal power-water flow ```julia using JuMP, Juniper, Ipopt, Cbc using PowerWaterModels +const WM = PowerWaterModels.WaterModels # Set up the optimization solvers. -ipopt = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "print_level"=>0, "sb"=>"yes") -cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel"=>0) +ipopt = JuMP.optimizer_with_attributes(Ipopt.Optimizer, "print_level" => 0, "sb" => "yes") +cbc = JuMP.optimizer_with_attributes(Cbc.Optimizer, "logLevel" => 0) juniper = JuMP.optimizer_with_attributes( - Juniper.Optimizer, "nl_solver"=>ipopt, "mip_solver"=>cbc, + Juniper.Optimizer, "nl_solver" => ipopt, "mip_solver" => cbc, "branch_strategy" => :MostInfeasible, "time_limit" => 60.0) # Specify paths to the power, water, and power-water linking files. @@ -57,14 +58,21 @@ p_file = "examples/data/opendss/IEEE13_CDPSM.dss" # Power network. w_file = "examples/data/epanet/cohen-short.inp" # Water network. pw_file = "examples/data/json/zamzam.json" # Power-water linking. -# Specify the power and water formulation types separately. -p_type, w_type = LinDist3FlowPowerModel, PWLRDWaterModel +# Parse the input files as a multi-infrastructure data object. +data = parse_files(p_file, w_file, pw_file) + +# Perform OBBT on water network to improve variable bounds. +WM.solve_obbt_owf!(data, ipopt; use_relaxed_network = false, + model_type = WM.CRDWaterModel, max_iter = 3) -# Specify the number of breakpoints used in the linearized water formulation. -wm_ext = Dict{Symbol,Any}(:pipe_breakpoints=>2, :pump_breakpoints=>3) +# Use WaterModels to set the partitioning of flows in the water network. +WM.set_flow_partitions_num!(data, 5) + +# Specify the power and water formulation types separately. +pwm_type = PowerWaterModel{LinDist3FlowPowerModel, PWLRDWaterModel} # Solve the joint optimal power-water flow problem and store the result. -result = run_opwf(p_file, w_file, pw_file, p_type, w_type, juniper; wm_ext=wm_ext) +result = run_opwf(data, pwm_type, juniper) ``` ### (Optional) Solving the Problem with Gurobi @@ -75,8 +83,8 @@ The problem considered above can then be solved using Gurobi (instead of Juniper import Gurobi # Solve the joint optimal power-water flow problem and store its result. -gurobi = JuMP.optimizer_with_attributes(Gurobi.Optimizer, "NonConvex"=>2) -result_grb = run_opwf(p_file, w_file, pw_file, p_type, w_type, gurobi; wm_ext=wm_ext) +gurobi = JuMP.optimizer_with_attributes(Gurobi.Optimizer, "NonConvex" => 2) +result_grb = run_opwf(data, pwm_type, gurobi) ``` First, note that Gurobi solves the problem much more quickly than Juniper. @@ -91,7 +99,7 @@ The objective value obtained via Gurobi is _smaller_ than the one obtained via J The `run` commands in PowerWaterModels return detailed results data in the form of a Julia `Dict`. This dictionary can be saved for further processing as follows: ```julia -result = run_opwf(p_file, w_file, pw_file, p_type, w_type, juniper; wm_ext=wm_ext) +result = run_opwf(data, pwm_type, juniper) ``` For example, the algorithm's runtime and final objective value can be accessed with @@ -103,7 +111,7 @@ result["objective"] # Final objective value (in units of the objective). The `"solution"` field contains detailed information about the solution produced by the `run` method. For example, the following can be used to inspect the temporal variation in the volume of tank 1 in the water distribution network: ``` -tank_1_volume = Dict(nw=>data["tank"]["10"]["V"] for (nw, data) in result["solution"]["nw"]) +tank_1_volume = Dict(nw=>data["tank"]["10"]["V"] for (nw, data) in result["solution"]["it"]["wm"]["nw"]) ``` For more information about PowerWaterModels result data, see the [PowerWaterModels Result Data Format](@ref) section. @@ -111,30 +119,13 @@ For more information about PowerWaterModels result data, see the [PowerWaterMode ## Modifying Network Data The following example demonstrates one way to perform PowerWaterModels solves while modifying network data. ```julia -p_data, w_data, pw_data = parse_files(p_file, w_file, pw_file) - -for (nw, network) in w_data["nw"] - network["demand"]["3"]["flow_nominal"] *= 0.1 - network["demand"]["4"]["flow_nominal"] *= 0.1 - network["demand"]["5"]["flow_nominal"] *= 0.1 +for (nw, network) in data["it"]["wm"]["nw"] + network["demand"]["3"]["flow_nominal"] *= 0.90 + network["demand"]["4"]["flow_nominal"] *= 0.90 + network["demand"]["5"]["flow_nominal"] *= 0.90 end -result_mod = run_opwf(p_data, w_data, pw_data, p_type, w_type, juniper; wm_ext=wm_ext) +result_mod = run_opwf(data, pwm_type, juniper) ``` Note that the smaller demands in the modified problem result in an overall smaller objective value. For additional details about the network data, see the [PowerWaterModels Network Data Format](@ref) section. - -## Alternate Methods for Building and Solving Models -The following example demonstrates how to break a `run_opwf` call into separate model building and solving steps. -This allows inspection of the JuMP model created by PowerWaterModels for the problem. -```julia -# Instantiate the joint power-water models. -pm, wm = instantiate_model(p_data, w_data, pw_data, p_type, w_type, build_opwf; wm_ext=wm_ext) - -# Print the (shared) JuMP model. -print(pm.model) - -# Create separate power and water result dictionaries. -power_result = PowerWaterModels._IM.optimize_model!(pm, optimizer=juniper) -water_result = PowerWaterModels._IM.build_result(wm, power_result["solve_time"]) -``` diff --git a/examples/data/json/zamzam.json b/examples/data/json/zamzam.json index 08e6db8..0795f47 100644 --- a/examples/data/json/zamzam.json +++ b/examples/data/json/zamzam.json @@ -1,22 +1,41 @@ { - "power_water_links": [ - { - "load_source_id": "Load.634a", - "pump_source_id": "1" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "1" + }, + "load": { + "source_id": "Load.634a" + }, + "status": 1 + }, + "2": { + "pump": { + "source_id": "2" + }, + "load": { + "source_id": "Load.645" + }, + "status": 1 + }, + "3": { + "pump": { + "source_id": "5" + }, + "load": { + "source_id": "Load.611" + }, + "status": 1 + } + } }, - { - "load_source_id": "Load.645", - "pump_source_id": "2" + "pmd": { + "source_type": "opendss" }, - { - "load_source_id": "Load.611", - "pump_source_id": "5" + "wm": { + "source_type": "epanet" } - ], - "power_metadata": { - "source_type": "opendss" - }, - "water_metadata": { - "source_type": "epanet" } } diff --git a/src/prob/opwf.jl b/src/prob/opwf.jl index 38d0b00..9a22a66 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -7,6 +7,12 @@ function run_opwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) end +"Entry point for running the optimal power-water flow problem." +function run_opwf(data, pwm_type, optimizer; kwargs...) + return run_model(data, pwm_type, optimizer, build_opwf; kwargs...) +end + + "Construct the optimal power-water flow problem." function build_opwf(pwm::AbstractPowerWaterModel) # Power-only related variables and constraints. diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index 33a5e69..5a85c92 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -7,6 +7,12 @@ function run_pwf(p_file, w_file, pw_file, pwm_type, optimizer; kwargs...) end +"Entry point for running the power-water flow feasibility problem." +function run_pwf(data, pwm_type, optimizer; kwargs...) + return run_model(data, pwm_type, optimizer, build_pwf; kwargs...) +end + + "Construct the power-water flow feasibility problem." function build_pwf(pwm::AbstractPowerWaterModel) # Power-only related variables and constraints. From b2f75c792ef1f3abd752925ad91ba1935ab8508a Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Wed, 25 Aug 2021 17:11:13 -0600 Subject: [PATCH 16/19] Fix multinetwork lengths for power versus water. --- src/core/data.jl | 81 +++++++++++++++++++++++++++------------------ src/prob/linking.jl | 4 +-- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/core/data.jl b/src/core/data.jl index dc09f4f..8c03587 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -1,4 +1,4 @@ -function correct_network_data!(data::Dict{String, Any}) +function correct_network_data!(data::Dict{String,Any}) # Correct and prepare power network data. _PMD.correct_network_data!(data; make_pu = true) @@ -7,39 +7,51 @@ function correct_network_data!(data::Dict{String, Any}) end -function _get_load_id_from_name(data::Dict{String,<:Any}, name::String, power_source_type::String; nw::String = _PMD.nw_id_default) +function _get_load_id_from_name( + data::Dict{String,<:Any}, + name::String, + power_source_type::String; + nw::String = _PMD.nw_id_default, +) pmd_data = _PMD.get_pmd_data(data) if power_source_type == "matpower" - return findfirst(x -> x["source_id"][2] == parse(Int64, name), pmd_data["nw"][nw]["load"]) + return findfirst( + x -> x["source_id"][2] == parse(Int64, name), + pmd_data["nw"][nw]["load"], + ) else return findfirst(x -> x["source_id"] == lowercase(name), pmd_data["nw"][nw]["load"]) end end -function assign_pump_loads!(data::Dict{String, Any}) +function assign_pump_loads!(data::Dict{String,Any}) # Ensure power and water network multinetworks are consistent. if !networks_are_consistent(data["it"][_PMD.pmd_it_name], data["it"][_WM.wm_it_name]) Memento.error(_LOGGER, "Multinetworks cannot be reconciled.") end - # Ensure the multinetwork indices are the same across power and water data sets. + # Ensure the multinetwork lengths are compatible across power and water data sets. nw_ids_pmd = sort(collect(keys(data["it"][_PMD.pmd_it_name]["nw"]))) nw_ids_wm = sort(collect(keys(data["it"][_WM.wm_it_name]["nw"]))) - @assert nw_ids_pmd == nw_ids_wm - + + if length(nw_ids_pmd) != 1 && length(nw_ids_wm) != 1 + # The water model should include one more time index than power. + @assert length(nw_ids_pmd) + 1 == length(nw_ids_wm) + end + # Ensure the power source data type has been specified. @assert haskey(data["it"][_PMD.pmd_it_name], "source_type") power_source_type = data["it"][_PMD.pmd_it_name]["source_type"] for nw in nw_ids_pmd - # Assign the pump loads at every multinetwork index. + # Assign the pump loads at appropriate multinetwork indices. _assign_pump_loads!(data, power_source_type, nw) end end -function _assign_pump_loads!(data::Dict{String, Any}, power_source_type::String, nw::String) +function _assign_pump_loads!(data::Dict{String,Any}, power_source_type::String, nw::String) for pump_load in values(data["it"]["dep"]["nw"][nw]["pump_load"]) # Change the indices of the pump to match network subdataset. pump_name = pump_load["pump"]["source_id"] @@ -71,7 +83,14 @@ end function networks_are_consistent(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}) - return get_num_networks_pmd(p_data) == _IM.get_num_networks(w_data) + num_networks_pmd = get_num_networks_pmd(p_data) + num_networks_wm = _IM.get_num_networks(w_data) + + if num_networks_pmd == 1 && num_networks_wm == 1 + return true + else + return num_networks_pmd + 1 == num_networks_wm + end end @@ -95,7 +114,7 @@ function make_multinetwork(data::Dict{String,<:Any}) # Parse the PowerModelsDistribution data. pmd_data = _PMD.get_pmd_data(data) pmd_source_type = pmd_data["source_type"] - + # If the network comes from OpenDSS data, transform to a mathematical model. if !(haskey(pmd_data, "source_type") && pmd_data["source_type"] == "matpower") pmd_data = _PMD.transform_data_model(pmd_data; multinetwork = true) @@ -123,26 +142,24 @@ function make_multinetwork(data::Dict{String,<:Any}) w_data_tmp = translate_w ? _WM.make_multinetwork(wm_data) : wm_data if translate_p - p_data_tmp = _replicate_power_data(pmd_data, num_steps_w) + p_data_tmp = _replicate_power_data(pmd_data, num_steps_w - 1) else - if num_steps_p == num_steps_w - p_data_tmp = pmd_data - elseif num_steps_p == 1 - # Get water and power network indices. - nw_id_pmd = collect(keys(pmd_data["nw"]))[1] - nw_ids_wm = collect(keys(w_data_tmp["nw"])) - - # Assume the same power properties across all subnetworks. - p_data_tmp = deepcopy(pmd_data) - p_data_tmp["nw"] = Dict(nw => deepcopy( - pmd_data["nw"][nw_id_pmd]) for nw in nw_ids_wm) - else - Memento.error(_LOGGER, "Multinetworks cannot be reconciled.") - end + # Get water and power network indices. + nw_id_pmd = collect(keys(pmd_data["nw"]))[1] + nw_ids_wm = collect(keys(w_data_tmp["nw"])) + + + # Assume the same power properties across all subnetworks. + p_data_tmp = deepcopy(pmd_data) + nw_ids_wm_int = sort([parse(Int, x) for x in nw_ids_wm]) + p_data_tmp["nw"] = Dict( + string(nw) => deepcopy(pmd_data["nw"][nw_id_pmd]) for + nw in nw_ids_wm_int[1:end-1] + ) end elseif num_steps_p > 1 && num_steps_w == 1 p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data - w_data_tmp = translate_w ? _WM.replicate(wm_data, num_steps_p) : wm_data + w_data_tmp = translate_w ? _WM.replicate(wm_data, num_steps_p + 1) : wm_data else p_data_tmp = translate_p ? _make_power_multinetwork(pmd_data) : pmd_data w_data_tmp = translate_w ? _WM.make_multinetwork(wm_data) : wm_data @@ -188,7 +205,7 @@ function _modify_loads!(data::Dict{String,<:Any}) nw_ids_pmd = sort([parse(Int, x) for x in collect(keys(p_data["nw"]))]) nw_ids_wm = sort([parse(Int, x) for x in collect(keys(w_data["nw"]))]) nw_ids_inner = length(nw_ids_wm) > 1 ? nw_ids_wm[1:end-1] : nw_ids_wm - @assert nw_ids_pmd == nw_ids_wm + @assert nw_ids_pmd == nw_ids_inner # Get important scaling data. base_mass = get(w_data, "base_mass", 1.0) @@ -212,8 +229,8 @@ function _modify_loads!(data::Dict{String,<:Any}) # Change the loads associated with pumps. load = p_data["nw"][string(nw)]["load"][string(pump_load["load"]["index"])] load_power = inv(sum(x -> x > 0.0, load["pd"])) * factor * P_max - load["pd"][load["pd"] .> 0.0] .= load_power - load["qd"][load["qd"] .> 0.0] .= 0.0 # Assume no reactive load. + load["pd"][load["pd"].>0.0] .= load_power + load["qd"][load["qd"].>0.0] .= 0.0 # Assume no reactive load. end end end @@ -239,8 +256,8 @@ function _get_power_conversion_factor(data::Dict{String,<:Any}, nw::String)::Flo end # Watts per PowerModelsDistribution power unit. - base_power_pmd = 1.0e6 * base_mva_pmd - + base_power_pmd = 1.0e6 * base_mva_pmd + # Get the conversion factor for power used by the water network. data_wm = _WM.get_wm_data(data) transform_mass = _WM._calc_mass_per_unit_transform(data_wm) diff --git a/src/prob/linking.jl b/src/prob/linking.jl index 79a85cc..13a9fc1 100644 --- a/src/prob/linking.jl +++ b/src/prob/linking.jl @@ -1,10 +1,8 @@ function build_linking(pwm::AbstractPowerWaterModel) # Get important data that will be used in the modeling loop. pmd = _get_powermodel_from_powerwatermodel(pwm) - nw_ids = sort(collect(_IM.nw_ids(pwm, :dep))) - nw_ids_inner = length(nw_ids) > 1 ? nw_ids[1:end-1] : nw_ids - for nw in nw_ids_inner + for nw in _IM.nw_ids(pwm, :dep) # Obtain all pump loads at multinetwork index. pump_loads = _IM.ref(pwm, :dep, nw, :pump_load) From 66d3b37517393aa8afc89851879931f96746495d Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Tue, 31 Aug 2021 17:26:40 -0600 Subject: [PATCH 17/19] Update WaterModels version in Project.toml. --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 5bc2a3d..9dabe0b 100644 --- a/Project.toml +++ b/Project.toml @@ -14,7 +14,7 @@ WaterModels = "7c60b362-08f4-5b14-8680-cd67a3e18348" InfrastructureModels = "~0.6" PowerModels = "~0.18" PowerModelsDistribution = "~0.11" -WaterModels = "~0.7" +WaterModels = "~0.8" julia = "^1" [extras] From f9f5c826f5201b8fcf678737e4505e79d9622721 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Thu, 2 Sep 2021 15:08:47 -0600 Subject: [PATCH 18/19] Update WaterModels version. --- Project.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 5bc2a3d..f0875bc 100644 --- a/Project.toml +++ b/Project.toml @@ -2,7 +2,7 @@ name = "PowerWaterModels" uuid = "7f4f7f52-2f44-4c87-b9ed-462dc784f1b2" authors = ["Byron Tasseff"] repo = "https://github.com/lanl-ansi/PowerWaterModels.jl" -version = "0.1.0" +version = "0.2.0" [deps] InfrastructureModels = "2030c09a-7f63-5d83-885d-db604e0e9cc0" @@ -14,7 +14,7 @@ WaterModels = "7c60b362-08f4-5b14-8680-cd67a3e18348" InfrastructureModels = "~0.6" PowerModels = "~0.18" PowerModelsDistribution = "~0.11" -WaterModels = "~0.7" +WaterModels = "~0.8" julia = "^1" [extras] From 8605961c995c1e5e4e2f04a59c68d58175b2f4c7 Mon Sep 17 00:00:00 2001 From: Byron Tasseff Date: Thu, 2 Sep 2021 18:40:10 -0600 Subject: [PATCH 19/19] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index f0875bc..4e1e5ef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,6 +1,6 @@ name = "PowerWaterModels" uuid = "7f4f7f52-2f44-4c87-b9ed-462dc784f1b2" -authors = ["Byron Tasseff"] +authors = ["Byron Tasseff", "Russell Bent", "Carleton Coffrin"] repo = "https://github.com/lanl-ansi/PowerWaterModels.jl" version = "0.2.0"