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/Project.toml b/Project.toml index 34c8a78..4e1e5ef 100644 --- a/Project.toml +++ b/Project.toml @@ -1,16 +1,20 @@ 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.1.0" +version = "0.2.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] -PowerModelsDistribution = "~0.9" -WaterModels = "~0.6" +InfrastructureModels = "~0.6" +PowerModels = "~0.18" +PowerModelsDistribution = "~0.11" +WaterModels = "~0.8" julia = "^1" [extras] 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/PowerWaterModels.jl b/src/PowerWaterModels.jl index 73759b7..b5c8e02 100644 --- a/src/PowerWaterModels.jl +++ b/src/PowerWaterModels.jl @@ -1,13 +1,15 @@ module PowerWaterModels + import InfrastructureModels + import InfrastructureModels: optimize_model!, @im_fields, ismultinetwork, 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 = _PMD._IM # InfrastructureModels - const _PM = _PMD._PM # PowerModels + const _IM = InfrastructureModels const _MOI = _IM._MOI # MathOptInterface # Borrow dependencies from other packages. @@ -37,13 +39,20 @@ module PowerWaterModels Memento.config!(Memento.getlogger("PowerWaterModels"), level) end + 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/helpers.jl") include("core/constraint.jl") include("core/objective.jl") + include("core/types.jl") + include("prob/linking.jl") include("prob/pwf.jl") include("prob/opwf.jl") diff --git a/src/core/base.jl b/src/core/base.jl index 8a6d3bd..263f966 100644 --- a/src/core/base.jl +++ b/src/core/base.jl @@ -1,154 +1,164 @@ -""" - 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...) +"Root of the PowerWaterModels formulation hierarchy." +abstract type AbstractPowerWaterModel{ + T1<:_PMD.AbstractUnbalancedPowerModel, + T2<:_WM.AbstractWaterModel, +} <: _IM.AbstractInfrastructureModel end + + +"A macro for adding the base PowerWaterModels fields to a type definition." +_IM.@def pwm_fields begin + PowerWaterModels.@im_fields 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) + data::Dict{String,<:Any}, + model_type::Type, + 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 - # 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) + 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. - - power_result = _IM.optimize_model!( - pm, optimizer=optimizer, solution_processors=pm_solution_processors) + start_time = time() - water_result = _IM.build_result( - wm, power_result["solve_time"]; solution_processors=wm_solution_processors) + solution_processors = transform_solution_processors(pwm, solution_processors) - # Create a combined power-water result object. - result = power_result # Contains most of the result data. + result = _IM.optimize_model!( + pwm, + optimizer = optimizer, + solution_processors = solution_processors, + relax_integrality = relax_integrality, + ) - # 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 PowerModelsDistribution 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/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/constraint.jl b/src/core/constraint.jl index cd8b26f..b8e2d28 100644 --- a/src/core/constraint.jl +++ b/src/core/constraint.jl @@ -1,23 +1,27 @@ -function constraint_fixed_load(pm::_PM.AbstractPowerModel, 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(pm::_PM.AbstractPowerModel, 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) +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.data, string(nw)) + JuMP.@constraint(pwm.model, factor * pump_load == power_load) end -function _get_power_load_expression(pm::_PM.AbstractPowerModel, 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::_PM.AbstractPowerModel, 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 b44c232..8c03587 100644 --- a/src/core/data.jl +++ b/src/core/data.jl @@ -1,111 +1,272 @@ +function correct_network_data!(data::Dict{String,Any}) + # 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) +end + + +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"], + ) + else + return findfirst(x -> x["source_id"] == lowercase(name), pmd_data["nw"][nw]["load"]) + end +end + + +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 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"]))) + + 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 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) + 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]["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]["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"] = parse(Int, 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) + 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 + + +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_multinetworks(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}) +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(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 + + # 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 - 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 + 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 - 1) + else + # 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(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 + 1) : 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 ? _WM.make_multinetwork(wm_data) : wm_data end - # Return the (potentially modified) power and water networks. - return p_data_tmp, w_data_tmp -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 -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) - else - return _PMD.transform_data_model(p_data; build_multinetwork=true) + # 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 -function _replicate_power_data(p_data::Dict{String,<:Any}, num_networks::Int64) +function _make_power_multinetwork(p_data::Dict{String,<:Any}) if haskey(p_data, "source_type") && p_data["source_type"] == "matpower" - return _IM.replicate(p_data, num_networks, _PM._pm_global_keys) + return _IM.make_multinetwork(p_data, _PMD.pmd_it_name, _PMD._pmd_global_keys) else - return _IM.replicate(p_data, num_networks, _PMD._pmd_math_global_keys) + return _PMD.transform_data_model(p_data; multinetwork = true) end 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] +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 -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 +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] - return p_data["load"][load_id] -end + # 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_inner + # 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) -function _modify_loads(p_data::Dict{String,<:Any}, w_data::Dict{String,<:Any}, pw_data::Dict{String,<:Any}) - # 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 + rho_s = _WM._calc_scaled_density(base_mass, base_length) + g_s = _WM._calc_scaled_gravity(base_length, base_time) - for nw in keys(p_data["nw"]) # Loop over all subnetworks. + for nw in nw_ids_inner # Loop over all subnetworks. # Where pumps are linked to power network components, change the loads. - for link in pw_data["power_water_links"] - # 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)) - # 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["pd"][load["pd"] .> 0.0] .= load_power - load["qd"][load["qd"] .> 0.0] .= 0.0 # Assume no reactive load. + 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) - # Add an index variable for the pump within the load object. - load["pump_id"] = pump["index"] + # 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. 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/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 75c663a..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::_PM.AbstractPowerModel) - 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 new file mode 100644 index 0000000..b17e120 --- /dev/null +++ b/src/core/types.jl @@ -0,0 +1,3 @@ +mutable struct PowerWaterModel{T1,T2} <: AbstractPowerWaterModel{T1,T2} + @pwm_fields +end diff --git a/src/io/common.jl b/src/io/common.jl index 88ea93c..0458589 100644 --- a/src/io/common.jl +++ b/src/io/common.jl @@ -1,37 +1,71 @@ """ - 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) + if split(file_path, ".")[end] == "m" # If reading a MATPOWER file. + data = _PM.parse_file(file_path) + _scale_loads!(data, 1.0 / 3.0) + _PMD.make_multiconductor!(data, 3) + else + data = _PMD.parse_file(file_path) + end + + 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)) + 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) + + # Make the power and water data sets multinetwork. + joint_network_data_mn = make_multinetwork(joint_network_data) - # 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. + # Prepare and correct pump load linking data. + assign_pump_loads!(joint_network_data_mn) - # Create new network data, where network sizes match. - p_data, w_data = make_multinetworks(p_data, w_data) + # Modify variable load properties in the power network. + _modify_loads!(joint_network_data_mn) - # Return three data dictionaries. - return p_data, w_data, pw_data + # Return the network dictionary. + return joint_network_data_mn 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/src/prob/linking.jl b/src/prob/linking.jl new file mode 100644 index 0000000..13a9fc1 --- /dev/null +++ b/src/prob/linking.jl @@ -0,0 +1,25 @@ +function build_linking(pwm::AbstractPowerWaterModel) + # Get important data that will be used in the modeling loop. + pmd = _get_powermodel_from_powerwatermodel(pwm) + + for nw in _IM.nw_ids(pwm, :dep) + # 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 512dd31..9a22a66 100644 --- a/src/prob/opwf.jl +++ b/src/prob/opwf.jl @@ -1,34 +1,31 @@ # 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 + + +"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(pm::_PM.AbstractPowerModel, 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) - # Get all loads defined in the power network. - loads = _PMD.ref(pm, 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) - 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) - end - end + # Power-water linking constraints. + build_linking(pwm) # Add the objective that minimizes power generation costs. - _PM.objective_min_fuel_cost(pm) + _PMD.objective_mc_min_fuel_cost(pmd) end diff --git a/src/prob/pwf.jl b/src/prob/pwf.jl index 12217a3..5a85c92 100644 --- a/src/prob/pwf.jl +++ b/src/prob/pwf.jl @@ -1,34 +1,31 @@ # 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 + + +"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(pm::_PM.AbstractPowerModel, 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) - # Get all loads defined in the power network. - loads = _PMD.ref(pm, 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) - 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) - end - end + # Power-water linking constraints. + build_linking(pwm) # 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 aaa84d2..d55c602 100644 --- a/test/base.jl +++ b/test/base.jl @@ -1,36 +1,31 @@ @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) - @test pm.model == wm.model + 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 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 - 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) + 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 "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 31286dd..f343448 100644 --- a/test/data.jl +++ b/test/data.jl @@ -1,56 +1,66 @@ +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_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) + @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) + + # 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) + 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) + 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) + 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 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 + 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) - @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) + # 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_throws ErrorException parse_files(power_path, water_path, link_path) 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 = _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/data/json/case2-pump.json b/test/data/json/case2-pump.json index 64c1771..e261128 100644 --- a/test/data/json/case2-pump.json +++ b/test/data/json/case2-pump.json @@ -1,14 +1,23 @@ { - "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 + } + } + }, + "pmd": { + "source_type": "opendss" + }, + "wm": { + "source_type": "epanet" } - ], - "power_metadata": { - "source_type": "opendss" - }, - "water_metadata": { - "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 diff --git a/test/data/json/case3-pump.json b/test/data/json/case3-pump.json index 1e9d802..1f64278 100644 --- a/test/data/json/case3-pump.json +++ b/test/data/json/case3-pump.json @@ -1,14 +1,23 @@ { - "power_water_links": [ - { - "load_source_id": "3", - "pump_source_id": "1" + "it": { + "dep": { + "pump_load": { + "1": { + "pump": { + "source_id": "1" + }, + "load": { + "source_id": "3" + }, + "status": 1 + } + } + }, + "pmd": { + "source_type": "matpower" + }, + "wm": { + "source_type": "epanet" } - ], - "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..a646d73 100644 --- a/test/io.jl +++ b/test/io.jl @@ -1,27 +1,61 @@ @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"] == "3" + @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"] == "3" + @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/matpower/case3.m" + data = parse_power_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_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/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) + + @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/objective.jl b/test/objective.jl index 30e732a..e8f4cf6 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 = 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 d363985..0f48c0b 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, 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 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, 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 6d4adf6..ef9b96a 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, ipopt; relax_integrality = true) @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, ipopt; relax_integrality = true) @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 2a39f68..1c76347 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,6 +1,6 @@ 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 @@ -13,7 +13,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 +26,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,12 +53,12 @@ wm_path = joinpath(dirname(pathof(_WM)), "..") include("PowerWaterModels.jl") - include("base.jl") - include("io.jl") include("data.jl") + include("base.jl") + include("objective.jl") include("pwf.jl")